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