View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2015-2017 ForgeRock AS.
15   */
16  package org.forgerock.audit.handlers.csv;
17  
18  import static java.lang.String.format;
19  import static org.forgerock.audit.events.AuditEventHelper.ARRAY_TYPE;
20  import static org.forgerock.audit.events.AuditEventHelper.OBJECT_TYPE;
21  import static org.forgerock.audit.events.AuditEventHelper.dotNotationToJsonPointer;
22  import static org.forgerock.audit.events.AuditEventHelper.getAuditEventProperties;
23  import static org.forgerock.audit.events.AuditEventHelper.getAuditEventSchema;
24  import static org.forgerock.audit.events.AuditEventHelper.getPropertyType;
25  import static org.forgerock.audit.events.AuditEventHelper.jsonPointerToDotNotation;
26  import static org.forgerock.audit.util.JsonSchemaUtils.generateJsonPointers;
27  import static org.forgerock.audit.util.JsonValueUtils.JSONVALUE_FILTER_VISITOR;
28  import static org.forgerock.audit.util.JsonValueUtils.expand;
29  import static org.forgerock.json.JsonValue.field;
30  import static org.forgerock.json.JsonValue.json;
31  import static org.forgerock.json.JsonValue.object;
32  import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_ID;
33  import static org.forgerock.json.resource.Responses.newQueryResponse;
34  import static org.forgerock.json.resource.Responses.newResourceResponse;
35  import static org.forgerock.util.Utils.isNullOrEmpty;
36  
37  import java.io.File;
38  import java.io.FileReader;
39  import java.io.IOException;
40  import java.security.NoSuchAlgorithmException;
41  import java.security.SecureRandom;
42  import java.util.ArrayList;
43  import java.util.Collection;
44  import java.util.Collections;
45  import java.util.HashMap;
46  import java.util.HashSet;
47  import java.util.LinkedHashMap;
48  import java.util.LinkedHashSet;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.Random;
52  import java.util.Set;
53  import java.util.concurrent.ConcurrentHashMap;
54  import java.util.concurrent.ConcurrentMap;
55  
56  import javax.inject.Inject;
57  
58  import org.forgerock.audit.Audit;
59  import org.forgerock.audit.events.EventTopicsMetaData;
60  import org.forgerock.audit.events.handlers.AuditEventHandlerBase;
61  import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.CsvSecurity;
62  import org.forgerock.audit.handlers.csv.CsvAuditEventHandlerConfiguration.EventBufferingConfiguration;
63  import org.forgerock.audit.providers.KeyStoreHandlerProvider;
64  import org.forgerock.audit.retention.TimeStampFileNamingPolicy;
65  import org.forgerock.audit.secure.JcaKeyStoreHandler;
66  import org.forgerock.audit.secure.KeyStoreHandler;
67  import org.forgerock.audit.util.JsonValueUtils;
68  import org.forgerock.json.JsonPointer;
69  import org.forgerock.json.JsonValue;
70  import org.forgerock.json.resource.ActionRequest;
71  import org.forgerock.json.resource.ActionResponse;
72  import org.forgerock.json.resource.BadRequestException;
73  import org.forgerock.json.resource.InternalServerErrorException;
74  import org.forgerock.json.resource.NotFoundException;
75  import org.forgerock.json.resource.QueryFilters;
76  import org.forgerock.json.resource.QueryRequest;
77  import org.forgerock.json.resource.QueryResourceHandler;
78  import org.forgerock.json.resource.QueryResponse;
79  import org.forgerock.json.resource.ResourceException;
80  import org.forgerock.json.resource.ResourceResponse;
81  import org.forgerock.json.resource.Responses;
82  import org.forgerock.services.context.Context;
83  import org.forgerock.util.Reject;
84  import org.forgerock.util.promise.Promise;
85  import org.forgerock.util.query.QueryFilter;
86  import org.forgerock.util.time.Duration;
87  import org.slf4j.Logger;
88  import org.slf4j.LoggerFactory;
89  import org.supercsv.cellprocessor.Optional;
90  import org.supercsv.cellprocessor.ift.CellProcessor;
91  import org.supercsv.io.CsvMapReader;
92  import org.supercsv.io.ICsvMapReader;
93  import org.supercsv.prefs.CsvPreference;
94  import org.supercsv.quote.AlwaysQuoteMode;
95  import org.supercsv.util.CsvContext;
96  
97  import com.fasterxml.jackson.databind.ObjectMapper;
98  
99  /**
100  * Handles AuditEvents by writing them to a CSV file.
101  */
102 public class CsvAuditEventHandler extends AuditEventHandlerBase {
103 
104     private static final Logger LOGGER = LoggerFactory.getLogger(CsvAuditEventHandler.class);
105 
106     /** Name of action to force file rotation. */
107     public static final String ROTATE_FILE_ACTION_NAME = "rotate";
108 
109     static final String SECURE_CSV_FILENAME_PREFIX = "tamper-evident-";
110 
111     private static final ObjectMapper MAPPER = new ObjectMapper();
112     private static final Random RANDOM;
113 
114     static {
115         try {
116             RANDOM = SecureRandom.getInstance("SHA1PRNG");
117         } catch (NoSuchAlgorithmException ex) {
118             throw new RuntimeException(ex);
119         }
120     }
121 
122     private final CsvAuditEventHandlerConfiguration configuration;
123     private final CsvPreference csvPreference;
124     private final ConcurrentMap<String, CsvWriter> writers = new ConcurrentHashMap<>();
125     private final Map<String, Set<String>> fieldOrderByTopic;
126     /** Caches a JSON pointer for each field. */
127     private final Map<String, JsonPointer> jsonPointerByField;
128     /** Caches the dot notation for each field. */
129     private final Map<String, String> fieldDotNotationByField;
130     private KeyStoreHandler keyStoreHandler;
131 
132     /**
133      * Create a new CsvAuditEventHandler instance.
134      *
135      * @param configuration
136      *          Configuration parameters that can be adjusted by system administrators.
137      * @param eventTopicsMetaData
138      *          Meta-data for all audit event topics.
139      * @param keyStoreHandlerProvider
140      *          The secure storage to use for keys.
141      */
142     @Inject
143     public CsvAuditEventHandler(
144             final CsvAuditEventHandlerConfiguration configuration,
145             final EventTopicsMetaData eventTopicsMetaData,
146             @Audit KeyStoreHandlerProvider keyStoreHandlerProvider) {
147 
148         super(configuration.getName(), eventTopicsMetaData, configuration.getTopics(), configuration.isEnabled());
149         this.configuration = configuration;
150         this.csvPreference = createCsvPreference(this.configuration);
151         CsvSecurity security = configuration.getSecurity();
152         if (security.isEnabled()) {
153             Duration duration = security.getSignatureIntervalDuration();
154             Reject.ifTrue(duration.isZero() || duration.isUnlimited(),
155                     "The signature interval can't be zero or unlimited");
156             Reject.ifFalse(
157                     !isNullOrEmpty(security.getKeyStoreHandlerName())
158                             ^ (!isNullOrEmpty(security.getFilename()) && !isNullOrEmpty(security.getPassword())),
159                     "Either keyStoreHandlerName or filename/password security settings must be specified, "
160                             + "but not both");
161 
162             if (security.getKeyStoreHandlerName() != null) {
163                 this.keyStoreHandler = keyStoreHandlerProvider.getKeystoreHandler(security.getKeyStoreHandlerName());
164                 Reject.ifTrue(keyStoreHandler == null,
165                         "No keystore configured for keyStoreHandlerName: "
166                                 + security.getKeyStoreHandlerName());
167             } else {
168                 try {
169                     keyStoreHandler = new JcaKeyStoreHandler(CsvSecureConstants.KEYSTORE_TYPE, security.getFilename(),
170                             security.getPassword());
171                 } catch (Exception e) {
172                     throw new IllegalArgumentException(
173                             "Unable to create secure storage from file: " + security.getFilename(), e);
174                 }
175             }
176         }
177 
178         Map<String, Set<String>> fieldOrderByTopic = new HashMap<>();
179         Map<String, JsonPointer> jsonPointerByField = new HashMap<>();
180         Map<String, String> fieldDotNotationByField = new HashMap<>();
181         for (String topic : this.eventTopicsMetaData.getTopics()) {
182             try {
183                 Set<String> fieldOrder = getFieldOrder(topic, this.eventTopicsMetaData);
184                 for (String field : fieldOrder) {
185                     if (!jsonPointerByField.containsKey(field)) {
186                         jsonPointerByField.put(field, new JsonPointer(field));
187                         fieldDotNotationByField.put(field, jsonPointerToDotNotation(field));
188                     }
189                 }
190                 fieldOrderByTopic.put(topic, Collections.unmodifiableSet(fieldOrder));
191             } catch (ResourceException e) {
192                 LOGGER.error(topic + " topic schema meta-data misconfigured.");
193             }
194         }
195         this.fieldOrderByTopic = Collections.unmodifiableMap(fieldOrderByTopic);
196         this.jsonPointerByField = Collections.unmodifiableMap(jsonPointerByField);
197         this.fieldDotNotationByField = Collections.unmodifiableMap(fieldDotNotationByField);
198     }
199 
200     private CsvPreference createCsvPreference(final CsvAuditEventHandlerConfiguration config) {
201         return new CsvPreference.Builder(
202                 config.getFormatting().getQuoteChar(),
203                 config.getFormatting().getDelimiterChar(),
204                 config.getFormatting().getEndOfLineSymbols())
205                 .useQuoteMode(new AlwaysQuoteMode())
206                 .build();
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     @Override
213     public void startup() throws ResourceException {
214         LOGGER.trace("Audit logging to: {}", configuration.getLogDirectory());
215         File file = new File(configuration.getLogDirectory());
216         if (!file.isDirectory()) {
217             if (file.exists()) {
218                 LOGGER.warn("Specified path is file but should be a directory: {}", configuration.getLogDirectory());
219             } else {
220                 if (!file.mkdirs()) {
221                     LOGGER.warn("Unable to create audit directory in the path: {}", configuration.getLogDirectory());
222                 }
223             }
224         }
225         for (String topic : eventTopicsMetaData.getTopics()) {
226             File auditLogFile = getAuditLogFile(topic);
227             try {
228                 openWriter(topic, auditLogFile);
229             } catch (IOException e) {
230                 LOGGER.error("Error when creating audit file: {}", auditLogFile, e);
231             }
232         }
233     }
234 
235     /** {@inheritDoc} */
236     @Override
237     public void shutdown() throws ResourceException {
238         cleanup();
239     }
240 
241     /**
242      * Create a csv audit log entry.
243      * {@inheritDoc}
244      */
245     @Override
246     public Promise<ResourceResponse, ResourceException> publishEvent(Context context, String topic, JsonValue event) {
247         try {
248             checkTopic(topic);
249             publishEventWithRetry(topic, event);
250             return newResourceResponse(
251                     event.get(ResourceResponse.FIELD_CONTENT_ID).asString(), null, event).asPromise();
252         } catch (ResourceException e) {
253             return e.asPromise();
254         }
255     }
256 
257     private void checkTopic(String topic) throws ResourceException {
258         final JsonValue auditEventProperties = getAuditEventProperties(eventTopicsMetaData.getSchema(topic));
259         if (auditEventProperties == null || auditEventProperties.isNull()) {
260             throw new InternalServerErrorException("No audit event properties defined for audit event: " + topic);
261         }
262     }
263 
264     /**
265      * Publishes the provided event, and returns the writer used.
266      */
267     private void publishEventWithRetry(final String topic, final JsonValue event)
268                     throws ResourceException {
269         final CsvWriter csvWriter = getWriter(topic);
270         try {
271             writeEvent(topic, csvWriter, event);
272         } catch (IOException ex) {
273             // Re-try once in case the writer stream became closed for some reason
274             LOGGER.debug("IOException while writing ({})", ex.getMessage());
275             CsvWriter newCsvWriter;
276             // An IOException may be thrown if the csvWriter reference we have above was reset by another thread.
277             // Synchronize to ensure that we wait for any reset to complete before proceeding - Otherwise, we may
278             // lose multiple events or have multiple threads attempting to reset the writer.
279             synchronized (this) {
280                 // Lookup the current writer directly from the map so we can check if another thread has reset it.
281                 newCsvWriter = writers.get(topic);
282                 if (newCsvWriter == csvWriter) {
283                     // If both references are the same, the writer hasn't been reset.
284                     newCsvWriter = resetAndReopenWriter(topic, false);
285                     LOGGER.debug("Resetting writer");
286                 } else {
287                     LOGGER.debug("Writer reset by another thread");
288                 }
289             }
290             try {
291                 writeEvent(topic, newCsvWriter, event);
292             } catch (IOException e) {
293                 throw new BadRequestException(e);
294             }
295         }
296     }
297 
298     /**
299      * Lookup CsvWriter for specified topic.
300      * <br/>
301      * Uses lazy synchronization in case another thread may be resetting the writer. If the writer is still null
302      * after synchronizing then the writer is reset.
303      * <br/>
304      * This method is only intended for use by {@link #publishEventWithRetry(String, JsonValue)}.
305      */
306     private CsvWriter getWriter(String topic) throws BadRequestException {
307         CsvWriter csvWriter = writers.get(topic);
308         if (csvWriter == null) {
309             LOGGER.debug("CSV file writer for {} topic is null; checking for reset by another thread", topic);
310             synchronized (this) {
311                 csvWriter = writers.get(topic);
312                 if (csvWriter == null) {
313                     LOGGER.debug("CSV file writer for {} topic not reset by another thread; resetting", topic);
314                     csvWriter = resetAndReopenWriter(topic, false);
315                 }
316             }
317         }
318         return csvWriter;
319     }
320 
321     private CsvWriter writeEvent(final String topic, CsvWriter csvWriter, final JsonValue event)
322                     throws IOException {
323         writeEntry(topic, csvWriter, event);
324         EventBufferingConfiguration bufferConfig = configuration.getBuffering();
325         if (!bufferConfig.isEnabled() || !bufferConfig.isAutoFlush()) {
326             csvWriter.flush();
327         }
328         return csvWriter;
329     }
330 
331     private Set<String> getFieldOrder(final String topic, final EventTopicsMetaData eventTopicsMetaData)
332             throws ResourceException {
333         final Set<String> fieldOrder = new LinkedHashSet<>();
334         fieldOrder.addAll(generateJsonPointers(getAuditEventSchema(eventTopicsMetaData.getSchema(topic))));
335         return fieldOrder;
336     }
337 
338     private synchronized CsvWriter openWriter(final String topic, final File auditFile) throws IOException {
339         final CsvWriter writer = createCsvWriter(auditFile, topic);
340         writers.put(topic, writer);
341         return writer;
342     }
343 
344     private synchronized CsvWriter createCsvWriter(final File auditFile, String topic) throws IOException {
345         String[] headers = buildHeaders(fieldOrderByTopic.get(topic));
346         if (configuration.getSecurity().isEnabled()) {
347             return new SecureCsvWriter(auditFile, headers, csvPreference, configuration, keyStoreHandler, RANDOM);
348         } else {
349             return new StandardCsvWriter(auditFile, headers, csvPreference, configuration);
350         }
351     }
352 
353     private ICsvMapReader createCsvMapReader(final File auditFile) throws IOException {
354         CsvMapReader csvReader = new CsvMapReader(new FileReader(auditFile), csvPreference);
355 
356         if (configuration.getSecurity().isEnabled()) {
357             return new CsvSecureMapReader(csvReader);
358         } else {
359             return csvReader;
360         }
361     }
362 
363     private String[] buildHeaders(final Collection<String> fieldOrder) {
364         final String[] headers = new String[fieldOrder.size()];
365         fieldOrder.toArray(headers);
366         for (int i = 0; i < headers.length; i++) {
367             headers[i] = jsonPointerToDotNotation(headers[i]);
368         }
369         return headers;
370     }
371 
372     /**
373      * Perform a query on the csv audit log.
374      * {@inheritDoc}
375      */
376     @Override
377     public Promise<QueryResponse, ResourceException> queryEvents(
378             Context context,
379             String topic,
380             QueryRequest query,
381             QueryResourceHandler handler) {
382         try {
383             for (final JsonValue value : getEntries(topic, query.getQueryFilter())) {
384                 handler.handleResource(newResourceResponse(value.get(FIELD_CONTENT_ID).asString(), null, value));
385             }
386             return newQueryResponse().asPromise();
387         } catch (Exception e) {
388             return new BadRequestException(e).asPromise();
389         }
390     }
391 
392     /**
393      * Read from the csv audit log.
394      * {@inheritDoc}
395      */
396     @Override
397     public Promise<ResourceResponse, ResourceException> readEvent(Context context, String topic, String resourceId) {
398         try {
399             final Set<JsonValue> entry = getEntries(topic, QueryFilters.parse("/_id eq \"" + resourceId + "\""));
400             if (entry.isEmpty()) {
401                 throw new NotFoundException(topic + " audit log not found");
402             }
403             final JsonValue resource = entry.iterator().next();
404             return newResourceResponse(resource.get(FIELD_CONTENT_ID).asString(), null, resource).asPromise();
405         } catch (ResourceException e) {
406             return e.asPromise();
407         } catch (IOException e) {
408             return new BadRequestException(e).asPromise();
409         }
410     }
411 
412     @Override
413     public Promise<ActionResponse, ResourceException> handleAction(
414             Context context, String topic, ActionRequest request) {
415         try {
416             String action = request.getAction();
417             if (topic == null) {
418                 return new BadRequestException(format("Topic is required for action %s", action)).asPromise();
419             }
420             if (action.equals(ROTATE_FILE_ACTION_NAME)) {
421                 return handleRotateAction(topic).asPromise();
422             }
423             final String error = format("This action is unknown for the CSV handler: %s", action);
424             return new BadRequestException(error).asPromise();
425         } catch (BadRequestException e) {
426             return e.asPromise();
427         }
428     }
429 
430     private ActionResponse handleRotateAction(String topic)
431             throws BadRequestException {
432         CsvWriter csvWriter = writers.get(topic);
433         if (csvWriter == null) {
434             LOGGER.debug("Unable to rotate file for topic: {}", topic);
435             throw new BadRequestException("Unable to rotate file for topic: " + topic);
436         }
437         if (configuration.getFileRotation().isRotationEnabled()) {
438             try {
439                 if (!csvWriter.forceRotation()) {
440                     throw new BadRequestException("Unable to rotate file for topic: " + topic);
441                 }
442             } catch (IOException e) {
443                 throw new BadRequestException("Error when rotating file for topic: " + topic, e);
444             }
445         } else {
446             // use a default rotation instead
447             resetAndReopenWriter(topic, true);
448         }
449         return Responses.newActionResponse(json(object(field("rotated", "true"))));
450     }
451 
452     private File getAuditLogFile(final String type) {
453         final String prefix = configuration.getSecurity().isEnabled() ? SECURE_CSV_FILENAME_PREFIX : "";
454         return new File(configuration.getLogDirectory(), prefix + type + ".csv");
455     }
456 
457     private void writeEntry(final String topic, final CsvWriter csvWriter, final JsonValue obj) throws IOException {
458         Set<String> fieldOrder = fieldOrderByTopic.get(topic);
459         Map<String, String> cells = new HashMap<>(fieldOrder.size());
460         for (Map.Entry<String, JsonPointer> columnKey : jsonPointerByField.entrySet()) {
461             cells.put(fieldDotNotationByField.get(columnKey.getKey()),
462                     JsonValueUtils.extractValueAsString(obj, columnKey.getValue()));
463         }
464         csvWriter.writeEvent(cells);
465     }
466 
467     private synchronized CsvWriter resetAndReopenWriter(final String topic, boolean forceRotation)
468             throws BadRequestException {
469         closeWriter(topic);
470         try {
471             File auditLogFile = getAuditLogFile(topic);
472             if (forceRotation) {
473                 TimeStampFileNamingPolicy namingPolicy = new TimeStampFileNamingPolicy(auditLogFile, null, null);
474                 File rotatedFile = namingPolicy.getNextName();
475                 if (!auditLogFile.renameTo(rotatedFile)) {
476                     throw new BadRequestException(
477                             format("Unable to rename file %s to %s when rotating", auditLogFile, rotatedFile));
478                 }
479             }
480             return openWriter(topic, auditLogFile);
481         } catch (IOException e) {
482             throw new BadRequestException(e);
483         }
484     }
485 
486     private synchronized void closeWriter(final String topic) {
487         CsvWriter writerToClose = writers.remove(topic);
488         if (writerToClose != null) {
489             // attempt clean-up close
490             try {
491                 writerToClose.close();
492             } catch (Exception ex) {
493                 // Debug level as the writer is expected to potentially be invalid
494                 LOGGER.debug("File writer close in closeWriter reported failure ", ex);
495             }
496         }
497     }
498 
499     /**
500      * Parser the csv file corresponding the the specified audit entry type and returns a set of matching audit entries.
501      *
502      * @param auditEntryType the audit log type
503      * @param queryFilter the query filter to apply to the entries
504      * @return  A audit log entry; null if no entry exists
505      * @throws IOException If unable to get an entry from the CSV file.
506      */
507     private Set<JsonValue> getEntries(final String auditEntryType, QueryFilter<JsonPointer> queryFilter)
508             throws IOException {
509         final File auditFile = getAuditLogFile(auditEntryType);
510         final Set<JsonValue> results = new HashSet<>();
511         if (queryFilter == null) {
512             queryFilter = QueryFilter.alwaysTrue();
513         }
514         if (auditFile.exists()) {
515             try (ICsvMapReader reader = createCsvMapReader(auditFile)) {
516                 // the header elements are used to map the values to the bean (names must match)
517                 final String[] header = convertDotNotationToSlashes(reader.getHeader(true));
518                 final CellProcessor[] processors = createCellProcessors(auditEntryType, header);
519                 Map<String, Object> entry;
520                 while ((entry = reader.read(header, processors)) != null) {
521                     entry = convertDotNotationToSlashes(entry);
522                     final JsonValue jsonEntry = expand(entry);
523                     if (queryFilter.accept(JSONVALUE_FILTER_VISITOR, jsonEntry)) {
524                         results.add(jsonEntry);
525                     }
526                 }
527 
528             }
529         }
530         return results;
531     }
532 
533     private CellProcessor[] createCellProcessors(final String auditEntryType, final String[] headers)
534             throws ResourceException {
535         final List<CellProcessor> cellProcessors = new ArrayList<>();
536         final JsonValue auditEvent = eventTopicsMetaData.getSchema(auditEntryType);
537 
538         for (String header: headers) {
539             final String propertyType = getPropertyType(auditEvent, new JsonPointer(header));
540             if ((propertyType.equals(OBJECT_TYPE) || propertyType.equals(ARRAY_TYPE))) {
541                 cellProcessors.add(new Optional(new ParseJsonValue()));
542             } else {
543                 cellProcessors.add(new Optional());
544             }
545         }
546 
547         return cellProcessors.toArray(new CellProcessor[cellProcessors.size()]);
548     }
549 
550     /**
551      * CellProcessor for parsing JsonValue objects from CSV file.
552      */
553     public class ParseJsonValue implements CellProcessor {
554 
555         @Override
556         public Object execute(final Object value, final CsvContext context) {
557             JsonValue jv = null;
558             // Check if value is JSON object
559             if (((String) value).startsWith("{") && ((String) value).endsWith("}")) {
560                 try {
561                     jv = new JsonValue(MAPPER.readValue((String) value, Map.class));
562                 } catch (Exception e) {
563                     LOGGER.debug("Error parsing JSON string: " + e.getMessage());
564                 }
565             } else if (((String) value).startsWith("[") && ((String) value).endsWith("]")) {
566                 try {
567                     jv = new JsonValue(MAPPER.readValue((String) value, List.class));
568                 } catch (Exception e) {
569                     LOGGER.debug("Error parsing JSON string: " + e.getMessage());
570                 }
571             }
572             if (jv == null) {
573                 return value;
574             }
575             return jv.getObject();
576         }
577 
578     }
579 
580     private synchronized void cleanup() throws ResourceException {
581         try {
582             for (CsvWriter csvWriter : writers.values()) {
583                 if (csvWriter != null) {
584                     csvWriter.flush();
585                     csvWriter.close();
586                 }
587             }
588         } catch (IOException e) {
589             LOGGER.error("Unable to close filewriters during {} cleanup", this.getClass().getName(), e);
590             throw new InternalServerErrorException(
591                     "Unable to close filewriters during " + this.getClass().getName() + " cleanup", e);
592         }
593     }
594 
595     private Map<String, Object> convertDotNotationToSlashes(final Map<String, Object> entries) {
596         final Map<String, Object> newEntry = new LinkedHashMap<>();
597         for (Map.Entry<String, Object> entry : entries.entrySet()) {
598             final String key = dotNotationToJsonPointer(entry.getKey());
599             newEntry.put(key, entry.getValue());
600         }
601         return newEntry;
602     }
603 
604     private String[] convertDotNotationToSlashes(final String[] entries) {
605         String[] result = new String[entries.length];
606         for (int i = 0; i < entries.length; i++) {
607             result[i] = dotNotationToJsonPointer(entries[i]);
608         }
609         return result;
610     }
611 
612 }