diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 32e02fdbca..d6b8aeb280 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -193,14 +193,10 @@ ReviewModeCasePanel.MetadataFileHeaderText=Metadata File OpenMultiUserCasePanel.jLabel1.text=Recent Cases OpenMultiUserCasePanel.openButton.text=Open OpenMultiUserCasePanel.cancelButton.text=Cancel -MultiUserCasesPanel.bnOpen.text=&Open CueBannerPanel.newCaseLabel.text=New Case CueBannerPanel.openCaseButton.text= CueBannerPanel.openCaseLabel.text=Open Case -MultiUserCasesPanel.bnOpenSingleUserCase.text=Open Single-User Case... CueBannerPanel.newCaseButton.text= -MultiUserCasesPanel.searchLabel.text=Select any case and start typing to search by case name -MultiUserCasesPanel.cancelButton.text=Cancel ImageFilePanel.sectorSizeLabel.text=Sector size: LocalDiskPanel.sectorSizeLabel.text=Sector Size: LocalFilesPanel.displayNameLabel.text=Logical File Set Display Name: Default @@ -240,4 +236,7 @@ ImageFilePanel.sha1HashTextField.text= ImageFilePanel.md5HashTextField.text= ImageFilePanel.errorLabel.text=Error Label ImageFilePanel.hashValuesNoteLabel.text=NOTE: These values will not be validated when the data source is added. -ImageFilePanel.hashValuesLabel.text=Hash Values (optional): \ No newline at end of file +ImageFilePanel.hashValuesLabel.text=Hash Values (optional): +OpenMultiUserCasePanel.searchLabel.text=Select any case and start typing to search by case name + +OpenMultiUserCasePanel.bnOpenSingleUserCase.text=Open Single-User Case... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 46e9dad2bc..44f1a893af 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -32,6 +32,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; @@ -57,6 +58,7 @@ import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; +import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; @@ -80,6 +82,7 @@ import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.commonfilesearch.CommonAttributeSearchAction; import org.sleuthkit.autopsy.communications.OpenCommVisualizationToolAction; +import org.sleuthkit.autopsy.coordinationservice.CaseNodeData; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; @@ -1956,9 +1959,9 @@ public class Case { } if (isNewCase) { - createCaseData(progressIndicator); + createCaseData(progressIndicator); // RJCTODO: This name is vague } else { - openCaseData(progressIndicator); + openCaseData(progressIndicator); // RJCTODO: This name is vague } if (Thread.currentThread().isInterrupted()) { @@ -1970,6 +1973,7 @@ public class Case { if (Thread.currentThread().isInterrupted()) { throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); } + } catch (CaseActionException ex) { /* * Cancellation or failure. Clean up. The sleep is a little hack to @@ -2003,6 +2007,19 @@ public class Case { "Case.exceptionMessage.couldNotCreateMetadataFile=Failed to create case metadata file." }) private void createCaseData(ProgressIndicator progressIndicator) throws CaseActionException { + // RJCTODO: progress + try { + CoordinationService coordinationService = CoordinationService.getInstance(); + CaseNodeData nodeData = new CaseNodeData(metadata); + coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); + } catch (CoordinationServiceException | InterruptedException | ParseException ex) { + // RJCTODO + } + + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + /* * Create the case directory, if it does not already exist. * @@ -2014,6 +2031,10 @@ public class Case { Case.createCaseDirectory(metadata.getCaseDirectory(), metadata.getCaseType()); } + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + /* * Create the case database. */ @@ -2065,6 +2086,24 @@ public class Case { }) private void openCaseData(ProgressIndicator progressIndicator) throws CaseActionException { try { + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + + // RJCTODO: progress + try { + CoordinationService coordinationService = CoordinationService.getInstance(); + CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory())); + nodeData.setLastAccessDate(new Date()); + coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); + } catch (CoordinationServiceException | InterruptedException | CaseNodeData.InvalidDataException ex) { + // RJCTODO + } + + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + progressIndicator.progress(Bundle.Case_progressMessage_openingCaseDatabase()); String databaseName = metadata.getCaseDatabaseName(); if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java deleted file mode 100644 index 402b64d78f..0000000000 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2017-2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.casemodule; - -import java.lang.reflect.InvocationTargetException; -import javax.swing.ListSelectionModel; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableColumnModel; -import org.netbeans.swing.etable.ETableColumn; -import org.netbeans.swing.etable.ETableColumnModel; -import org.netbeans.swing.outline.DefaultOutlineModel; -import org.netbeans.swing.outline.Outline; -import org.openide.nodes.Node; -import java.awt.EventQueue; -import java.io.File; -import java.io.IOException; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import javax.swing.SwingWorker; -import org.openide.explorer.ExplorerManager; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coordinationservice.CoordinationService; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.EmptyNode; - -/** - * A Swing JPanel with a scroll pane child component. The scroll pane contain - * the table of cases. - * - * Used to display a list of multi user cases and allow the user to open one of - * them. - */ -@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -class CaseBrowser extends javax.swing.JPanel implements ExplorerManager.Provider { - - private static final long serialVersionUID = 1L; - private final Outline outline; - private ExplorerManager em; - private final org.openide.explorer.view.OutlineView outlineView; - private int originalPathColumnIndex = 0; - private static final Logger LOGGER = Logger.getLogger(CaseBrowser.class.getName()); - private LoadCaseListWorker tableWorker; - - @Override - public ExplorerManager getExplorerManager() { - return em; - } - - /** - * Creates a new CaseBrowser - */ - CaseBrowser() { - outlineView = new org.openide.explorer.view.OutlineView(); - initComponents(); - - outline = outlineView.getOutline(); - outlineView.setPropertyColumns( - Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(), - Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath()); - ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.CaseNode_column_name()); - customize(); - - } - - /** - * Configures the the table of cases and its columns. - */ - private void customize() { - outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - TableColumnModel columnModel = outline.getColumnModel(); - int dateColumnIndex = 0; - for (int index = 0; index < columnModel.getColumnCount(); index++) { - //get indexes for created date column and path column - if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_metadataFilePath())) { - originalPathColumnIndex = index; - } else if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_createdTime())) { - dateColumnIndex = index; - } - } - //Hide path column by default (user can unhide it) - ETableColumn column = (ETableColumn) columnModel.getColumn(originalPathColumnIndex); - ((ETableColumnModel) columnModel).setColumnHidden(column, true); - outline.setRootVisible(false); - - //Sort on Created date column in descending order by default - outline.setColumnSorted(dateColumnIndex, false, 1); - if (null == em) { - em = new ExplorerManager(); - } - caseTableScrollPane.setViewportView(outlineView); - this.setVisible(true); - outline.setRowSelectionAllowed(false); - } - - /** - * Add a listener to changes in case selections in the table - * - * @param listener the ListSelectionListener to add - */ - void addListSelectionListener(ListSelectionListener listener) { - outline.getSelectionModel().addListSelectionListener(listener); - } - - /** - * Get the path to the .aut file for the selected case. - * - * @return the full path to the selected case's .aut file - */ - String getCasePath() { - int[] selectedRows = outline.getSelectedRows(); - if (selectedRows.length == 1) { - try { - return ((Node.Property) outline.getModel().getValueAt(outline.convertRowIndexToModel(selectedRows[0]), originalPathColumnIndex)).getValue().toString(); - } catch (IllegalAccessException | InvocationTargetException ex) { - LOGGER.log(Level.SEVERE, "Unable to get case path from table.", ex); - } - } - return null; - } - - /** - * Check if a row could be and is selected. - * - * @return true if a row is selected, false if no row is selected - */ - boolean isRowSelected() { - return outline.getRowSelectionAllowed() && outline.getSelectedRows().length > 0; - } - - @NbBundle.Messages({"CaseBrowser.caseListLoading.message=Please Wait..."}) - /** - * Gets the list of cases known to the review mode cases manager and - * refreshes the cases table. - */ - void refresh() { - if (tableWorker == null || tableWorker.isDone()) { - outline.setRowSelectionAllowed(false); - //create a new TableWorker to and execute it in a background thread if one is not currently working - //set the table to display text informing the user that the list is being retreived and disable case selection - EmptyNode emptyNode = new EmptyNode(Bundle.CaseBrowser_caseListLoading_message()); - em.setRootContext(emptyNode); - tableWorker = new LoadCaseListWorker(); - tableWorker.execute(); - } - - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // //GEN-BEGIN:initComponents - private void initComponents() { - - caseTableScrollPane = new javax.swing.JScrollPane(); - - setMinimumSize(new java.awt.Dimension(0, 5)); - setPreferredSize(new java.awt.Dimension(5, 5)); - setLayout(new java.awt.BorderLayout()); - - caseTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - caseTableScrollPane.setMinimumSize(new java.awt.Dimension(0, 5)); - caseTableScrollPane.setOpaque(false); - caseTableScrollPane.setPreferredSize(new java.awt.Dimension(5, 5)); - add(caseTableScrollPane, java.awt.BorderLayout.CENTER); - }// //GEN-END:initComponents - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JScrollPane caseTableScrollPane; - // End of variables declaration//GEN-END:variables - - /** - * Swingworker to fetch the updated List of cases in a background thread - */ - private class LoadCaseListWorker extends SwingWorker { - - private List cases; - - /** - * Gets a list of the cases in the top level case folder - * - * @return List of cases. - * - * @throws CoordinationServiceException - */ - private List getCases() throws CoordinationService.CoordinationServiceException { - List caseList = new ArrayList<>(); - List nodeList = CoordinationService.getInstance().getNodeList(CoordinationService.CategoryNode.CASES); - - for (String node : nodeList) { - Path casePath; - try { - casePath = Paths.get(node).toRealPath(LinkOption.NOFOLLOW_LINKS); - - File caseFolder = casePath.toFile(); - if (caseFolder.exists()) { - /* - * Search for '*.aut' files. - */ - File[] fileArray = caseFolder.listFiles(); - if (fileArray == null) { - continue; - } - String autFilePath = null; - for (File file : fileArray) { - String name = file.getName().toLowerCase(); - if (autFilePath == null && name.endsWith(".aut")) { - try { - caseList.add(new CaseMetadata(Paths.get(file.getAbsolutePath()))); - } catch (CaseMetadata.CaseMetadataException ex) { - LOGGER.log(Level.SEVERE, String.format("Error reading case metadata file '%s'.", autFilePath), ex); - } - break; - } - } - } - } catch (IOException ignore) { - //if a path could not be resolved to a real path do add it to the caseList - } - } - return caseList; - } - - @Override - protected Void doInBackground() throws Exception { - - try { - cases = getCases(); - } catch (CoordinationService.CoordinationServiceException ex) { - LOGGER.log(Level.SEVERE, "Unexpected exception while refreshing the table.", ex); //NON-NLS - } - return null; - } - - @Override - protected void done() { - EventQueue.invokeLater(() -> { - MultiUserNode caseListNode = new MultiUserNode(cases); - em.setRootContext(caseListNode); - outline.setRowSelectionAllowed(true); - }); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index 6e47363db5..91c1ca216c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -52,7 +52,8 @@ import org.xml.sax.SAXException; public final class CaseMetadata { private static final String FILE_EXTENSION = ".aut"; - private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); + private static final String DATE_FORMAT_STRING = "yyyy/MM/dd HH:mm:ss (z)"; + private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_STRING); /* * Fields from schema version 1 @@ -92,6 +93,7 @@ public final class CaseMetadata { private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS + /* * Unread fields, regenerated on save. */ @@ -119,6 +121,15 @@ public final class CaseMetadata { return FILE_EXTENSION; } + /** + * Gets the date format used for dates in case metadata. + * + * @return The date format. + */ + public static DateFormat getDateFormat() { + return new SimpleDateFormat(DATE_FORMAT_STRING); + } + /** * Constructs a CaseMetadata object for a new case. The metadata is not * persisted to the case metadata file until writeFile or a setX method is @@ -295,7 +306,7 @@ public final class CaseMetadata { * * @return The date this case was created, as a string. */ - String getCreatedDate() { + public String getCreatedDate() { return createdDate; } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index a8e18da650..47c002d36b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -161,7 +161,7 @@ public final class CaseOpenAction extends CallableSystemAction implements Action WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); if (multiUserCaseWindow == null) { - multiUserCaseWindow = MultiUserCasesDialog.getInstance(); + multiUserCaseWindow = OpenMultiUserCaseDialog.getInstance(); } multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); multiUserCaseWindow.setVisible(true); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java new file mode 100755 index 0000000000..0a762f5f3b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java @@ -0,0 +1,198 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2017-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.SwingUtilities; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coordinationservice.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.NodeProperty; + +/** + * A NetBeans Explorer View node that represents a multi-user case. + */ +final class MultiUserCaseNode extends AbstractNode { + + private static final Logger logger = Logger.getLogger(MultiUserCaseNode.class.getName()); + private static final String CASE_AUTO_INGEST_LOG_FILE_NAME = "auto_ingest_log.txt"; + private final CaseNodeData caseNodeData; + private final Path caseMetadataFilePath; + private final Path caseAutoIngestLogFilePath; + + /** + * Constructs a NetBeans Explorer View node that represents a multi-user + * case. + * + * @param caseNodeData The coordination service node data for the case. + */ + MultiUserCaseNode(CaseNodeData caseNodeData) { + super(Children.LEAF); + super.setName(caseNodeData.getDisplayName()); + setName(caseNodeData.getDisplayName()); + setDisplayName(caseNodeData.getDisplayName()); + this.caseNodeData = caseNodeData; + this.caseMetadataFilePath = Paths.get(caseNodeData.getDirectory().toString(), caseNodeData.getName() + CaseMetadata.getFileExtension()); + this.caseAutoIngestLogFilePath = Paths.get(caseNodeData.getDirectory().toString(), CASE_AUTO_INGEST_LOG_FILE_NAME); + } + + @NbBundle.Messages({ + "CaseNode.column.name=Name", + "CaseNode.column.createTime=Create Time", + "CaseNode.column.path=Path" + }) + @Override + protected Sheet createSheet() { + Sheet sheet = super.createSheet(); + Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); + if (sheetSet == null) { + sheetSet = Sheet.createPropertiesSet(); + sheet.put(sheetSet); + } + sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_name(), + Bundle.CaseNode_column_name(), + Bundle.CaseNode_column_name(), + caseNodeData.getDisplayName())); + sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_createTime(), + Bundle.CaseNode_column_createTime(), + Bundle.CaseNode_column_createTime(), + caseNodeData.getCreateDate())); + sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_path(), + Bundle.CaseNode_column_path(), + Bundle.CaseNode_column_path(), + caseNodeData.getDirectory().toString())); + return sheet; + } + + @Override + public Action[] getActions(boolean context) { + List actions = new ArrayList<>(); + actions.add(new OpenMultiUserCaseAction(this.caseMetadataFilePath)); + actions.add(new OpenCaseAutoIngestLogAction(this.caseAutoIngestLogFilePath)); + return actions.toArray(new Action[actions.size()]); + } + + @Override + public Action getPreferredAction() { + return new OpenMultiUserCaseAction(this.caseMetadataFilePath); + } + + /** + * An action that opens the specified case and hides the multi-user case + * panel. + */ + @NbBundle.Messages({ + "MultiUserNode.OpenMultiUserCaseAction.menuItemText=Open Case", + "# {0} - caseErrorMessage", "MultiUserNode.OpenMultiUserCaseAction.caseOpeningErrorErrorMsg=Failed to open case: {0}" + }) + private static final class OpenMultiUserCaseAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private final Path caseMetadataFilePath; + + OpenMultiUserCaseAction(Path caseMetadataFilePath) { + super(Bundle.MultiUserNode_OpenMultiUserCaseAction_menuItemText()); + this.caseMetadataFilePath = caseMetadataFilePath; + } + + public void actionPerformed(ActionEvent e) { + StartupWindowProvider.getInstance().close(); + OpenMultiUserCaseDialog.getInstance().setVisible(false); + new Thread(() -> { + try { + Case.openAsCurrentCase(caseMetadataFilePath.toString()); + } catch (CaseActionException ex) { + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS + /* + * CaseActionException error messages are user-friendly. + */ + MessageNotifyUtil.Message.error(Bundle.MultiUserNode_OpenMultiUserCaseAction_caseOpeningErrorErrorMsg(ex.getLocalizedMessage())); + } + SwingUtilities.invokeLater(() -> { + StartupWindowProvider.getInstance().open(); + OpenMultiUserCaseDialog.getInstance().setVisible(true); + }); + } + }).start(); + } + + @Override + public Object clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + + } + + /** + * An action that opens the case auto ingest log for the specified case. + */ + @NbBundle.Messages({ + "MultiUserNode.OpenCaseAutoIngestLogAction.menuItemText=Open Auto Ingest Log File", + "MultiUserNode.OpenCaseAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted.", + "MultiUserNode.OpenCaseAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details.", + }) + private static final class OpenCaseAutoIngestLogAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private final Path caseAutoIngestLogFilePath; + + OpenCaseAutoIngestLogAction(Path caseAutoIngestLogFilePath) { + super(Bundle.MultiUserNode_OpenCaseAutoIngestLogAction_menuItemText()); + this.caseAutoIngestLogFilePath = caseAutoIngestLogFilePath; + this.setEnabled(caseAutoIngestLogFilePath.toFile().exists()); + } + + @Override + public void actionPerformed(ActionEvent e) { + try { + if (caseAutoIngestLogFilePath.toFile().exists()) { + Desktop.getDesktop().edit(caseAutoIngestLogFilePath.toFile()); + } else { + MessageNotifyUtil.Message.error(Bundle.MultiUserNode_OpenCaseAutoIngestLogAction_deletedLogErrorMsg()); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error opening case auto ingest log file at %s", caseAutoIngestLogFilePath), ex); //NON-NLS + MessageNotifyUtil.Message.error(Bundle.MultiUserNode_OpenCaseAutoIngestLogAction_logOpenFailedErrorMsg()); + } + } + + @Override + public Object clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.form b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.form similarity index 100% rename from Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.form rename to Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.form diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java new file mode 100644 index 0000000000..2a99346792 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java @@ -0,0 +1,259 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2017-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import javax.swing.ListSelectionModel; +import javax.swing.table.TableColumnModel; +import org.netbeans.swing.etable.ETableColumn; +import org.netbeans.swing.etable.ETableColumnModel; +import org.netbeans.swing.outline.DefaultOutlineModel; +import org.netbeans.swing.outline.Outline; +import java.awt.EventQueue; +import java.io.File; +import java.io.IOException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.openide.explorer.ExplorerManager; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coordinationservice.CaseNodeData; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.EmptyNode; +import java.util.concurrent.ExecutionException; +import org.openide.explorer.view.OutlineView; + +/** + * A JPanel with a scroll pane child component that contains a NetBeans + * OutlineView that can be used to display a list of the multi-user cases known + * to the coordination service. + */ +@SuppressWarnings("PMD.SingularField") // Matisse-generated UI widgets cause lots of false positives +class MultiUserCasesBrowserPanel extends javax.swing.JPanel implements ExplorerManager.Provider { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(MultiUserCasesBrowserPanel.class.getName()); + private ExplorerManager explorerManager; // RJCTODO: COnsider making this final + private final Outline outline; + private final OutlineView outlineView; + private LoadCaseListWorker loadCaseListWorker; + + /** + * Constructs a JPanel with a scroll pane child component that contains a + * NetBeans OutlineView that can be used to display a list of the multi-user + * cases known to the coordination service. + */ + MultiUserCasesBrowserPanel() { + outlineView = new org.openide.explorer.view.OutlineView(); + initComponents(); + outline = outlineView.getOutline(); + customizeOutlineView(); + } + + /** + * Configures the the table of cases and its columns. + */ + private void customizeOutlineView() { + outlineView.setPropertyColumns( + Bundle.CaseNode_column_createTime(), Bundle.CaseNode_column_createTime(), // RJCTODO: Move these into this file? + Bundle.CaseNode_column_path(), Bundle.CaseNode_column_path()); + ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.CaseNode_column_name()); + outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + TableColumnModel columnModel = outline.getColumnModel(); + int pathColumnIndex = 0; + int dateColumnIndex = 0; + for (int index = 0; index < columnModel.getColumnCount(); index++) { + if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_path())) { + pathColumnIndex = index; + } else if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_createTime())) { + dateColumnIndex = index; + } + } + + /* + * Hide path column by default (user can unhide it) + */ + ETableColumn column = (ETableColumn) columnModel.getColumn(pathColumnIndex); + ((ETableColumnModel) columnModel).setColumnHidden(column, true); + outline.setRootVisible(false); + + /* + * Sort on Created date column in descending order by default. + */ + outline.setColumnSorted(dateColumnIndex, false, 1); + + if (null == explorerManager) { + explorerManager = new ExplorerManager(); + } + + caseTableScrollPane.setViewportView(outlineView); + this.setVisible(true); + } + + @Override + public ExplorerManager getExplorerManager() { + return explorerManager; + } + + /** + * Gets the list of cases known to the review mode cases manager and + * refreshes the cases table. + */ + @NbBundle.Messages({ + "MultiUserCasesBrowserPanel.waitNode.message=Please Wait..." + }) + void refreshCases() { + if (loadCaseListWorker == null || loadCaseListWorker.isDone()) { + /* + * RJCTODO: Explain this or move the data fetching into the create + * keys method of the nodes... + */ + EmptyNode emptyNode = new EmptyNode(Bundle.MultiUserCasesBrowserPanel_waitNode_message()); + explorerManager.setRootContext(emptyNode); + + loadCaseListWorker = new LoadCaseListWorker(); + loadCaseListWorker.execute(); + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + caseTableScrollPane = new javax.swing.JScrollPane(); + + setMinimumSize(new java.awt.Dimension(0, 5)); + setPreferredSize(new java.awt.Dimension(5, 5)); + setLayout(new java.awt.BorderLayout()); + + caseTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + caseTableScrollPane.setMinimumSize(new java.awt.Dimension(0, 5)); + caseTableScrollPane.setOpaque(false); + caseTableScrollPane.setPreferredSize(new java.awt.Dimension(5, 5)); + add(caseTableScrollPane, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane caseTableScrollPane; + // End of variables declaration//GEN-END:variables + + /** + * A background task that gets the multi-user case data from the case + * directory lock coordination service nodes. + */ + private class LoadCaseListWorker extends SwingWorker, Void> { + + private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS + private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS + + @Override + protected List doInBackground() throws Exception { + final List cases = new ArrayList<>(); + final CoordinationService coordinationService = CoordinationService.getInstance(); + final List nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); + for (String nodeName : nodeList) { + /* + * Ignore case name lock nodes. + */ + final Path nodeNameAsPath = Paths.get(nodeName); + if (!(nodeNameAsPath.toString().contains("\\") || nodeNameAsPath.toString().contains("//"))) { + continue; + } + + /* + * Ignore case auto ingest log lock nodes and resource lock + * nodes. + */ + final String lastNodeNameComponent = nodeNameAsPath.getFileName().toString(); + if (lastNodeNameComponent.equals(CASE_AUTO_INGEST_LOG_NAME) || lastNodeNameComponent.endsWith(RESOURCES_LOCK_SUFFIX)) { + continue; + } + + /* + * Get the data from the case directory lock node. This data may not exist + * for "legacy" nodes. If it is missing, create it. + */ + try { + CaseNodeData nodeData; + byte[] nodeBytes = CoordinationService.getInstance().getNodeData(CoordinationService.CategoryNode.CASES, nodeName); + if (nodeBytes != null && nodeBytes.length > 0) { + nodeData = new CaseNodeData(nodeBytes); + if (nodeData.getVersion() > 0) { + cases.add(nodeData); + } else { + nodeData = createNodeDataFromCaseMetadata(nodeName); + } + } else { + nodeData = createNodeDataFromCaseMetadata(nodeName); + } + cases.add(nodeData); + + } catch (CoordinationService.CoordinationServiceException | CaseNodeData.InvalidDataException | IOException | CaseMetadata.CaseMetadataException ex) { + logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex); + } + + } + return cases; + } + + @Override + protected void done() { + try { + final List cases = get(); + EventQueue.invokeLater(() -> { + MultiUserCasesRootNode caseListNode = new MultiUserCasesRootNode(cases); + explorerManager.setRootContext(caseListNode); + }); + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, "Unexpected interrupt during background processing", ex); + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "Error during background processing", ex); + } + } + + private CaseNodeData createNodeDataFromCaseMetadata(String nodeName) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { + Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS); + File caseDirectory = caseDirectoryPath.toFile(); + if (caseDirectory.exists()) { + File[] files = caseDirectory.listFiles(); + for (File file : files) { + String name = file.getName().toLowerCase(); + if (name.endsWith(CaseMetadata.getFileExtension())) { + CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath())); + CaseNodeData nodeData = new CaseNodeData(metadata); + CoordinationService coordinationService = CoordinationService.getInstance(); + coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); + } + } + } + throw new IOException(String.format("Could not find case metadata file for %s", nodeName)); + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java deleted file mode 100644 index af95b0c5a0..0000000000 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.casemodule; - -import java.awt.Dialog; -import java.awt.event.KeyEvent; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.KeyStroke; -import org.openide.windows.WindowManager; - -/** - * This class extends a JDialog and maintains the MultiUserCasesPanel. - */ -@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -final class MultiUserCasesDialog extends JDialog { - - private static final long serialVersionUID = 1L; - private static final String REVIEW_MODE_TITLE = "Open Multi-User Case"; - private static MultiUserCasesPanel multiUserCasesPanel; - private static MultiUserCasesDialog instance; - - /** - * Gets the instance of the MultiuserCasesDialog. - * - * @return The instance. - */ - static public MultiUserCasesDialog getInstance() { - if(instance == null) { - instance = new MultiUserCasesDialog(); - instance.init(); - } - return instance; - } - - /** - * Constructs a MultiUserCasesDialog object. - */ - private MultiUserCasesDialog() { - super(WindowManager.getDefault().getMainWindow(), - REVIEW_MODE_TITLE, - Dialog.ModalityType.APPLICATION_MODAL); - } - - /** - * Initializes the multi-user cases panel. - */ - private void init() { - getRootPane().registerKeyboardAction( - e -> { - setVisible(false); - }, - KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); - - multiUserCasesPanel = new MultiUserCasesPanel(this); - add(multiUserCasesPanel); - pack(); - setResizable(false); - } - - /** - * Set the dialog visibility. When setting it to visible, the contents will - * refresh. - * - * @param value True or false. - */ - @Override - public void setVisible(boolean value) { - if(value) { - multiUserCasesPanel.refresh(); - } - super.setVisible(value); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java new file mode 100644 index 0000000000..3ed3f15492 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java @@ -0,0 +1,69 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2017-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import java.util.List; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coordinationservice.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * A root node for displaying MultiUserCaseNodes in a NetBeans Explorer View. + */ +final class MultiUserCasesRootNode extends AbstractNode { + + /** + * Constructs a root node for displaying MultiUserCaseNodes in a NetBeans + * Explorer View. + * + * @param case A list of coordination service node data objects representing + * multi-user cases. + */ + MultiUserCasesRootNode(List cases) { + super(Children.create(new MultiUserCasesRootNodeChildren(cases), true)); + } + + private static class MultiUserCasesRootNodeChildren extends ChildFactory { + + private final List cases; + + MultiUserCasesRootNodeChildren(List cases) { + this.cases = cases; + } + + @Override + protected boolean createKeys(List keys) { + if (cases != null && cases.size() > 0) { + keys.addAll(cases); + } + return true; + } + + @Override + protected Node createNodeForKey(CaseNodeData key) { + return new MultiUserCaseNode(key); + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java deleted file mode 100644 index 1efbffe190..0000000000 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2017-2018 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.casemodule; - -import java.awt.Desktop; -import java.awt.event.ActionEvent; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import org.openide.nodes.AbstractNode; -import org.openide.nodes.ChildFactory; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.nodes.Sheet; -import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.datamodel.NodeProperty; - -/** - * A root node containing child nodes of the multi user cases - */ -final class MultiUserNode extends AbstractNode { - - @Messages({"CaseNode.column.name=Name", - "CaseNode.column.createdTime=Created Time", - "CaseNode.column.metadataFilePath=Path"}) - private static final Logger LOGGER = Logger.getLogger(MultiUserNode.class.getName()); - private static final String LOG_FILE_NAME = "auto_ingest_log.txt"; - - /** - * Provides a root node with children which each represent a case. - * - * @param caseList the list of CaseMetadata objects representing the cases - */ - MultiUserNode(List caseList) { - super(Children.create(new MultiUserNodeChildren(caseList), true)); - } - - static class MultiUserNodeChildren extends ChildFactory { - - private final List caseList; - - MultiUserNodeChildren(List caseList) { - this.caseList = caseList; - } - - @Override - protected boolean createKeys(List list) { - if (caseList != null && caseList.size() > 0) { - list.addAll(caseList); - } - return true; - } - - @Override - protected Node createNodeForKey(CaseMetadata key) { - return new MultiUserCaseNode(key); - } - - } - - /** - * A node which represents a single multi user case. - */ - static final class MultiUserCaseNode extends AbstractNode { - - private final String caseName; - private final String caseCreatedDate; - private final String caseMetadataFilePath; - private final Path caseLogFilePath; - - MultiUserCaseNode(CaseMetadata multiUserCase) { - super(Children.LEAF); - caseName = multiUserCase.getCaseDisplayName(); - caseCreatedDate = multiUserCase.getCreatedDate(); - super.setName(caseName); - setName(caseName); - setDisplayName(caseName); - caseMetadataFilePath = multiUserCase.getFilePath().toString(); - caseLogFilePath = Paths.get(multiUserCase.getCaseDirectory(), LOG_FILE_NAME); - } - - /** - * Returns action to open the Case represented by this node - * @return an action which will open the current case - */ - @Override - public Action getPreferredAction() { - return new OpenMultiUserCaseAction(caseMetadataFilePath); - } - - @Override - protected Sheet createSheet() { - Sheet sheet = super.createSheet(); - Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); - if (sheetSet == null) { - sheetSet = Sheet.createPropertiesSet(); - sheet.put(sheetSet); - } - sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_name(), Bundle.CaseNode_column_name(), Bundle.CaseNode_column_name(), - caseName)); - sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(), - caseCreatedDate)); - sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath(), - caseMetadataFilePath)); - return sheet; - } - - @Override - public Action[] getActions(boolean context) { - List actions = new ArrayList<>(); - actions.add(new OpenMultiUserCaseAction(caseMetadataFilePath)); //open case context menu option - actions.add(new OpenCaseLogAction(caseLogFilePath)); - return actions.toArray(new Action[actions.size()]); - } - } - - @Messages({"MultiUserNode.OpenMultiUserCaseAction.text=Open Case"}) - /** - * An action that opens the specified case and hides the multi user case - * panel. - */ - private static final class OpenMultiUserCaseAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - - private final String caseMetadataFilePath; - - OpenMultiUserCaseAction(String path) { - super(Bundle.MultiUserNode_OpenMultiUserCaseAction_text()); - caseMetadataFilePath = path; - } - - @Override - public void actionPerformed(ActionEvent e) { - StartupWindowProvider.getInstance().close(); - MultiUserCasesDialog.getInstance().setVisible(false); - new Thread( - () -> { - try { - Case.openAsCurrentCase(caseMetadataFilePath); - } catch (CaseActionException ex) { - if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { - LOGGER.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS - MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); - } - SwingUtilities.invokeLater(() -> { - //GUI changes done back on the EDT - StartupWindowProvider.getInstance().open(); - MultiUserCasesDialog.getInstance().setVisible(true); - }); - } - } - ).start(); - } - - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); //To change body of generated methods, choose Tools | Templates. - } - } - - @Messages({"MultiUserNode.OpenCaseLogAction.text=Open Log File"}) - /** - * An action that opens the specified case and hides the multi user case - * panel. - */ - private static final class OpenCaseLogAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - - private final Path pathToLog; - - OpenCaseLogAction(Path caseLogFilePath) { - super(Bundle.MultiUserNode_OpenCaseLogAction_text()); - pathToLog = caseLogFilePath; - this.setEnabled(caseLogFilePath != null && caseLogFilePath.toFile().exists()); - } - - @Override - public void actionPerformed(ActionEvent e) { - - if (pathToLog != null) { - try { - if (pathToLog.toFile().exists()) { - Desktop.getDesktop().edit(pathToLog.toFile()); - - } else { - JOptionPane.showMessageDialog(MultiUserCasesDialog.getInstance(), org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.cannotFindLog"), - org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.unableToShowLogFile"), JOptionPane.ERROR_MESSAGE); - } - } catch (IOException ex) { - LOGGER.log(Level.SEVERE, String.format("Error attempting to open case auto ingest log file %s", pathToLog), ex); - JOptionPane.showMessageDialog(MultiUserCasesDialog.getInstance(), - org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.cannotOpenLog"), - org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.unableToShowLogFile"), - JOptionPane.PLAIN_MESSAGE); - } - } - } - - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); //To change body of generated methods, choose Tools | Templates. - } - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java new file mode 100644 index 0000000000..6fdf6044ae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java @@ -0,0 +1,96 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2017-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule; + +import java.awt.Dialog; +import java.awt.event.KeyEvent; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.KeyStroke; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; + +/** + * A singleton JDialog that allows a user to open a multi-user case. + */ +final class OpenMultiUserCaseDialog extends JDialog { + + private static final long serialVersionUID = 1L; + private static OpenMultiUserCaseDialog instance; + private static OpenMultiUserCasePanel multiUserCasesPanel; + + /** + * Gets the singleton JDialog singleton JDialog that allows a user to open a + * multi-user case. + * + * @return The singleton JDialog instance. + */ + public synchronized static OpenMultiUserCaseDialog getInstance() { + if (instance == null) { + instance = new OpenMultiUserCaseDialog(); + instance.init(); + } + return instance; + } + + /** + * Constructs a singleton JDialog singleton JDialog that allows a user to + * open a multi-user case. + */ + @NbBundle.Messages({ + "OpenMultiUserCaseDialog.title=Open Multi-User Case" + }) + private OpenMultiUserCaseDialog() { + super(WindowManager.getDefault().getMainWindow(), + Bundle.OpenMultiUserCaseDialog_title(), + Dialog.ModalityType.APPLICATION_MODAL); + } + + /** + * Registers a keyboard action to hide the dialog when the escape key is + * pressed and adds a OpenMultiUserCasePanel child component. + */ + private void init() { + getRootPane().registerKeyboardAction( + action -> { + setVisible(false); + }, + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JComponent.WHEN_IN_FOCUSED_WINDOW); + multiUserCasesPanel = new OpenMultiUserCasePanel(this); + add(multiUserCasesPanel); + pack(); + setResizable(false); + } + + /** + * Sets the dialog visibility. When made visible, the dialog refreshes the + * display of its OpenMultiUserCasePanel child component. + * + * @param value True or false. + */ + @Override + public void setVisible(boolean value) { + if (value) { + multiUserCasesPanel.refreshDisplay(); + } + super.setVisible(value); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form similarity index 71% rename from Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form rename to Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form index 71810b7387..8f6f0e138b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form @@ -28,12 +28,10 @@ - + - - - + @@ -48,7 +46,6 @@ - @@ -58,30 +55,10 @@ - - - - - - - - - - - - - - - - - - - - - + @@ -97,7 +74,7 @@ - + @@ -116,7 +93,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java similarity index 59% rename from Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java rename to Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java index 07d863bc2b..6c533a6ff6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2017-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,107 +18,52 @@ */ package org.sleuthkit.autopsy.casemodule; -import java.awt.Cursor; import java.util.ArrayList; import java.util.Date; -import java.util.logging.Level; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.SortOrder; -import javax.swing.SwingUtilities; -import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableRowSorter; import org.openide.util.Lookup; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** - * A panel that allows a user to open cases created by auto ingest. + * A JPanel that allows a user to open a multi-user case. */ -@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -final class MultiUserCasesPanel extends JPanel{ +@SuppressWarnings("PMD.SingularField") // Matisse-generated UI widgets cause lots of false positives +final class OpenMultiUserCasePanel extends JPanel { - private static final Logger logger = Logger.getLogger(MultiUserCasesPanel.class.getName()); private static final long serialVersionUID = 1L; private final JDialog parentDialog; - private final CaseBrowser caseBrowserPanel; + private final MultiUserCasesBrowserPanel caseBrowserPanel; /** - * Constructs a panel that allows a user to open cases created by automated - * ingest. + * Constructs a JPanel that allows a user to open a multi-user case. + * + * @param parentDialog The parent dialog of the panel, may be null. If + * provided, the dialog is hidden when this poanel's + * cancel button is pressed. */ - MultiUserCasesPanel(JDialog parentDialog) { + OpenMultiUserCasePanel(JDialog parentDialog) { this.parentDialog = parentDialog; initComponents(); - - caseBrowserPanel = new CaseBrowser(); + caseBrowserPanel = new MultiUserCasesBrowserPanel(); caseExplorerScrollPane.add(caseBrowserPanel); caseExplorerScrollPane.setViewportView(caseBrowserPanel); - /* - * Listen for row selection changes and set button state for the current - * selection. - */ - caseBrowserPanel.addListSelectionListener((ListSelectionEvent e) -> { - setButtons(); - }); - } /** - * Gets the list of cases known to the review mode cases manager and - * refreshes the cases table. + * Refreshes the child component that displays the multi-user cases known to + * the coordination service.. */ - void refresh() { - caseBrowserPanel.refresh(); + void refreshDisplay() { + caseBrowserPanel.refreshCases(); } /** - * Enables/disables the Open and Show Log buttons based on the case selected - * in the cases table. - */ - void setButtons() { - bnOpen.setEnabled(caseBrowserPanel.isRowSelected()); - } - - /** - * Open a case. - * - * @param caseMetadataFilePath The path to the case metadata file. - */ - private void openCase(String caseMetadataFilePath) { - if (caseMetadataFilePath != null) { - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - - StartupWindowProvider.getInstance().close(); - if (parentDialog != null) { - parentDialog.setVisible(false); - } - new Thread(() -> { - try { - Case.openAsCurrentCase(caseMetadataFilePath); - } catch (CaseActionException ex) { - if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { - logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS - MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); - } - SwingUtilities.invokeLater(() -> { - //GUI changes done back on the EDT - StartupWindowProvider.getInstance().open(); - }); - } finally { - SwingUtilities.invokeLater(() -> { - //GUI changes done back on the EDT - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - }); - } - }).start(); - } - } - - /** - * RowSorter which makes columns whose type is Date to be sorted first in - * Descending order then in Ascending order + * A RowSorter which causes columns whose type is Date to be sorted in + * either descending order or ascending order with descending order as the + * default. */ private static class RowSorter extends TableRowSorter { @@ -155,7 +100,6 @@ final class MultiUserCasesPanel extends JPanel{ // //GEN-BEGIN:initComponents private void initComponents() { - bnOpen = new javax.swing.JButton(); bnOpenSingleUserCase = new javax.swing.JButton(); cancelButton = new javax.swing.JButton(); searchLabel = new javax.swing.JLabel(); @@ -164,18 +108,7 @@ final class MultiUserCasesPanel extends JPanel{ setName("Completed Cases"); // NOI18N setPreferredSize(new java.awt.Dimension(960, 485)); - org.openide.awt.Mnemonics.setLocalizedText(bnOpen, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.bnOpen.text")); // NOI18N - bnOpen.setEnabled(false); - bnOpen.setMaximumSize(new java.awt.Dimension(80, 23)); - bnOpen.setMinimumSize(new java.awt.Dimension(80, 23)); - bnOpen.setPreferredSize(new java.awt.Dimension(80, 23)); - bnOpen.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - bnOpenActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(bnOpenSingleUserCase, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.bnOpenSingleUserCase.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(bnOpenSingleUserCase, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.bnOpenSingleUserCase.text")); // NOI18N bnOpenSingleUserCase.setMinimumSize(new java.awt.Dimension(156, 23)); bnOpenSingleUserCase.setPreferredSize(new java.awt.Dimension(156, 23)); bnOpenSingleUserCase.addActionListener(new java.awt.event.ActionListener() { @@ -184,7 +117,7 @@ final class MultiUserCasesPanel extends JPanel{ } }); - org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.cancelButton.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.cancelButton.text")); // NOI18N cancelButton.setMaximumSize(new java.awt.Dimension(80, 23)); cancelButton.setMinimumSize(new java.awt.Dimension(80, 23)); cancelButton.setPreferredSize(new java.awt.Dimension(80, 23)); @@ -194,7 +127,7 @@ final class MultiUserCasesPanel extends JPanel{ } }); - org.openide.awt.Mnemonics.setLocalizedText(searchLabel, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.searchLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(searchLabel, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.searchLabel.text")); // NOI18N javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); @@ -206,17 +139,12 @@ final class MultiUserCasesPanel extends JPanel{ .addComponent(caseExplorerScrollPane) .addGroup(layout.createSequentialGroup() .addComponent(searchLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 350, Short.MAX_VALUE) .addComponent(bnOpenSingleUserCase, javax.swing.GroupLayout.PREFERRED_SIZE, 192, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(190, 190, 190) - .addComponent(bnOpen, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addContainerGap()) ); - - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnOpen, cancelButton}); - layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() @@ -225,7 +153,6 @@ final class MultiUserCasesPanel extends JPanel{ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(bnOpen, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(bnOpenSingleUserCase, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(searchLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addContainerGap()) @@ -233,14 +160,10 @@ final class MultiUserCasesPanel extends JPanel{ }// //GEN-END:initComponents /** - * Open button action + * Opens the standard open single user case window. * - * @param evt -- The event that caused this to be called + * @param evt An ActionEvent, unused. */ - private void bnOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenActionPerformed - openCase(caseBrowserPanel.getCasePath()); - }//GEN-LAST:event_bnOpenActionPerformed - private void bnOpenSingleUserCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenSingleUserCaseActionPerformed Lookup.getDefault().lookup(CaseOpenAction.class).openCaseSelectionWindow(); }//GEN-LAST:event_bnOpenSingleUserCaseActionPerformed @@ -252,7 +175,6 @@ final class MultiUserCasesPanel extends JPanel{ }//GEN-LAST:event_cancelButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton bnOpen; private javax.swing.JButton bnOpenSingleUserCase; private javax.swing.JButton cancelButton; private javax.swing.JScrollPane caseExplorerScrollPane; diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java index 0b220e04b2..7b10f80c55 100644 --- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2017-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,67 +20,129 @@ package org.sleuthkit.autopsy.coordinationservice; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; /** - * An object that converts case data for a case directory coordination service + * An object that converts data for a case directory lock coordination service * node to and from byte arrays. */ public final class CaseNodeData { - private static final int CURRENT_VERSION = 0; - + private static final int CURRENT_VERSION = 1; + + /* + * Version 0 fields. + */ private int version; private boolean errorsOccurred; + /* + * Version 1 fields. + */ + private Path directory; + private long createDate; + private long lastAccessDate; + private String name; + private String displayName; + private short deletedItemFlags; + /** - * Gets the current version of the case directory coordination service node - * data. + * Gets the current version of the case directory lock coordination service + * node data. * * @return The version number. */ public static int getCurrentVersion() { return CaseNodeData.CURRENT_VERSION; } - + + /** + * Uses a CaseMetadata object to construct an object that converts data for + * a case directory lock coordination service node to and from byte arrays. + * + * @param metadata The case meta data. + * + * @throws java.text.ParseException If there is an error parsing dates from + * string representations of dates in the + * meta data. + */ + public CaseNodeData(CaseMetadata metadata) throws ParseException { + this.version = CURRENT_VERSION; + this.errorsOccurred = false; + this.directory = Paths.get(metadata.getCaseDirectory()); + this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate()).getTime(); + this.lastAccessDate = new Date().getTime(); // Don't really know. + this.name = metadata.getCaseName(); + this.displayName = metadata.getCaseDisplayName(); + this.deletedItemFlags = 0; + } + /** * Uses coordination service node data to construct an object that converts - * case data for a case directory coordination service node to and from byte + * data for a case directory lock coordination service node to and from byte * arrays. * * @param nodeData The raw bytes received from the coordination service. - * + * * @throws InvalidDataException If the node data buffer is smaller than * expected. */ public CaseNodeData(byte[] nodeData) throws InvalidDataException { - if(nodeData == null || nodeData.length == 0) { - this.version = CURRENT_VERSION; - this.errorsOccurred = false; - } else { - /* - * Get fields from node data. - */ - ByteBuffer buffer = ByteBuffer.wrap(nodeData); - try { - if (buffer.hasRemaining()) { - this.version = buffer.getInt(); + if (nodeData == null || nodeData.length == 0) { + throw new InvalidDataException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array"); + } - /* - * Flags bit format: 76543210 - * 0-6 --> reserved for future use - * 7 --> errorsOccurred - */ - byte flags = buffer.get(); - this.errorsOccurred = (flags < 0); - } - } catch (BufferUnderflowException ex) { - throw new InvalidDataException("Node data is incomplete", ex); + /* + * Get the fields from the node data. + */ + ByteBuffer buffer = ByteBuffer.wrap(nodeData); + try { + /* + * Get version 0 fields. + */ + this.version = buffer.getInt(); + + /* + * Flags bit format: 76543210 0-6 --> reserved for future use 7 --> + * errorsOccurred + */ + byte flags = buffer.get(); + this.errorsOccurred = (flags < 0); + + if (buffer.hasRemaining()) { + /* + * Get version 1 fields. + */ + this.directory = Paths.get(NodeDataUtils.getStringFromBuffer(buffer)); + this.createDate = buffer.getLong(); + this.lastAccessDate = buffer.getLong(); + this.name = NodeDataUtils.getStringFromBuffer(buffer); + this.displayName = NodeDataUtils.getStringFromBuffer(buffer); + this.deletedItemFlags = buffer.getShort(); } + + } catch (BufferUnderflowException ex) { + throw new InvalidDataException("Node data is incomplete", ex); } } /** - * Gets whether or not any errors occurred during the processing of the job. + * Gets the node data version number of this node. + * + * @return The version number. + */ + public int getVersion() { + return this.version; + } + + /** + * Gets whether or not any errors occurred during the processing of any auto + * ingest job for the case represented by this node data. * * @return True or false. */ @@ -89,7 +151,8 @@ public final class CaseNodeData { } /** - * Sets whether or not any errors occurred during the processing of job. + * Sets whether or not any errors occurred during the processing of any auto + * ingest job for the case represented by this node data. * * @param errorsOccurred True or false. */ @@ -98,14 +161,99 @@ public final class CaseNodeData { } /** - * Gets the node data version number. + * Gets the path of the case directory of the case represented by this node + * data. * - * @return The version number. + * @return The case directory path. */ - public int getVersion() { - return this.version; + public Path getDirectory() { + return this.directory; } - + + /** + * Sets the path of the case directory of the case represented by this node + * data. + * + * @param caseDirectory The case directory path. + */ + public void setDirectory(Path caseDirectory) { + this.directory = caseDirectory; + } + + /** + * Gets the date the case represented by this node data was created. + * + * @return The create date. + */ + public Date getCreateDate() { + return new Date(this.createDate); + } + + /** + * Sets the date the case represented by this node data was created. + * + * @param createDate The create date. + */ + public void setCreateDate(Date createDate) { + this.createDate = createDate.getTime(); + } + + /** + * Gets the date the case represented by this node data last accessed. + * + * @return The last access date. + */ + public Date getLastAccessDate() { + return new Date(this.lastAccessDate); + } + + /** + * Sets the date the case represented by this node data was last accessed. + * + * @param lastAccessDate The last access date. + */ + public void setLastAccessDate(Date lastAccessDate) { + this.lastAccessDate = lastAccessDate.getTime(); + } + + /** + * Gets the unique and immutable (user cannot change it) name of the case + * represented by this node data. + * + * @return The case name. + */ + public String getName() { + return this.name; + } + + /** + * Sets the unique and immutable (user cannot change it) name of the case + * represented by this node data. + * + * @param name The case name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the display name of the case represented by this node data. + * + * @return The case display name. + */ + public String getDisplayName() { + return this.displayName; + } + + /** + * Sets the display name of the case represented by this node data. + * + * @param displayName The case display name. + */ + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + /** * Gets the node data as a byte array that can be sent to the coordination * service. @@ -113,16 +261,31 @@ public final class CaseNodeData { * @return The node data as a byte array. */ public byte[] toArray() { - ByteBuffer buffer = ByteBuffer.allocate(5); - + int bufferSize = Integer.BYTES; // version + bufferSize += 1; // errorsOccurred + bufferSize += this.directory.toString().getBytes().length; // directory + bufferSize += Long.BYTES; // createDate + bufferSize += Long.BYTES; // lastAccessDate + bufferSize += this.name.getBytes().length; // name + bufferSize += this.displayName.getBytes().length; // displayName + bufferSize += Short.BYTES; // deletedItemFlags + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + buffer.putInt(this.version); - buffer.put((byte)(this.errorsOccurred ? 0x80 : 0)); - - // Prepare the array + buffer.put((byte) (this.errorsOccurred ? 0x80 : 0)); + + if (this.version >= 1) { + NodeDataUtils.putStringIntoBuffer(this.directory.toString(), buffer); + buffer.putLong(this.createDate); + buffer.putLong(this.lastAccessDate); + NodeDataUtils.putStringIntoBuffer(name, buffer); + NodeDataUtils.putStringIntoBuffer(displayName, buffer); + buffer.putShort(deletedItemFlags); + } + byte[] array = new byte[buffer.position()]; buffer.rewind(); buffer.get(array, 0, array.length); - return array; } diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/InvalidNodeDataException.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/InvalidNodeDataException.java new file mode 100755 index 0000000000..f13c2be677 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/InvalidNodeDataException.java @@ -0,0 +1,35 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.coordinationservice; + +/** + * Exception thrown when invlaid coordniation service node data is encountered. + */ +public final class InvalidNodeDataException extends Exception { + + private static final long serialVersionUID = 1L; + + private InvalidNodeDataException(String message) { + super(message); + } + + private InvalidNodeDataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/NodeDataUtils.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/NodeDataUtils.java new file mode 100755 index 0000000000..cfdb5a5d51 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/NodeDataUtils.java @@ -0,0 +1,64 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.coordinationservice; + +import java.nio.ByteBuffer; +import javax.lang.model.type.TypeKind; + +/** + * Utilities for reading and writing node data. + */ +public final class NodeDataUtils { + + /** + * This method retrieves a string from a given buffer. Assumes the string + * was written using putStringIntoBuffer and first reads the the length of + * the string. + * + * @param buffer The buffer from which the string will be read. + * + * @return The string read from the buffer. + */ + public static String getStringFromBuffer(ByteBuffer buffer) { + String output = ""; + int length = buffer.getInt(); + if (length > 0) { + byte[] array = new byte[length]; + buffer.get(array, 0, length); + output = new String(array); + } + return output; + } + + /** + * This method puts a given string into a given buffer. The length of the + * string will be inserted prior to the string. + * + * @param stringValue The string to write to the buffer. + * @param buffer The buffer to which the string will be written. + */ + public static void putStringIntoBuffer(String stringValue, ByteBuffer buffer) { + buffer.putInt(stringValue.getBytes().length); + buffer.put(stringValue.getBytes()); + } + + private NodeDataUtils() { + } + +}