Autopsy  4.19.3
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  */
19 package org.sleuthkit.autopsy.directorytree;
20 
21 import com.fasterxml.jackson.databind.ObjectWriter;
22 import com.fasterxml.jackson.databind.SequenceWriter;
23 import com.fasterxml.jackson.dataformat.csv.CsvMapper;
24 import com.fasterxml.jackson.dataformat.csv.CsvSchema;
25 import java.awt.Component;
26 import java.awt.event.ActionEvent;
27 import java.io.File;
28 import java.lang.reflect.InvocationTargetException;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Calendar;
32 import java.util.Collection;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.concurrent.ExecutionException;
40 import java.util.logging.Level;
41 import javax.swing.AbstractAction;
42 import javax.swing.JFileChooser;
43 import javax.swing.JOptionPane;
44 import javax.swing.SwingWorker;
45 import javax.swing.filechooser.FileNameExtensionFilter;
46 import org.netbeans.api.progress.ProgressHandle;
47 import org.openide.util.Cancellable;
48 import org.openide.util.NbBundle;
49 import org.openide.util.Utilities;
55 import org.openide.nodes.Node;
56 import org.openide.nodes.Node.PropertySet;
57 import org.openide.nodes.Node.Property;
59 
63 public 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) {
88  instance = new ExportCSVAction();
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 
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 
220  return userDefinedExportPath;
221  }
222 
232  private static void updateExportDirectory(String exportPath, Case openCase) {
233  if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) {
234  userDefinedExportPath = null;
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 }
static synchronized ExportCSVAction getInstance()
static void updateExportDirectory(String exportPath, Case openCase)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void saveNodesToCSV(Collection<?extends Node > nodesToExport, Component component)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Jun 27 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.