Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
ExportCSVAction.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2019-2021 Basis Technology Corp.
5 * Contact: carrier <at> sleuthkit <dot> org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this content except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.sleuthkit.autopsy.directorytree;
20
21import com.fasterxml.jackson.databind.ObjectWriter;
22import com.fasterxml.jackson.databind.SequenceWriter;
23import com.fasterxml.jackson.dataformat.csv.CsvMapper;
24import com.fasterxml.jackson.dataformat.csv.CsvSchema;
25import java.awt.Component;
26import java.awt.event.ActionEvent;
27import java.io.File;
28import java.lang.reflect.InvocationTargetException;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Calendar;
32import java.util.Collection;
33import java.util.HashMap;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.List;
37import java.util.Map;
38import java.util.Set;
39import java.util.concurrent.ExecutionException;
40import java.util.logging.Level;
41import javax.swing.AbstractAction;
42import javax.swing.JFileChooser;
43import javax.swing.JOptionPane;
44import javax.swing.SwingWorker;
45import javax.swing.filechooser.FileNameExtensionFilter;
46import org.netbeans.api.progress.ProgressHandle;
47import org.openide.util.Cancellable;
48import org.openide.util.NbBundle;
49import org.openide.util.Utilities;
50import org.sleuthkit.autopsy.casemodule.Case;
51import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
52import org.sleuthkit.autopsy.coreutils.Logger;
53import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
54import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType;
55import org.openide.nodes.Node;
56import org.openide.nodes.Node.PropertySet;
57import org.openide.nodes.Node.Property;
58import org.sleuthkit.autopsy.guiutils.JFileChooserFactory;
59
63public final class ExportCSVAction extends AbstractAction {
64 // number of rows to sample for different columns
65 private static final int COLUMN_SAMPLING_ROW_NUM = 100;
66 private static final Logger logger = Logger.getLogger(ExportCSVAction.class.getName());
67 private final static String DEFAULT_FILENAME = "Results";
68 private final static List<String> columnsToSkip = Arrays.asList(AbstractFilePropertyType.SCORE.toString(),
70
71 private static String userDefinedExportPath;
72
73 // This class is a singleton to support multi-selection of nodes, since
74 // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every
75 // node in the array returns a reference to the same action object from Node.getActions(boolean).
76 private static ExportCSVAction instance;
77
79
86 public static synchronized ExportCSVAction getInstance() {
87 if (null == instance) {
89 }
90 return instance;
91 }
92
96 @NbBundle.Messages({"ExportCSV.title.text=Export Selected Rows to CSV"})
97 private ExportCSVAction() {
98 super(Bundle.ExportCSV_title_text());
99 }
100
107
108 @Override
109 public void actionPerformed(ActionEvent e) {
110 Collection<? extends Node> selectedNodes = Utilities.actionsGlobalContext().lookupAll(Node.class);
111 saveNodesToCSV(selectedNodes, (Component)e.getSource());
112 }
113
120 @NbBundle.Messages({
121 "# {0} - Output file",
122 "ExportCSV.saveNodesToCSV.fileExists=File {0} already exists",
123 "ExportCSV.saveNodesToCSV.noCurrentCase=No open case available",
124 "ExportCSV.saveNodesToCSV.empty=No data to export"})
125 public static void saveNodesToCSV(Collection<? extends Node> nodesToExport, Component component) {
126
127 if (nodesToExport.isEmpty()) {
128 MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_empty());
129 return;
130 }
131
132 try {
133 // Set up the file chooser with a default name and either the Export
134 // folder or the last used folder.
135 String fileName = getDefaultOutputFileName(nodesToExport.iterator().next().getParentNode());
136 JFileChooser fileChooser = chooserHelper.getChooser();
137 fileChooser.setCurrentDirectory(new File(getExportDirectory(Case.getCurrentCaseThrows())));
138 fileChooser.setSelectedFile(new File(fileName));
139 fileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv"));
140
141 int returnVal = fileChooser.showSaveDialog(component);
142 if (returnVal == JFileChooser.APPROVE_OPTION) {
143
144 // Get the file name, appending .csv if necessary
145 File selectedFile = fileChooser.getSelectedFile();
146 if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS
147 selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS
148 }
149
150 // Save the directory used for next time
151 updateExportDirectory(selectedFile.getParent(), Case.getCurrentCaseThrows());
152
153 if (selectedFile.exists()) {
154 logger.log(Level.SEVERE, "File {0} already exists", selectedFile.getAbsolutePath()); //NON-NLS
155 MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_fileExists(selectedFile));
156 return;
157 }
158
159 CSVWriter writer = new CSVWriter(nodesToExport, selectedFile);
160 writer.execute();
161 }
162 } catch (NoCurrentCaseException ex) {
163 JOptionPane.showMessageDialog(component, Bundle.ExportCSV_saveNodesToCSV_noCurrentCase());
164 logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS
165 }
166 }
167
175 private static String getDefaultOutputFileName(Node parent) {
176 String dateStr = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS", Calendar.getInstance());
177
178 if (parent != null) {
179 // The first value in the property set is generally a reasonable name
180 for (PropertySet set : parent.getPropertySets()) {
181 for (Property<?> prop : set.getProperties()) {
182 try {
183 String parentName = prop.getValue().toString();
184
185 // Strip off the count (if present)
186 parentName = parentName.replaceAll("\\([0-9]+\\)$", "");
187
188 // Strip out any invalid characters
189 parentName = parentName.replaceAll("[\\\\/:*?\"<>|]", "_");
190
191 return parentName + " " + dateStr;
192 } catch (IllegalAccessException | InvocationTargetException ex) {
193 logger.log(Level.WARNING, "Failed to get property set value as string", ex);
194 }
195 }
196 }
197 }
198 return DEFAULT_FILENAME + " " + dateStr;
199 }
200
208 private static String getExportDirectory(Case openCase) {
209 String caseExportPath = openCase.getExportDirectory();
210
211 if (userDefinedExportPath == null) {
212 return caseExportPath;
213 }
214
215 File file = new File(userDefinedExportPath);
216 if (file.exists() == false || file.isDirectory() == false) {
217 return caseExportPath;
218 }
219
221 }
222
232 private static void updateExportDirectory(String exportPath, Case openCase) {
233 if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) {
235 } else {
236 userDefinedExportPath = exportPath;
237 }
238 }
239
240
244 private static class CSVWriter extends SwingWorker<Object, Void> {
245
246 private static final Logger logger = Logger.getLogger(CSVWriter.class.getName());
247 private ProgressHandle progress;
248
249 private final Collection<? extends Node> nodesToExport;
250 private final File outputFile;
251
257 CSVWriter(Collection<? extends Node> nodesToExport, File outputFile) {
258 this.nodesToExport = nodesToExport;
259 this.outputFile = outputFile;
260 }
261
262 @NbBundle.Messages({"CSVWriter.progress.extracting=Exporting to CSV file",
263 "CSVWriter.progress.cancelling=Cancelling"})
264 @Override
265 protected Object doInBackground() throws Exception {
266 if (nodesToExport.isEmpty()) {
267 return null;
268 }
269
270 // Set up progress bar.
271 final String displayName = Bundle.CSVWriter_progress_extracting();
272 progress = ProgressHandle.createHandle(displayName, new Cancellable() {
273 @Override
274 public boolean cancel() {
275 if (progress != null) {
276 progress.setDisplayName(Bundle.CSVWriter_progress_cancelling());
277 }
278 return ExportCSVAction.CSVWriter.this.cancel(true);
279 }
280 });
281 progress.start();
282 progress.switchToIndeterminate();
283
284 if (this.isCancelled()) {
285 return null;
286 }
287
288 Set<String> columnHeaderStrs = new HashSet<>();
289 List<CsvSchema.Column> columnHeaders = new ArrayList<>();
290 int remainingRowsToSample = 0;
291 int columnIdx = 0;
292 for (Node nd: nodesToExport) {
293 // sample up to 100 rows
294 if (remainingRowsToSample >= COLUMN_SAMPLING_ROW_NUM) {
295 break;
296 }
297 remainingRowsToSample++;
298
299 for (PropertySet ps: nd.getPropertySets()) {
300 for (Property prop: ps.getProperties()) {
301 if (!columnHeaderStrs.contains(prop.getDisplayName()) && !columnsToSkip.contains(prop.getName())) {
302 columnHeaderStrs.add(prop.getDisplayName());
303 columnHeaders.add(new CsvSchema.Column(columnIdx, prop.getDisplayName()));
304 columnIdx++;
305 }
306 }
307 }
308 }
309
310 if (this.isCancelled()) {
311 return null;
312 }
313
314 CsvSchema schema = CsvSchema.builder()
315 .addColumns(columnHeaders)
316 .setUseHeader(true)
317 .setNullValue("")
318 .build();
319
320 CsvMapper mapper = new CsvMapper();
321 ObjectWriter writer = mapper.writerFor(Map.class).with(schema);
322 try (SequenceWriter seqWriter = writer.writeValues(outputFile)) {
323 // Write each line
324 Iterator<?> nodeIterator = nodesToExport.iterator();
325 while (nodeIterator.hasNext()) {
326 if (this.isCancelled()) {
327 return null;
328 }
329
330 Map<String, Object> rowMap = new HashMap<>();
331 Node node = (Node)nodeIterator.next();
332 for(PropertySet set : node.getPropertySets()) {
333 for (Property<?> prop : set.getProperties()) {
334 if (!columnsToSkip.contains(prop.getName())) {
335 rowMap.put(prop.getDisplayName(), prop.getValue());
336 }
337 }
338 }
339 seqWriter.write(rowMap);
340 }
341 }
342 return null;
343 }
344
352 private String escapeQuotes(String original) {
353 return original.replaceAll("\"", "\\\\\"");
354 }
355
363 private String listToCSV(List<String> values) {
364 return "\"" + String.join("\",\"", values) + "\"\n";
365 }
366
367 @NbBundle.Messages({"CSVWriter.done.notifyMsg.error=Error exporting to CSV file",
368 "# {0} - Output file",
369 "CSVWriter.done.notifyMsg.success=Wrote to {0}"})
370 @Override
371 protected void done() {
372 boolean msgDisplayed = false;
373 try {
374 super.get();
375 } catch (InterruptedException | ExecutionException ex) {
376 logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS
377 MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_error());
378 msgDisplayed = true;
379 } catch (java.util.concurrent.CancellationException ex) {
380 // catch and ignore if we were cancelled
381 } finally {
382 progress.finish();
383 if (!this.isCancelled() && !msgDisplayed) {
384 MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_success(outputFile));
385 }
386 }
387 }
388 }
389}
synchronized static Logger getLogger(String name)
Definition Logger.java:124
static void updateExportDirectory(String exportPath, Case openCase)
static void saveNodesToCSV(Collection<? extends Node > nodesToExport, Component component)
static synchronized ExportCSVAction getInstance()

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.