diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 109ee0e66c..b87ba81e88 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -49,6 +49,14 @@ 1.31.1 + + org.netbeans.swing.outline + + + + 1.34.1 + + org.openide.awt diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties new file mode 100755 index 0000000000..8d03fe3edd --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties @@ -0,0 +1,22 @@ +SelectMultiUserCasesPanel.selectAllButton.text=Select All +SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All +SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name +SelectMultiUserCasesPanel.confirmSelections.text=OK +SelectMultiUserCasesPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchErrorDialog.closeButton.text=Close +MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match +MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match +MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression +MultiCaseKeywordSearchPanel.keywordTextField.text_1= +MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected collections to find cases which contain hits. The cases can be opened to examine the results more closely. +MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases +MultiCaseKeywordSearchPanel.resultsLabel.text=Results +MultiCaseKeywordSearchPanel.uncheckButton.text=Uncheck All +MultiCaseKeywordSearchPanel.checkButton.text=Check All +MultiCaseKeywordSearchPanel.searchButton.text=Search +MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors +MultiCaseKeywordSearchPanel.warningLabel.text= +MultiCaseKeywordSearchPanel.exportButton.text=Export Results +MultiCaseKeywordSearchPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchPanel.resultsCountLabel.text= +MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Pick Cases diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED new file mode 100755 index 0000000000..7b5dabbe0c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED @@ -0,0 +1,108 @@ +CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search +CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search +CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search +MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching +MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard +MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case +MultiCaseKeywordSearchNode.properties.case=Case +MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory +MultiCaseKeywordSearchNode.properties.dataSource=Data Source +MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path +MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source +MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type +MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. Would you like the search to continue in the background while the search window is closed? +MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search +MultiCaseKeywordSearchPanel.countOfResults.label=Count: +MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait... +# {0} - numberOfErrors +MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search +MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv) +# {0} - file name +MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0} +MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed +MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export +# {0} - file name +MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite? +MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search +MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field. +MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search. +MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel. +MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search +MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled +# {0} - connection info +# {1} - case name +# {2} - case directory +MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2} +# {0} - PostgreSQL server host +# {1} - PostgreSQL server port +# {2} - case database name +# {3} - case directory +MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3} +# {0} - case directory +MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0} +# {0} - case_name +MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0} +# {0} - case directory path +MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0} +# {0} - case directory +MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0} +# {0} - host +# {1} - port +MultiCaseSearcher.exceptionMessage.failedToQueryCoordinationServer=Failed to obtain read lock for case directory at {0}:{1} +# {0} - Solr document id +# {1} - case database name +# {2} - case directory +MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2} +# {0} - file name +# {1} - case directory +MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1} +# {0} - list of cases +MultiCaseSearcher.exceptionMessage.noCasesFound=No cases found for: {0} +# {0} - file name +# {1} - case directory +MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1} +# {0} - query +# {1} - case_name +MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query "{0}" on case {1} +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for "{0}" ({1} of {2} case(s)) +MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for "{0}" ({1} of {2} case(s)) +MultiCaseSearcher.progressMessage.findingCases=Finding selected cases +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for "{0}" ({1} of {2} case(s)) +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for "{0}" ({1} of {2} case(s)) +# {0} - total cases +MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s) +SelectMultiUserCasesPanel.selectAllButton.text=Select All +SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All +SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name +SelectMultiUserCasesPanel.confirmSelections.text=OK +SelectMultiUserCasesPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchErrorDialog.closeButton.text=Close +MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match +MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match +MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression +MultiCaseKeywordSearchPanel.keywordTextField.text_1= +MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected collections to find cases which contain hits. The cases can be opened to examine the results more closely. +MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases +MultiCaseKeywordSearchPanel.resultsLabel.text=Results +MultiCaseKeywordSearchPanel.uncheckButton.text=Uncheck All +MultiCaseKeywordSearchPanel.checkButton.text=Check All +MultiCaseKeywordSearchPanel.searchButton.text=Search +MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors +MultiCaseKeywordSearchPanel.warningLabel.text= +MultiCaseKeywordSearchPanel.exportButton.text=Export Results +MultiCaseKeywordSearchPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchPanel.resultsCountLabel.text= +MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Pick Cases diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form new file mode 100755 index 0000000000..368f9ebaa0 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form @@ -0,0 +1,87 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java new file mode 100755 index 0000000000..fd21972f69 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java @@ -0,0 +1,112 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import javax.swing.JDialog; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; + +/** + * Dialog to display the errors encounter while perfomring a multi-case keyword search. + */ +final class MultiCaseKeywordSearchErrorDialog extends JDialog { + + private static final long serialVersionUID = 1L; + + /** + * Creates new MultiCaseKeywordSearchErrorDialog + */ + @Messages({"MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching"}) + MultiCaseKeywordSearchErrorDialog(String contents) { + setTitle(Bundle.MultiCaseKeywordSearchErrorDialog_title_text()); + initComponents(); + errorsTextArea.setText(contents); + this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + pack(); + setModal(true); + setResizable(false); + setVisible(true); + } + + /** + * 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() { + + errorsScrollPane = new javax.swing.JScrollPane(); + errorsTextArea = new javax.swing.JTextArea(); + closeButton = new javax.swing.JButton(); + + errorsScrollPane.setPreferredSize(new java.awt.Dimension(470, 175)); + + errorsTextArea.setEditable(false); + errorsTextArea.setColumns(40); + errorsTextArea.setLineWrap(true); + errorsTextArea.setRows(5); + errorsTextArea.setWrapStyleWord(true); + errorsTextArea.setPreferredSize(new java.awt.Dimension(460, 160)); + errorsScrollPane.setViewportView(errorsTextArea); + + org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchErrorDialog.class, "MultiCaseKeywordSearchErrorDialog.closeButton.text")); // NOI18N + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 480, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(closeButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 196, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(closeButton) + .addGap(14, 14, 14)) + ); + }// //GEN-END:initComponents + + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + dispose(); + }//GEN-LAST:event_closeButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JScrollPane errorsScrollPane; + private javax.swing.JTextArea errorsTextArea; + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java new file mode 100755 index 0000000000..2e69b41422 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java @@ -0,0 +1,291 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.Action; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionCancelledException; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import static org.sleuthkit.autopsy.casemodule.CaseMetadata.getFileExtension; +import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.NodeProperty; + +@NbBundle.Messages({ + "MultiCaseKeywordSearchNode.properties.case=Case", + "MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory", + "MultiCaseKeywordSearchNode.properties.dataSource=Data Source", + "MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type", + "MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source", + "MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path" +}) + +/** + * A root node containing child nodes of the results of a multi-case keyword + * Search. + */ +class MultiCaseKeywordSearchNode extends AbstractNode { + + private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchNode.class.getName()); + + /** + * Construct a new MultiCaseKeywordSearchNode + * + * @param resultList the list of KeywordSearchHits which will be the + * children of this node. + */ + MultiCaseKeywordSearchNode(Collection resultList) { + super(new MultiCaseKeywordSearchChildren(resultList)); + } + + /** + * A factory for creating children of the MultiCaseKeywordSearchNode. + */ + static class MultiCaseKeywordSearchChildren extends Children.Keys { + private final Collection resultList; + + /** + * Construct a new MultiCaseKeywordSearchChildren + * + * @param resultList the list of KeywordSearchHits which will be used to + * construct the children. + */ + MultiCaseKeywordSearchChildren(Collection resultList) { + this.resultList = resultList; + } + + @Override + protected void addNotify() { + super.addNotify(); + setKeys(resultList); + } + + @Override + protected void removeNotify() { + super.removeNotify(); + setKeys(Collections.emptyList()); + } + + @Override + protected Node[] createNodes(SearchHit t) { + return new Node[]{new SearchHitNode(t)}; + } + + @Override + public Object clone() { + return super.clone(); + } + + } + + /** + * A leaf node which represents a hit for the multi-case keyword search. + */ + static final class SearchHitNode extends AbstractNode { + + private final SearchHit searchHit; + + /** + * Construct a new SearchHitNode + * + * @param kwsHit the KeywordSearchHit which will be represented by this + * node. + */ + SearchHitNode(SearchHit kwsHit) { + super(Children.LEAF); + searchHit = kwsHit; + super.setName(searchHit.getCaseDisplayName()); + setDisplayName(searchHit.getCaseDisplayName()); + } + + @Override + public Action getPreferredAction() { + return new OpenCaseAction(getCasePath()); + } + + /** + * Get the path to the case directory + * + * @return the path to the case directory for the KeywordSearchHit + * represented by this node + */ + private String getCasePath() { + return searchHit.getCaseDirectoryPath(); + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); + } + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), + searchHit.getCaseDisplayName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), + searchHit.getCaseDirectoryPath())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), + searchHit.getDataSourceName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), + searchHit.getSourcePath())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), + searchHit.getSourceType().getDisplayName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), + searchHit.getSourceName())); + return s; + } + + @Override + public Action[] getActions(boolean context) { + List actions = new ArrayList<>(); + actions.add(new OpenCaseAction(getCasePath())); + actions.add(new CopyResultAction(searchHit)); + return actions.toArray(new Action[actions.size()]); + } + } + + @NbBundle.Messages({"MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard"}) + /** + * Put the contents of the selected row in the clipboard in the same tab + * seperated format as pressing ctrl+c. + */ + private static class CopyResultAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + + SearchHit result; + + /** + * Construct a new CopyResultAction + */ + CopyResultAction(SearchHit selectedResult) { + super(Bundle.MultiCaseKeywordSearchNode_copyResultAction_text()); + result = selectedResult; + } + + @Override + public void actionPerformed(ActionEvent e) { + StringSelection resultSelection = new StringSelection(result.getCaseDisplayName()+ "\t" + + result.getCaseDirectoryPath() + "\t" + + result.getDataSourceName() + "\t" + + result.getSourceType().getDisplayName() + "\t" + + result.getSourceName() + "\t" + + result.getSourcePath() + "\t"); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(resultSelection, resultSelection); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); //To change body of generated methods, choose Tools | Templates. + } + + } + + @NbBundle.Messages({"MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case"}) + /** + * Action to open the case associated with the selected node. + */ + private static class OpenCaseAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private final String caseDirPath; + + /** + * Finds the path to the .aut file for the specified case directory. + * + * @param caseDirectory the directory to check for a .aut file + * + * @return the path to the first .aut file found in the directory + * + * @throws CaseActionException if there was an issue finding a .aut file + */ + private static String findAutFile(String caseDirectory) throws CaseActionException { + File caseFolder = Paths.get(caseDirectory).toFile(); + if (caseFolder.exists()) { + /* + * Search for '*.aut' files. + */ + File[] fileArray = caseFolder.listFiles(); + if (fileArray == null) { + throw new CaseActionException("No files found in case directory"); + } + String autFilePath = null; + for (File file : fileArray) { + String name = file.getName().toLowerCase(); + if (autFilePath == null && name.endsWith(getFileExtension())) { + return file.getAbsolutePath(); + } + } + throw new CaseActionException("No .aut files found in case directory"); + } + throw new CaseActionException("Case directory was not found"); + } + + /** + * Construct a new open case action + * + * @param path the path to the case directory for the case to open + */ + OpenCaseAction(String path) { + super(Bundle.MultiCaseKeywordSearchNode_OpenCaseAction_text()); + caseDirPath = path; + } + + @Override + public void actionPerformed(ActionEvent e) { + StartupWindowProvider.getInstance().close(); + new Thread( + () -> { + try { + Case.openAsCurrentCase(findAutFile(caseDirPath)); + } 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", caseDirPath), ex); //NON-NLS + MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); + } + } + } + ).start(); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); //To change body of generated methods, choose Tools | Templates. + } + + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java new file mode 100755 index 0000000000..2cc30ab381 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.sleuthkit.autopsy.core.UserPreferences; + +@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.experimental.autoingest.MultiCaseKeywordSearchOpenAction") +@ActionReference(path = "Menu/Tools", position = 202) +@ActionRegistration(displayName = "#CTL_MultiCaseKeywordSearchOpenAction", lazy = false) +@Messages({"CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search"}) +/** + * Action to open the top level component for the multi-case keyword search. + */ +public final class MultiCaseKeywordSearchOpenAction extends CallableSystemAction { + + private static final String DISPLAY_NAME = Bundle.CTL_MultiCaseKeywordSearchOpenAction(); + private static final long serialVersionUID = 1L; + + @Override + public boolean isEnabled() { + return UserPreferences.getIsMultiUserModeEnabled(); + } + + @Override + public void performAction() { + MultiCaseKeywordSearchTopComponent.openTopComponent(); + } + + @Override + public String getName() { + return DISPLAY_NAME; + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form new file mode 100755 index 0000000000..ffe256e8da --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form @@ -0,0 +1,432 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java new file mode 100755 index 0000000000..6aed7b3aa4 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java @@ -0,0 +1,908 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import com.google.common.eventbus.Subscribe; +import com.google.common.eventbus.DeadEvent; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.logging.Level; +import javax.swing.AbstractButton; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.table.TableColumn; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.table.TableCellRenderer; +import org.netbeans.swing.outline.DefaultOutlineModel; +import org.openide.explorer.ExplorerManager; +import org.netbeans.swing.outline.Outline; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.EmptyNode; +import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseSearcher.MultiCaseSearcherException; +import org.sleuthkit.autopsy.keywordsearch.multicase.SearchQuery.QueryType; + +/** + * Panel to display the controls and results for the multi-case search. + */ +final class MultiCaseKeywordSearchPanel extends javax.swing.JPanel implements ExplorerManager.Provider { + + @Messages({ + "MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait..." + }) + private static final long serialVersionUID = 1L; + private volatile SearchThread searchThread = null; + private final Outline outline; + private final ExplorerManager em; + private final org.openide.explorer.view.OutlineView outlineView; + private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchPanel.class.getName()); + private static final EmptyNode PLEASE_WAIT_NODE = new EmptyNode(Bundle.MultiCaseKeywordSearchPanel_emptyNode_waitText()); + private static final MultiCaseKeywordSearchNode NO_RESULTS_NODE = new MultiCaseKeywordSearchNode(new ArrayList<>()); + private Collection allSearchHits = new ArrayList<>(); + private Collection searchExceptions = new ArrayList<>(); + private SelectMultiUserCasesDialog caseSelectionDialog = SelectMultiUserCasesDialog.getInstance(); + private Node[] currentSelections; + + /** + * Creates new form MultiCaseKeywordSearchPanel + */ + MultiCaseKeywordSearchPanel() { + em = new ExplorerManager(); + outlineView = new org.openide.explorer.view.OutlineView(); + outline = outlineView.getOutline(); + outlineView.setPropertyColumns( + Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), + Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), + Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), + Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), + Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source()); + ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.MultiCaseKeywordSearchNode_properties_case()); + initComponents(); + outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + outline.setRootVisible(false); + outlineView.setPreferredSize(resultsScrollPane.getPreferredSize()); + resultsScrollPane.setViewportView(outlineView); + caseSelectionDialog.subscribeToNewCaseSelections(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + populateCasesList((Node[])e.getSource()); + revalidate(); + } + }); + searchEnabled(true); + outline.setRowSelectionAllowed(false); + searchProgressBar.setVisible(false); + exportButton.setEnabled(false); + outline.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + setColumnWidths(); + } + + /** + * If a collection of SearchHits is received update the results shown on the + * panel to include them. + * + * @param hits the collection of SearchHits which was received. + */ + @Messages({"MultiCaseKeywordSearchPanel.countOfResults.label=Count: "}) + @Subscribe + void subscribeToResults(Collection hits) { + allSearchHits.addAll(hits); + if (allSearchHits.size() > 0) { + MultiCaseKeywordSearchNode resultsNode = new MultiCaseKeywordSearchNode(allSearchHits); + SwingUtilities.invokeLater(() -> { + em.setRootContext(resultsNode); + outline.setRowSelectionAllowed(true); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + Integer.toString(outline.getRowCount())); + }); + } else { + em.setRootContext(NO_RESULTS_NODE); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); + } + } + + /** + * If a string is received and it matches the + * MultiCaseSearcher.SEARCH_COMPLETE_STRING reset elements of this panel + * to reflect that the search is done. + * + * @param stringRecived the String which was received + */ + @Subscribe + void subscribeToStrings(String stringReceived) { + if (stringReceived.equals(MultiCaseSearcher.getSearchCompleteMessage())) { + searchThread.unregisterWithSearcher(MultiCaseKeywordSearchPanel.this); + searchThread = null; + searchEnabled(true); + if (!searchExceptions.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_errorsEncounter_text(searchExceptions.size())); + } + if (!em.getRootContext().equals(PLEASE_WAIT_NODE) && !em.getRootContext().equals(NO_RESULTS_NODE)) { + exportButton.setEnabled(true); + SwingUtilities.invokeLater(() -> { + exportButton.setEnabled(true); + setColumnWidths(); + }); + } + } else { + //If it is not the SEARCH_COMPLETE_STRING log it. + LOGGER.log(Level.INFO, "String posted to MultiCaseKeywordSearchPanel EventBus with value of " + stringReceived); + } + } + + /** + * If a InterruptedException is received over the EventBus update the + * warning label. + * + * @param exception the InterruptedException which was received. + */ + @Subscribe + void subscribeToInterruptionExceptions(InterruptedException exception) { + warningLabel.setText(exception.getMessage()); + //if we are still displaying please wait force it to update to no results + if (em.getRootContext().equals(PLEASE_WAIT_NODE)) { + em.setRootContext(NO_RESULTS_NODE); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); + } + } + + /** + * If a MultiCaseSearcherException is received over the EventBus cancel the + * current search and update the warning label. + * + * @param exception the MultiCaseSearcherException which was received. + */ + @Messages({"# {0} - numberOfErrors", + "MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search" + }) + @Subscribe + void subscribeToMultiCaseSearcherExceptions(MultiCaseSearcherException exception) { + searchExceptions.add(exception); + } + + /** + * Log all other events received over the event bus which are not + * specifically covered by another @Subscribe method + * + * @param deadEvent Any object received over the event bus which was not of + * a type otherwise subscribed to + */ + @Subscribe + void subscribeToDeadEvents(DeadEvent deadEvent) { + LOGGER.log(Level.INFO, "Dead Event posted to MultiCaseKeywordSearchPanel EventBus " + deadEvent.toString()); + } + + private void displaySearchErrors() { + if (!searchExceptions.isEmpty()) { + StringBuilder strBuilder = new StringBuilder(""); + searchExceptions.forEach((exception) -> { + strBuilder.append("- ").append(exception.getMessage()).append(System.lineSeparator()); + }); + SwingUtilities.invokeLater(() -> { + new MultiCaseKeywordSearchErrorDialog(strBuilder.toString()); + }); + } + + } + + /** + * Get the list of cases from the Multi user case browser + */ + private void populateCasesList(Node[] selectedNodes) { + Collection disabledCases = getCases(false); + casesPanel.removeAll(); + currentSelections = selectedNodes; + int casePanelWidth = casesPanel.getPreferredSize().width; + int heightOfAllRows = 0; + for(Node data : selectedNodes) { + //select all new cases and cases which were previously selected + String multiUserCaseName = data.getName(); + boolean isSelected = true; + if (disabledCases.contains(multiUserCaseName)) { + isSelected = false; + } + JCheckBox caseCheckBox = new JCheckBox(multiUserCaseName, isSelected); + caseCheckBox.setBackground(Color.white); + if (casePanelWidth < caseCheckBox.getPreferredSize().width) { + casePanelWidth = caseCheckBox.getPreferredSize().width; + } + heightOfAllRows += caseCheckBox.getPreferredSize().height; + casesPanel.add(caseCheckBox); + } + casesPanel.setPreferredSize(new Dimension(casePanelWidth, heightOfAllRows)); + } + + @Override + public ExplorerManager getExplorerManager() { + return em; + } + + /** + * 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() { + + searchTypeGroup = new javax.swing.ButtonGroup(); + searchButton = new javax.swing.JButton(); + substringRadioButton = new javax.swing.JRadioButton(); + keywordTextField = new javax.swing.JTextField(); + exactRadioButton = new javax.swing.JRadioButton(); + regexRadioButton = new javax.swing.JRadioButton(); + casesScrollPane = new javax.swing.JScrollPane(); + casesPanel = new javax.swing.JPanel(); + casesLabel = new javax.swing.JLabel(); + resultsLabel = new javax.swing.JLabel(); + uncheckButton = new javax.swing.JButton(); + checkButton = new javax.swing.JButton(); + toolDescriptionScrollPane = new javax.swing.JScrollPane(); + toolDescriptionTextArea = new javax.swing.JTextArea(); + resultsScrollPane = new javax.swing.JScrollPane(); + cancelButton = new javax.swing.JButton(); + searchProgressBar = new javax.swing.JProgressBar(); + warningLabel = new javax.swing.JLabel(); + exportButton = new javax.swing.JButton(); + resultsCountLabel = new javax.swing.JLabel(); + viewErrorsButton = new javax.swing.JButton(); + pickCasesButton = new javax.swing.JButton(); + + setName(""); // NOI18N + setOpaque(false); + setPreferredSize(new java.awt.Dimension(1000, 442)); + + org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.searchButton.text")); // NOI18N + searchButton.setMaximumSize(new java.awt.Dimension(84, 23)); + searchButton.setMinimumSize(new java.awt.Dimension(84, 23)); + searchButton.setPreferredSize(new java.awt.Dimension(84, 23)); + searchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchButtonActionPerformed(evt); + } + }); + + searchTypeGroup.add(substringRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(substringRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.substringRadioButton.text_1")); // NOI18N + substringRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + substringRadioButtonActionPerformed(evt); + } + }); + + keywordTextField.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N + keywordTextField.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.keywordTextField.text_1")); // NOI18N + keywordTextField.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(192, 192, 192), 1, true)); + keywordTextField.setMinimumSize(new java.awt.Dimension(2, 25)); + keywordTextField.setPreferredSize(new java.awt.Dimension(2, 25)); + + searchTypeGroup.add(exactRadioButton); + exactRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(exactRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exactRadioButton.text_1")); // NOI18N + + searchTypeGroup.add(regexRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(regexRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.regexRadioButton.text_1")); // NOI18N + + casesScrollPane.setPreferredSize(new java.awt.Dimension(174, 281)); + + casesPanel.setBackground(new java.awt.Color(255, 255, 255)); + casesPanel.setPreferredSize(new java.awt.Dimension(152, 197)); + casesPanel.setLayout(new javax.swing.BoxLayout(casesPanel, javax.swing.BoxLayout.Y_AXIS)); + casesScrollPane.setViewportView(casesPanel); + + org.openide.awt.Mnemonics.setLocalizedText(casesLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.casesLabel.text_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(resultsLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(uncheckButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.uncheckButton.text")); // NOI18N + uncheckButton.setMargin(new java.awt.Insets(2, 6, 2, 6)); + uncheckButton.setMaximumSize(new java.awt.Dimension(84, 23)); + uncheckButton.setMinimumSize(new java.awt.Dimension(84, 23)); + uncheckButton.setPreferredSize(new java.awt.Dimension(84, 23)); + uncheckButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + uncheckButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(checkButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.checkButton.text")); // NOI18N + checkButton.setMaximumSize(new java.awt.Dimension(84, 23)); + checkButton.setMinimumSize(new java.awt.Dimension(84, 23)); + checkButton.setPreferredSize(new java.awt.Dimension(84, 23)); + checkButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkButtonActionPerformed(evt); + } + }); + + toolDescriptionTextArea.setEditable(false); + toolDescriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); + toolDescriptionTextArea.setColumns(20); + toolDescriptionTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N + toolDescriptionTextArea.setLineWrap(true); + toolDescriptionTextArea.setRows(3); + toolDescriptionTextArea.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text")); // NOI18N + toolDescriptionTextArea.setWrapStyleWord(true); + toolDescriptionTextArea.setFocusable(false); + toolDescriptionScrollPane.setViewportView(toolDescriptionTextArea); + + resultsScrollPane.setMinimumSize(new java.awt.Dimension(100, 40)); + resultsScrollPane.setPreferredSize(new java.awt.Dimension(200, 100)); + resultsScrollPane.setRequestFocusEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.cancelButton.text")); // NOI18N + cancelButton.setEnabled(false); + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + warningLabel.setForeground(new java.awt.Color(200, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.warningLabel.text")); // NOI18N + warningLabel.setFocusable(false); + + org.openide.awt.Mnemonics.setLocalizedText(exportButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exportButton.text")); // NOI18N + exportButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + exportButton.setMaximumSize(new java.awt.Dimension(84, 23)); + exportButton.setMinimumSize(new java.awt.Dimension(84, 23)); + exportButton.setPreferredSize(new java.awt.Dimension(84, 23)); + exportButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportButtonActionPerformed(evt); + } + }); + + resultsCountLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + org.openide.awt.Mnemonics.setLocalizedText(resultsCountLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsCountLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(viewErrorsButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.viewErrorsButton.text")); // NOI18N + viewErrorsButton.setEnabled(false); + viewErrorsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + viewErrorsButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(pickCasesButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.pickCasesButton.text_1")); // NOI18N + pickCasesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pickCasesButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(exactRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(substringRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(regexRadioButton)) + .addComponent(keywordTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 570, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(toolDescriptionScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 84, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(casesLabel) + .addComponent(casesScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(searchButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pickCasesButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(uncheckButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(checkButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(resultsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 154, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 98, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(viewErrorsButton) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 695, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(exportButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(cancelButton, javax.swing.GroupLayout.DEFAULT_SIZE, 87, Short.MAX_VALUE)))))) + .addContainerGap()) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(196, 196, 196) + .addComponent(searchProgressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 769, Short.MAX_VALUE) + .addGap(108, 108, 108))) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {checkButton, uncheckButton}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(layout.createSequentialGroup() + .addComponent(keywordTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(regexRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(exactRadioButton) + .addComponent(substringRadioButton)))) + .addComponent(toolDescriptionScrollPane)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(casesLabel) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(resultsLabel) + .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(casesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(uncheckButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(checkButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(exportButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(searchButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(viewErrorsButton) + .addComponent(pickCasesButton)) + .addComponent(cancelButton)) + .addContainerGap()) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap(433, Short.MAX_VALUE) + .addComponent(searchProgressBar, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap())) + ); + }// //GEN-END:initComponents + + @Messages({ + "MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search.", + "MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field." + }) + /** + * perform a search if the previous search is done or no previous search has + * occured + */ + private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed + if (null == searchThread) { + Collection cases = getCases(true); + String searchString = keywordTextField.getText(); + if (cases.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_noCases()); + } else if (searchString.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_emptySearch()); + } else { + //perform the search + warningLabel.setText(""); + allSearchHits = new ArrayList<>(); + searchExceptions = new ArrayList<>(); + searchEnabled(false); + exportButton.setEnabled(false); + outline.setRowSelectionAllowed(false); + SearchQuery kwsQuery = new SearchQuery(getQueryType(), searchString); + em.setRootContext(PLEASE_WAIT_NODE); + resultsCountLabel.setText(""); + searchThread = new SearchThread(cases, kwsQuery); + searchThread.registerWithSearcher(MultiCaseKeywordSearchPanel.this); + searchThread.start(); + } + } + }//GEN-LAST:event_searchButtonActionPerformed + + /** + * Get the cases which match the selected status specified by + * isSelected. + * + * @param isSelected true to get selected cases false to get + * unselected cases + * + * @return cases the cases that match the selected status of + * isSelected + */ + private Collection getCases(boolean isSelected) { + Collection cases = new HashSet<>(); + for (Component comp : casesPanel.getComponents()) { + if (comp instanceof JCheckBox) { + if (((AbstractButton) comp).isSelected() == isSelected) { + cases.add(((AbstractButton) comp).getText()); + } + } + } + return cases; + } + + /** + * Get the type of Query which was selected by the user. + * + * @return one of the values of the QueryType enum + */ + private QueryType getQueryType() { + String queryTypeText = ""; + Enumeration buttonGroup = searchTypeGroup.getElements(); + while (buttonGroup.hasMoreElements()) { + AbstractButton dspButton = buttonGroup.nextElement(); + if (dspButton.isSelected()) { + queryTypeText = dspButton.getText(); + break; + } + } + if (queryTypeText.equals(substringRadioButton.getText())) { + return QueryType.SUBSTRING; + } else if (queryTypeText.equals(regexRadioButton.getText())) { + return QueryType.REGEX; + } else { + //default to Exact match + return QueryType.EXACT_MATCH; + } + } + + /** + * Set the column widths to have their width influenced by the width of the + * content in them for up to the first hundred rows. + */ + private void setColumnWidths() { + int widthLimit = 1000; + int margin = 4; + int padding = 8; + for (int col = 0; col < outline.getColumnCount(); col++) { + int width = 115; //min initial width for columns + int rowsToResize = Math.min(outline.getRowCount(), 100); + for (int row = 0; row < rowsToResize; row++) { + if (outline.getValueAt(row, col) != null) { + TableCellRenderer renderer = outline.getCellRenderer(row, col); + Component comp = outline.prepareRenderer(renderer, row, col); + width = Math.max(comp.getPreferredSize().width, width); + } + + } + width += 2 * margin + padding; + width = Math.min(width, widthLimit); + TableColumn column = outline.getColumnModel().getColumn(outline.convertColumnIndexToModel(col)); + column.setPreferredWidth(width); + } + resultsScrollPane.setPreferredSize(new Dimension(outline.getPreferredSize().width, resultsScrollPane.getPreferredSize().height)); + } + + /** + * Un-select all check boxes in the cases list + * + * @param evt ignored + */ + private void uncheckButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_uncheckButtonActionPerformed + allCheckboxesSetSelected(false); + }//GEN-LAST:event_uncheckButtonActionPerformed + + /** + * Select all check boxes in the cases list + * + * @param evt ignored + */ + private void checkButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkButtonActionPerformed + allCheckboxesSetSelected(true); + }//GEN-LAST:event_checkButtonActionPerformed + + /** + * Cancel the current multi-case search which is being performed. + * + * @param evt ignored + */ + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + cancelSearch(); + }//GEN-LAST:event_cancelButtonActionPerformed + + /** + * Cancel the current multi-case search which is being performed. + */ + @Messages({ + "MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search"}) + private void cancelSearch() { + if (null != searchThread) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_searchThread_cancellingText()); + searchThread.interrupt(); + } + } + + @Messages({"MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv)", + "MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export", + "MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed" + }) + /** + * Export the currently displayed search results to a file specified by the + * user with data saved in comma seperated format. + */ + private void exportButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportButtonActionPerformed + JFileChooser chooser = new JFileChooser(); + final String EXTENSION = "csv"; //NON-NLS + FileNameExtensionFilter csvFilter = new FileNameExtensionFilter( + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_csvExtensionFilterlbl(), EXTENSION); + chooser.setFileFilter(csvFilter); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setName("Choose file to export results to"); + chooser.setMultiSelectionEnabled(false); + int returnVal = chooser.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File selFile = chooser.getSelectedFile(); + if (selFile == null) { + JOptionPane.showMessageDialog(this, + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.WARNING_MESSAGE); + LOGGER.warning("Selected file was null, when trying to export search results"); + return; + } + String fileAbs = selFile.getAbsolutePath(); + if (!fileAbs.endsWith("." + EXTENSION)) { + fileAbs = fileAbs + "." + EXTENSION; + selFile = new File(fileAbs); + } + saveResultsAsTextFile(selFile); + } + }//GEN-LAST:event_exportButtonActionPerformed + + private void viewErrorsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewErrorsButtonActionPerformed + displaySearchErrors(); + }//GEN-LAST:event_viewErrorsButtonActionPerformed + + private void pickCasesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pickCasesButtonActionPerformed + if(currentSelections != null) { + caseSelectionDialog.setNodeSelections(currentSelections); + } + caseSelectionDialog.setVisible(true); + }//GEN-LAST:event_pickCasesButtonActionPerformed + + private void substringRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_substringRadioButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_substringRadioButtonActionPerformed + + /** + * Set the user interface elements to reflect whether the search feature is + * currently enabled or disabled. + * + * @param canSearch True if the search feature should be enabled, false if + * it should be disabled. + */ + private void searchEnabled(boolean canSearch) { + searchButton.setEnabled(canSearch); + cancelButton.setEnabled(!canSearch); + viewErrorsButton.setEnabled(canSearch); + viewErrorsButton.setVisible(!searchExceptions.isEmpty()); + } + + @Messages({"# {0} - file name", + "MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite?", + "# {0} - file name", + "MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0}" + }) + /** + * Saves the results to the file specified + */ + private void saveResultsAsTextFile(File resultsFile) { + if (resultsFile.exists()) { + //if the file already exists ask the user how to proceed + boolean shouldWrite = JOptionPane.showConfirmDialog(null, + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_fileExistPrompt(resultsFile.getName()), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) + == JOptionPane.YES_OPTION; + if (!shouldWrite) { + return; + } + } + try { + BufferedWriter resultsWriter; + resultsWriter = new BufferedWriter(new FileWriter(resultsFile)); + int col = 0; + //write headers + while (col < outline.getColumnCount()) { + + resultsWriter.write(outline.getColumnName(col)); + col++; + if (col < outline.getColumnCount()) { + resultsWriter.write(","); + } + } + resultsWriter.write(System.lineSeparator()); + //write data + Children resultsChildren = em.getRootContext().getChildren(); + for (int row = 0; row < resultsChildren.getNodesCount(); row++) { + col = 0; + while (col < outline.getColumnCount()) { + if (outline.getValueAt(row, col) instanceof Node.Property) { + resultsWriter.write(((Node.Property) outline.getValueAt(row, col)).getValue().toString()); + } else { + resultsWriter.write(outline.getValueAt(row, col).toString()); + } + col++; + if (col < outline.getColumnCount()) { + resultsWriter.write(","); + } + } + resultsWriter.write(System.lineSeparator()); + } + resultsWriter.flush(); + resultsWriter.close(); + setColumnWidths(); + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_exportMsg(resultsFile.getName()), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.INFORMATION_MESSAGE); + } catch (IllegalAccessException | IOException | InvocationTargetException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.WARNING_MESSAGE); + LOGGER.log(Level.WARNING, "Export of search results failed unable to write results csv file", ex); + } + } + + /** + * Set the selected status of all checkboxes. + * + * @param selected true if all checkboxes should be selected, false if no + * check boxes should be selected. + */ + private void allCheckboxesSetSelected(boolean selected) { + for (Component comp : casesPanel.getComponents()) { + if (comp instanceof JCheckBox) { + ((AbstractButton) comp).setSelected(selected); + } + } + } + + /** + * Ask the user if they want to continue their search while this window is + * closed. Cancels the current search if they select no. + */ + @Messages({ + "MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. " + + "Would you like the search to continue in the background while the search window is closed?", + "MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search" + }) + void closeSearchPanel() { + if (cancelButton.isEnabled()) { + boolean shouldContinueSearch = JOptionPane.showConfirmDialog(null, + Bundle.MultiCaseKeywordSearchPanel_continueSearch_text(), + Bundle.MultiCaseKeywordSearchPanel_continueSearch_title(), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) + == JOptionPane.YES_OPTION; + if (!shouldContinueSearch) { + cancelSearch(); + } + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JLabel casesLabel; + private javax.swing.JPanel casesPanel; + private javax.swing.JScrollPane casesScrollPane; + private javax.swing.JButton checkButton; + private javax.swing.JRadioButton exactRadioButton; + private javax.swing.JButton exportButton; + private javax.swing.JTextField keywordTextField; + private javax.swing.JButton pickCasesButton; + private javax.swing.JRadioButton regexRadioButton; + private javax.swing.JLabel resultsCountLabel; + private javax.swing.JLabel resultsLabel; + private javax.swing.JScrollPane resultsScrollPane; + private javax.swing.JButton searchButton; + private javax.swing.JProgressBar searchProgressBar; + private javax.swing.ButtonGroup searchTypeGroup; + private javax.swing.JRadioButton substringRadioButton; + private javax.swing.JScrollPane toolDescriptionScrollPane; + private javax.swing.JTextArea toolDescriptionTextArea; + private javax.swing.JButton uncheckButton; + private javax.swing.JButton viewErrorsButton; + private javax.swing.JLabel warningLabel; + // End of variables declaration//GEN-END:variables + + /* + * A thread that performs a keyword search of cases + */ + private final class SearchThread extends Thread { + + private final Collection caseNames; + private final SearchQuery searchQuery; + private final MultiCaseSearcher multiCaseSearcher = new MultiCaseSearcher(); + + /** + * Constructs a thread that performs a keyword search of cases + * + * @param caseNames The names of the cases to search. + * @param query The keyword search query to perform. + */ + private SearchThread(Collection caseNames, SearchQuery searchQuery) { + this.caseNames = caseNames; + this.searchQuery = searchQuery; + } + + /** + * Register an object with the MultiCaseSearcher eventBus so that the + * object's subscribe methods can receive results. + * + * @param object the object to register with the MultiCaseSearcher + */ + private void registerWithSearcher(Object object) { + multiCaseSearcher.registerWithEventBus(object); + } + + /** + * Unregister an object with the MultiCaseSearcher so that the + * object's subscribe methods no longer receive results. + * + * @param object the object to unregister with the MultiCaseSearcher + */ + private void unregisterWithSearcher(Object object) { + multiCaseSearcher.unregisterWithEventBus(object); + } + + @Override + public void interrupt() { + super.interrupt(); + //in case it is running a method which causes InterruptedExceptions to be ignored + multiCaseSearcher.stopMultiCaseSearch(); + } + + @Override + public void run() { + multiCaseSearcher.performKeywordSearch(caseNames, searchQuery, new MultiCaseKeywordSearchProgressIndicator(searchProgressBar)); + } + + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java new file mode 100755 index 0000000000..d63b2a1f8e --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java @@ -0,0 +1,167 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * A progress indicator that updates a JProgressBar. + */ +final class MultiCaseKeywordSearchProgressIndicator implements ProgressIndicator { + + private final JProgressBar progress; + + /** + * Construct a new JProgressIndicator + * + * @param progressBar the JProgressBar you want this indicator to update + */ + MultiCaseKeywordSearchProgressIndicator(JProgressBar progressBar) { + progress = progressBar; + progress.setStringPainted(true); + } + + /** + * Start showing progress in the progress bar. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param max The total number of work units to be completed. + */ + @Override + public void start(String message, int max) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(false); + progress.setMinimum(0); + progress.setString(message); //the message + progress.setValue(0); + progress.setMaximum(max); + progress.setVisible(true); + }); + } + + /** + * Start showing progress in the progress bar. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void start(String message) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(true); + progress.setMinimum(0); + progress.setString(message); + progress.setValue(0); + progress.setVisible(true); + }); + } + + /** + * Switches the progress indicator to indeterminate mode (the total number + * of work units to be completed is unknown). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void switchToIndeterminate(String message) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(true); + progress.setString(message); + }); + } + + /** + * Switches the progress indicator to determinate mode (the total number of + * work units to be completed is known). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param current The number of work units completed so far. + * @param max The total number of work units to be completed. + */ + @Override + public void switchToDeterminate(String message, int current, int max) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(false); + progress.setMinimum(0); + progress.setString(message); + progress.setValue(current); + progress.setMaximum(max); + }); + } + + /** + * Updates the progress indicator with a progress message. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void progress(String message) { + SwingUtilities.invokeLater(() -> { + progress.setString(message); + }); + } + + /** + * Updates the progress indicator with the number of work units completed so + * far when in determinate mode (the total number of work units to be + * completed is known). + * + * @param current Number of work units completed so far. + */ + @Override + public void progress(int current) { + SwingUtilities.invokeLater(() -> { + progress.setValue(current); + }); + } + + /** + * Updates the progress indicator with a progress message and the number of + * work units completed so far when in determinate mode (the total number of + * work units to be completed is known). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param current Number of work units completed so far. + */ + @Override + public void progress(String message, int current) { + SwingUtilities.invokeLater(() -> { + progress.setString(message); + progress.setValue(current); + }); + } + + /** + * Finishes the progress indicator when the task is completed. + */ + @Override + public void finish() { + SwingUtilities.invokeLater(() -> { + progress.setVisible(false); + }); + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form new file mode 100755 index 0000000000..cbd81a1c12 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form @@ -0,0 +1,33 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java new file mode 100755 index 0000000000..3ce6b28505 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java @@ -0,0 +1,140 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import java.awt.BorderLayout; +import java.awt.Component; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.openide.windows.Mode; + +@TopComponent.Description( + preferredID = "MultiCaseKeywordSearchTopComponent", + persistenceType = TopComponent.PERSISTENCE_NEVER +) +@TopComponent.Registration(mode = "multiCaseKeywordSearch", openAtStartup = false) +@Messages({ + "CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search", + "CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search"}) +/** + * A top level component for the multi case keyword search feature. + */ +final class MultiCaseKeywordSearchTopComponent extends TopComponent { + + public final static String PREFERRED_ID = "MultiCaseKeywordSearchTopComponent"; // NON-NLS + private static final long serialVersionUID = 1L; + private static boolean topComponentInitialized = false; + + @Messages({ + "MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel.",}) + /** + * Open the top level component if it is not already open, if it is open + * bring it to the front and select it. + */ + static void openTopComponent() { + final MultiCaseKeywordSearchTopComponent tc = (MultiCaseKeywordSearchTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (tc != null) { + if (tc.isOpened() == false) { + topComponentInitialized = true; + Mode mode = WindowManager.getDefault().findMode("multiCaseKeywordSearch"); // NON-NLS + if (mode != null) { + mode.dockInto(tc); + } + tc.open(); + } + tc.toFront(); + tc.requestActive(); + } + } + + /** + * Close the top level componet. + */ + static void closeTopComponent() { + if (topComponentInitialized) { + final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (tc != null) { + try { + tc.close(); + } catch (Exception e) { + + } + } + } + } + + @Messages({"MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search"}) + /** + * Construct a new "MultiCaseKeywordSearchTopComponent. + */ + MultiCaseKeywordSearchTopComponent() { + initComponents(); + setName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setDisplayName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setToolTipText(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setSize(this.getPreferredSize()); + setLayout(new BorderLayout()); + MultiCaseKeywordSearchPanel searchPanel = new MultiCaseKeywordSearchPanel(); + searchPanel.setSize(searchPanel.getPreferredSize()); + searchPanel.setVisible(true); + add(searchPanel); + } + + @Override + public void componentOpened() { + super.componentOpened(); + WindowManager.getDefault().setTopComponentFloating(this, true); + } + + @Override + public boolean canClose() { + for (Component component : getComponents()) { + if (component instanceof MultiCaseKeywordSearchPanel) { + ((MultiCaseKeywordSearchPanel) component).closeSearchPanel(); + } + } + return super.canClose(); + } + + /** + * 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. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + setPreferredSize(new java.awt.Dimension(1002, 444)); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 902, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 444, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java new file mode 100755 index 0000000000..ab0bbfc296 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java @@ -0,0 +1,845 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import com.google.common.eventbus.EventBus; +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.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.apache.commons.lang.StringUtils; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrServer; +import org.apache.solr.client.solrj.request.CoreAdminRequest; +import org.apache.solr.client.solrj.response.CoreAdminResponse; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.params.CoreAdminParams; +import org.apache.solr.common.params.CursorMarkParams; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; +import org.sleuthkit.autopsy.keywordsearch.Server; +import org.sleuthkit.autopsy.progress.ProgressIndicator; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Performs keyword searches across multiple cases + */ +final class MultiCaseSearcher { + + private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS + private static final String SEARCH_COMPLETE_MESSAGE = "SEARCH_COMPLETE"; + private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS + private static final int CASE_DIR_READ_LOCK_TIMEOUT_HOURS = 12; //NON-NLS + private static final String SOLR_SERVER_URL_FORMAT_STRING = "http://%s:%s/solr"; //NON-NLS + private static final String SOLR_CORE_URL_FORMAT_STRING = "http://%s:%s/solr/%s"; //NON-NLS + private final static String SOLR_METADATA_FILE_NAME = "SolrCore.properties"; //NON-NLS + private static final String SOLR_CORE_NAME_XPATH = "/SolrCores/Core/CoreName/text()"; //NON-NLS + private static final String TEXT_INDEX_NAME_XPATH = "/SolrCores/Core/TextIndexPath/text()"; //NON-NLS + private static final String SOLR_CORE_INSTANCE_PATH_PROPERTY = "instanceDir"; //NON-NLS + private static final String SOLR_CONFIG_SET_NAME = "AutopsyConfig"; //NON-NLS + private static final int MAX_RESULTS_PER_CURSOR_MARK = 512; + private static final String SOLR_DOC_ID_FIELD = Server.Schema.ID.toString(); //NON-NLS + private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS + private static final String SOLR_DOC_CHUNK_SIZE_FIELD = Server.Schema.CHUNK_SIZE.toString(); //NON-NLS + private static final String SOLR_DOC_ID_PARTS_SEPARATOR = "_"; + private static final Logger logger = Logger.getLogger(MultiCaseSearcher.class.getName()); + private final EventBus eventBus = new EventBus("MultiCaseSearcherEventBus"); + private static final UNCPathUtilities pathUtils = new UNCPathUtilities(); + private volatile boolean searchStopped = true; + + MultiCaseSearcher() { + + } + + static String getSearchCompleteMessage() { + return SEARCH_COMPLETE_MESSAGE; + } + + /** + * + * Performs keyword searches across multiple cases + * + * @param caseNames The names of the cases to search. + * @param query The keyword search query to perform. + * @param progressIndicator A progrss indicator for the search. + * + * @return The search results. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "MultiCaseSearcher.progressMessage.findingCases=Finding selected cases", + "MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server", + "# {0} - total cases", + "MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s)", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for \"{0}\" ({1} of {2} case(s))", + "# {0} - case directory path", + "MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0}", + "MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled" + }) + void performKeywordSearch(final Collection caseNames, final SearchQuery query, final ProgressIndicator progressIndicator) { + progressIndicator.start(Bundle.MultiCaseSearcher_progressMessage_findingCases()); + try { + searchStopped = false; //mark the search as started + final List caseMetadata = getMultiCaseMetadata(caseNames); + checkForCancellation(); + //eventBus.post("number of cases to search determined"); + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_creatingSolrQuery()); + final SolrQuery solrQuery = createSolrQuery(query); + checkForCancellation(); + final int totalCases = caseMetadata.size(); + int caseCounter = 1; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases)); + int totalSteps = 5; + progressIndicator.switchToDeterminate(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases), 0, totalCases * totalSteps); + int caseNumber = 0; + for (MultiCaseMetadata aCase : caseMetadata) { + CaseMetadata metadata = aCase.getCaseMetadata(); + String caseName = metadata.getCaseDisplayName(); + SleuthkitCase caseDatabase = null; + + int stepsCompleted = 0; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_acquiringSharedLockForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + try (CoordinationService.Lock caseDirReadLock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.CASES, aCase.getCaseMetadata().getCaseDirectory(), CASE_DIR_READ_LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) { + if (null == caseDirReadLock) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory())); + } + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_loadingSolrCoreForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + final HttpSolrServer solrServer = loadSolrCoreForCase(aCase); + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_openingCaseDbForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + caseDatabase = openCase(aCase); + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_executingSolrQueryForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + eventBus.post(executeQuery(solrServer, solrQuery, caseDatabase, aCase)); + ++stepsCompleted; + + progressIndicator.progress(stepsCompleted + caseNumber * totalSteps); + ++caseCounter; + } catch (CoordinationService.CoordinationServiceException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory()), ex); + } catch (MultiCaseSearcherException exception) { + logger.log(Level.INFO, "Exception encountered while performing multi-case keyword search", exception); + eventBus.post(exception); + } finally { + if (null != caseDatabase) { + closeCase(caseDatabase); + } + } + caseNumber++; + } + } catch (InterruptedException exception) { + logger.log(Level.INFO, Bundle.MultiCaseSearcher_exceptionMessage_cancelledMessage(), exception); + eventBus.post(exception); + } catch (MultiCaseSearcherException exception) { + logger.log(Level.WARNING, "Exception encountered while performing multi-case keyword search", exception); + eventBus.post(new InterruptedException("Exception encountered while performing multi-case keyword search")); + eventBus.post(exception); + } finally { + progressIndicator.finish(); + eventBus.post(SEARCH_COMPLETE_MESSAGE); + } + } + + /** + * Gets metadata for the cases associated with one or more with the search + * + * @param caseNames The names of the cases to search. + * + * @return The metadata for the cases. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + private List getMultiCaseMetadata(final Collection caseNames) throws MultiCaseSearcherException, InterruptedException { + final Map casesToCasePaths = getCaseDirectories(caseNames); + checkForCancellation(); + final List cases = new ArrayList<>(); + for (Map.Entry entry : casesToCasePaths.entrySet()) { + final Path caseDirectoryPath = entry.getKey(); + final CaseMetadata caseMetadata = getCaseMetadata(caseDirectoryPath); + checkForCancellation(); + final TextIndexMetadata textIndexMetadata = getTextIndexMetadata(caseDirectoryPath); + checkForCancellation(); + cases.add(new MultiCaseMetadata(caseMetadata, textIndexMetadata)); + } + return cases; + } + + /** + * Uses coordination service data to find the case directories of the cases. + * + * @param caseNames The names of the cases. + * + * @return A mapping of case directory paths to case names, + * possibly empty. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - host", "# {1} - port", "MultiCaseSearcher.exceptionMessage.failedToQueryCoordinationServer=Failed to obtain read lock for case directory at {0}:{1}", + "# {0} - list of cases", "MultiCaseSearcher.exceptionMessage.noCasesFound=No cases found for: {0}" + }) + private Map getCaseDirectories(final Collection caseNames) throws MultiCaseSearcherException, InterruptedException { + final Map casePathToCaseMap = new HashMap<>(); + final List caseNodeNames; + try { + CoordinationService coordinationService = CoordinationService.getInstance(); + caseNodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); + } catch (CoordinationService.CoordinationServiceException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToQueryCoordinationServer(UserPreferences.getIndexingServerHost(), UserPreferences.getIndexingServerPort()), ex); + } + for (String nodeName : caseNodeNames) { + /* + * Find the case directory paths by + * selecting each coordination service case directory lock node path + * that has the case name in the path. + */ + checkForCancellation(); + final Path caseDirectoryPath = Paths.get(nodeName); + boolean contansSlash = caseDirectoryPath.toString().contains("\\") || caseDirectoryPath.toString().contains("//"); + if (!contansSlash) { + /* + * Skip case name lock nodes. + */ + continue; + } + final String fileName = caseDirectoryPath.getFileName().toString(); + if (fileName.equals(CASE_AUTO_INGEST_LOG_NAME) || fileName.endsWith(RESOURCES_LOCK_SUFFIX)) { + /* + * Skip case auto ingest log and case resource lock nodes. + */ + continue; + } + for (String aCase : caseNames) { + checkForCancellation(); + final String normalizedCaseName = aCase.toUpperCase(); + if (fileName.contains(normalizedCaseName)) { + logger.log(Level.INFO, "Match found: Case node name {0} contains case name {1}", new Object[]{nodeName, normalizedCaseName}); + try { + Path realCaseDirectoryPath = caseDirectoryPath.toRealPath(LinkOption.NOFOLLOW_LINKS); + logger.log(Level.INFO, "Case directory path {0} resolves to real path {1}", new Object[]{caseDirectoryPath, realCaseDirectoryPath}); + final File caseDirectory = realCaseDirectoryPath.toFile(); + if (caseDirectory.exists()) { + casePathToCaseMap.put(realCaseDirectoryPath, aCase); + } else { + logger.log(Level.SEVERE, String.format("Case directory %s does NOT exist", caseDirectoryPath)); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Case directory path %s does NOT resolve to a real path", caseDirectoryPath), ex); + } + break; + } + } + } + if (casePathToCaseMap.isEmpty()) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_noCasesFound(StringUtils.join(caseNames, ','))); + } + return casePathToCaseMap; + } + + /** + * Gets the metadata for a case from the case metadata file in a given case + * directory. + * + * @param caseDirectoryPath A case directory path. + * + * @return The case metadata. + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0}", + "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0}" + }) + + private static CaseMetadata getCaseMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { + CaseMetadata caseMetadata = null; + final File[] caseFiles = caseDirectoryPath.toFile().listFiles(); + for (File file : caseFiles) { + final String fileName = file.getName().toLowerCase(); + if (fileName.endsWith(CaseMetadata.getFileExtension())) { + try { + return new CaseMetadata(file.toPath()); + } catch (CaseMetadata.CaseMetadataException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToParseCaseMetadata(caseDirectoryPath), ex); + } + } + } + if (null == caseMetadata) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToFindCaseMetadata(caseDirectoryPath)); + } + return caseMetadata; + } + + /** + * Gets the text index metadata from the Solr.properties file in a given + * case directory. + * + * @param caseDirectoryPath A case directory path. + * + * @return The text index metadata. + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1}", + "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1}",}) + private static TextIndexMetadata getTextIndexMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { + final Path solrMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), SOLR_METADATA_FILE_NAME); + final File solrMetaDataFile = solrMetaDataFilePath.toFile(); + if (!solrMetaDataFile.exists() || !solrMetaDataFile.canRead()) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_missingSolrPropertiesFile(SOLR_METADATA_FILE_NAME, caseDirectoryPath)); + } + try { + final DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + final Document doc = docBuilder.parse(solrMetaDataFile); + final XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression xPathExpr = xPath.compile(SOLR_CORE_NAME_XPATH); + final String solrCoreName = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); + xPathExpr = xPath.compile(TEXT_INDEX_NAME_XPATH); + final String relativeTextIndexPath = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); + Path textIndexPath = caseDirectoryPath.resolve(relativeTextIndexPath); + textIndexPath = textIndexPath.getParent(); // Remove "index" path component + final String textIndexUNCPath = pathUtils.convertPathToUNC(textIndexPath.toString()); + return new TextIndexMetadata(caseDirectoryPath, solrCoreName, textIndexUNCPath); + } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrPropertiesFileParseError(SOLR_METADATA_FILE_NAME, caseDirectoryPath), ex); + } + } + + /** + * Converts a keyword search query into a Solr query. + * + * @param searchQuery A keyword search query. + * + * @return A Solr query. + */ + private static SolrQuery createSolrQuery(SearchQuery searchQuery) { + final SolrQuery solrQuery = new SolrQuery(); + solrQuery.setQuery(searchQuery.getSearchTerm()); + solrQuery.setRows(MAX_RESULTS_PER_CURSOR_MARK); + /* + * Note that setting the sort order is necessary for cursor based paging + * to work. + */ + solrQuery.setSort(SolrQuery.SortClause.asc(SOLR_DOC_ID_FIELD)); + solrQuery.setFields(SOLR_DOC_ID_FIELD, SOLR_DOC_CHUNK_SIZE_FIELD, SOLR_DOC_CONTENT_STR_FIELD); + return solrQuery; + } + + /** + * Connects to the Solr server and loads the Solr core for a given case. + * + * @param aCase + * + * @return A Solr server client object that can be used for executing + * queries of the specified text index. + * + * MultiCaseSearcherException + * + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - connection info", + "# {1} - case name", + "# {2} - case directory", + "MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2}" + }) + private HttpSolrServer loadSolrCoreForCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + TextIndexMetadata textIndexMetadata = aCase.getTextIndexMetadata(); + Server.IndexingServerProperties indexServer = Server.getMultiUserServerProperties(aCase.getCaseMetadata().getCaseDirectory()); + final String serverURL = String.format(SOLR_SERVER_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort()); + try { + /* + * Connect to the Solr server. + */ + final HttpSolrServer solrServer = new HttpSolrServer(serverURL); + CoreAdminRequest statusRequest = new CoreAdminRequest(); + statusRequest.setCoreName(null); + statusRequest.setAction(CoreAdminParams.CoreAdminAction.STATUS); + statusRequest.setIndexInfoNeeded(false); + checkForCancellation(); + statusRequest.process(solrServer); + checkForCancellation(); + + /* + * Load the core for the text index if it is not already loaded. + */ + CoreAdminResponse response = CoreAdminRequest.getStatus(textIndexMetadata.getSolrCoreName(), solrServer); + if (null == response.getCoreStatus(textIndexMetadata.getSolrCoreName()).get(SOLR_CORE_INSTANCE_PATH_PROPERTY)) { + CoreAdminRequest.Create loadCoreRequest = new CoreAdminRequest.Create(); + loadCoreRequest.setDataDir(textIndexMetadata.getTextIndexPath()); + loadCoreRequest.setCoreName(textIndexMetadata.getSolrCoreName()); + loadCoreRequest.setConfigSet(SOLR_CONFIG_SET_NAME); + loadCoreRequest.setIsLoadOnStartup(false); + loadCoreRequest.setIsTransient(true); + solrServer.request(loadCoreRequest); + } + + /* + * Create a server client object that can be used for executing + * queries of the specified text index. + */ + final String coreURL = String.format(SOLR_CORE_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort(), textIndexMetadata.getSolrCoreName()); + final HttpSolrServer coreServer = new HttpSolrServer(coreURL); + return coreServer; + + } catch (SolrServerException | IOException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorLoadingCore(serverURL, aCase.getCaseMetadata().getCaseName(), textIndexMetadata.getCaseDirectoryPath()), ex); + } + } + + /** + * Opens a case database. + * + * @param caseMetadata + * + * @return A case database. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - case_name", + "MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0}", + "# {0} - PostgreSQL server host", + "# {1} - PostgreSQL server port", + "# {2} - case database name", + "# {3} - case directory", + "MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3}" + }) + private SleuthkitCase openCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + CaseDbConnectionInfo dbConnectionInfo; + try { + dbConnectionInfo = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDatabaseConnectionInfo(aCase.getCaseMetadata().getCaseName()), ex); + } + checkForCancellation(); + final CaseMetadata caseMetadata = aCase.getCaseMetadata(); + try { + return SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); + } catch (UserPreferencesException | TskCoreException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorOpeningCaseDatabase(dbConnectionInfo.getHost(), dbConnectionInfo.getPort(), caseMetadata.getCaseDatabaseName(), caseMetadata.getCaseDirectory()), ex); + } + } + + /** + * Closes a case database. + * + * @param aCase a case database. + */ + private static void closeCase(SleuthkitCase aCase) { + aCase.close(); + } + + /** + * Executes a keyword search searchTerm in the text index of a case. + * + * @param solrServer The Solr server. + * @param solrQuery The Solr searchTerm. + * @param caseDatabase The case database. + * @param aCase The case metadata. + * + * @return A list of search results, possibly empty. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - query", + "# {1} - case_name", + "MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query \"{0}\" on case {1}" + }) + private Collection executeQuery(HttpSolrServer solrServer, SolrQuery solrQuery, SleuthkitCase caseDatabase, MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + final List hits = new ArrayList<>(); + final Set uniqueObjectIds = new HashSet<>(); + String cursorMark = CursorMarkParams.CURSOR_MARK_START; + boolean allResultsProcessed = false; + while (!allResultsProcessed) { + checkForCancellation(); + solrQuery.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark); + QueryResponse response; + try { + checkForCancellation(); + response = solrServer.query(solrQuery, SolrRequest.METHOD.POST); + } catch (SolrServerException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrQueryError(solrQuery.getQuery(), aCase.getCaseMetadata().getCaseName()), ex); + } + SolrDocumentList resultDocuments = response.getResults(); + for (SolrDocument resultDoc : resultDocuments) { + checkForCancellation(); + String solrDocumentId = resultDoc.getFieldValue(SOLR_DOC_ID_FIELD).toString(); + Long solrObjectId = parseSolrObjectId(solrDocumentId); + if (!uniqueObjectIds.contains(solrObjectId)) { + uniqueObjectIds.add(solrObjectId); + checkForCancellation(); + hits.add(processHit(solrObjectId, caseDatabase, aCase)); + } + } + checkForCancellation(); + String nextCursorMark = response.getNextCursorMark(); + if (cursorMark.equals(nextCursorMark)) { + allResultsProcessed = true; + } + cursorMark = nextCursorMark; + } + return hits; + } + + /** + * Parses a Solr document id to get the Solr object id. + * + * @param solrDocumentId A Solr document id. + * + * @return A Solr object id. + */ + private static Long parseSolrObjectId(String solrDocumentId) { + /** + * A Solr document id is of the form [solr_object_id] for Content object + * metadata documents and + * [solr_object_id][SOLR_DOC_ID_PARTS_SEPARATOR][chunk_id] for Content + * object text chunk documents. + */ + final String[] solrDocumentIdParts = solrDocumentId.split(SOLR_DOC_ID_PARTS_SEPARATOR); + if (1 == solrDocumentIdParts.length) { + return Long.parseLong(solrDocumentId); + } else { + return Long.parseLong(solrDocumentIdParts[0]); + } + } + + /** + * Creates a keyword search hit object for a Content object identified by + * its Solr object id. + * + * @param solrObjectId The Solr object id of a Content object. + * @param caseDatabase The case database of the case that has the + * Content. + * @param caseInfo Metadata about the case that has the content. + * + * @return + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - Solr document id", + "# {1} - case database name", + "# {2} - case directory", + "MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2}" + }) + + private static SearchHit processHit(Long solrObjectId, SleuthkitCase caseDatabase, MultiCaseMetadata caseInfo) throws MultiCaseSearcherException { + try { + final long objectId = getObjectIdForSolrObjectId(solrObjectId, caseDatabase); + final CaseMetadata caseMetadata = caseInfo.getCaseMetadata(); + final String caseDisplayName = caseMetadata.getCaseDisplayName(); + final String caseDirectoryPath = caseMetadata.getCaseDirectory(); + final Content content = caseDatabase.getContentById(objectId); + final Content dataSource = content.getDataSource(); + final String dataSourceName = dataSource.getName(); + SearchHit.SourceType sourceType = SearchHit.SourceType.FILE; + String sourceName = ""; + String sourcePath = ""; + if (content instanceof AbstractFile) { + AbstractFile sourceFile = (AbstractFile) content; + sourceName = sourceFile.getName(); + sourcePath = sourceFile.getLocalAbsPath(); + if (null == sourcePath) { + sourceType = SearchHit.SourceType.FILE; + sourcePath = sourceFile.getUniquePath(); + } else { + sourceType = SearchHit.SourceType.LOCAL_FILE; + sourceName = sourceFile.getName(); + } + } else if (content instanceof BlackboardArtifact) { + BlackboardArtifact sourceArtifact = (BlackboardArtifact) content; + sourceType = SearchHit.SourceType.ARTIFACT; + BlackboardArtifact.Type artifactType = caseDatabase.getArtifactType(sourceArtifact.getArtifactTypeName()); + sourceName = artifactType.getDisplayName(); + Content source = sourceArtifact.getParent(); + if (source instanceof AbstractFile) { + AbstractFile sourceFile = (AbstractFile) source; + sourcePath = sourceFile.getLocalAbsPath(); + if (null == sourcePath) { + sourcePath = sourceFile.getUniquePath(); + } + } else { + sourcePath = source.getUniquePath(); + } + } + return new SearchHit(caseDisplayName, caseDirectoryPath, dataSourceName, sourceType, sourceName, sourcePath); + } catch (SQLException | TskCoreException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_hitProcessingError(solrObjectId, caseInfo.getCaseMetadata().getCaseName(), caseInfo.getCaseMetadata().getCaseDirectory()), ex); + } + } + + /** + * Gets the Sleuthkit object id that corresponds to the Solr object id of + * some content. + * + * @param solrObjectId A solr object id for some content. + * @param caseDatabase The case database for the case that includes the + * content. + * + * @return The Sleuthkit object id of the content. + * + * @throws MultiCaseSearcherException + * @throws TskCoreException + * @throws SQLException + */ + private static long getObjectIdForSolrObjectId(long solrObjectId, SleuthkitCase caseDatabase) throws MultiCaseSearcherException, TskCoreException, SQLException { + if (0 < solrObjectId) { + return solrObjectId; + } else { + try (SleuthkitCase.CaseDbQuery databaseQuery = caseDatabase.executeQuery("SELECT artifact_obj_id FROM blackboard_artifacts WHERE artifact_id = " + solrObjectId)) { + final ResultSet resultSet = databaseQuery.getResultSet(); + if (resultSet.next()) { + return resultSet.getLong("artifact_obj_id"); + } else { + throw new TskCoreException("Empty result set getting obj_id for artifact with artifact_id =" + solrObjectId); + } + } + } + } + + /** + * Checks to see if the current thread has been interrupted (i.e, the search + * has been cancelled) and throws an InterruptedException if it has been. + * + * @throws InterruptedException + */ + private void checkForCancellation() throws InterruptedException { + if (Thread.currentThread().isInterrupted() || searchStopped) { + throw new InterruptedException("Search Cancelled"); + } + } + + /** + * A bundle of metadata for a case. + */ + private final static class MultiCaseMetadata { + + private final CaseMetadata caseMetadata; + private final TextIndexMetadata textIndexMetadata; + + /** + * Contructs a bundle of metadata for a case + * + * @param caseMetadata The case metadata. + * @param textIndexMetaData The text index metadata for the case. + */ + private MultiCaseMetadata(CaseMetadata caseMetadata, TextIndexMetadata textIndexMetaData) { + this.caseMetadata = caseMetadata; + this.textIndexMetadata = textIndexMetaData; + } + + /** + * Gets the case metadata. + * + * @return The case metadata. + */ + private CaseMetadata getCaseMetadata() { + return this.caseMetadata; + } + + /** + * Gets the text index metadata for the case. + * + * @return The text index metadata. + */ + private TextIndexMetadata getTextIndexMetadata() { + return this.textIndexMetadata; + } + + } + + /** + * Bundles a case directory path, a Solr core fileName, and a text index UNC + * path. + */ + private final static class TextIndexMetadata { + + private final Path caseDirectoryPath; + private final String solrCoreName; + private final String textIndexUNCPath; + + /** + * Constructs an object that bundles a Solr core fileName and a text + * index UNC path. + * + * @param caseDirectoryPath The case directory path. + * @param solrCoreName The core fileName. + * @param textIndexUNCPath The text index path. + */ + private TextIndexMetadata(Path caseDirectoryPath, String solrCoreName, String textIndexUNCPath) { + this.caseDirectoryPath = caseDirectoryPath; + this.solrCoreName = solrCoreName; + this.textIndexUNCPath = textIndexUNCPath; + } + + /** + * Gets the case directory path. + * + * @return The path. + */ + private Path getCaseDirectoryPath() { + return this.caseDirectoryPath; + } + + /** + * Gets the Solr core fileName. + * + * @return The Solr core fileName. + */ + private String getSolrCoreName() { + return this.solrCoreName; + } + + /** + * + * Gets the UNC path of the text index. + * + * @return The path. + */ + private String getTextIndexPath() { + return this.textIndexUNCPath; + } + + } + + /** + * Exception thrown if there is an error executing a search. + */ + static final class MultiCaseSearcherException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of the exception thrown if there is an error + * executing a search. + * + * @param message The exception message. + */ + private MultiCaseSearcherException(String message) { + super(message); + } + + /** + * Constructs an instance of the exception thrown if there is an error + * executing a search. + * + * @param message The exception message. + * @param cause The Throwable that caused the error. + */ + private MultiCaseSearcherException(String message, Throwable cause) { + super(message, cause); + } + + } + + /** + * Tell the MultiCaseSearcher that it's current search can be stopped the + * next time it checks for cancellation. + */ + void stopMultiCaseSearch() { + //This is necessary because if the interrupt occurs during CoreAdminRequest.process, + //CoreAdminRequest.getStatus, or HttpSolrServer.query the interrupt gets ignored + searchStopped = true; + } + + /** + * Register an object with the MultiCaseSearcher eventBus so that it's + * subscribe methods can receive results. + * + * @param object the object to register with the eventBus + */ + void registerWithEventBus(Object object) { + eventBus.register(object); + } + + /** + * Unregister an object with the MultiCaseSearcher eventBus so that it's + * subscribe methods no longer receive results. + * + * @param object the object to unregister with the eventBus + */ + void unregisterWithEventBus(Object object) { + eventBus.unregister(object); + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java new file mode 100755 index 0000000000..975d96094b --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java @@ -0,0 +1,137 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import javax.annotation.concurrent.Immutable; + +/** + * A keyword search hit from a multi-case keyword search. + */ +@Immutable +final class SearchHit { + + private final String caseDisplayName; + private final String caseDirectoryPath; + private final String dataSourceName; + private final SourceType sourceType; + private final String sourceName; + private final String sourcePath; + + /** + * Constructs a keyword search hit from a multi-case search. + * + * @param caseDisplayName The display name of the case where the hit occurred. + * @param caseDirectoryPath The path of the directory of the case where the hit occurred. + * @param dataSourceName The name of the data source within the case + * where the hit occurred. + * @param sourceType The type of the source content object. + * @param sourceName The name of the source, e.g., a file name, an + * artifact type name, or a report module name. + * @param sourcePath The path of the source content, or the path of + * the parent source content object for an artifact + * source. + */ + SearchHit(String caseDisplayName, String caseDirectoryPath, String dataSourceName, SourceType sourceType, String sourceName, String sourcePath) { + this.caseDisplayName = caseDisplayName; + this.caseDirectoryPath = caseDirectoryPath; + this.dataSourceName = dataSourceName; + this.sourceType = sourceType; + this.sourceName = sourceName; + this.sourcePath = sourcePath; + } + + /** + * Gets the display name of the case where the hit + * occurred. + * + * @return The case display name. + */ + String getCaseDisplayName() { + return this.caseDisplayName; + } + + /** + * Gets the path of the directory of the case where + * the hit occurred. + * + * @return The case directory path. + */ + String getCaseDirectoryPath() { + return this.caseDirectoryPath; + } + + /** + * Gets the name of the data source within the case where the hit occurred. + * + * @return + */ + String getDataSourceName() { + return this.dataSourceName; + } + + /** + * Gets the type of the source content object. + * + * @return The source type. + */ + SourceType getSourceType() { + return this.sourceType; + } + + /** + * Gets the name of the source, e.g., a file name, an artifact type name, or + * a report module name. + * + * @return The source name. + */ + String getSourceName() { + return this.sourceName; + } + + /** + * Gets the path of the source content, or the path of the parent source + * content object for an artifact source. + * + * @return The source object path. + */ + String getSourcePath() { + return this.sourcePath; + } + + /** + * An enumeration of the source types for keyword search hits. + */ + enum SourceType { + FILE("File"), + LOCAL_FILE("Local File"), + ARTIFACT("Artifact"), + REPORT("Report"); + + private final String displayName; + + private SourceType(String displayName) { + this.displayName = displayName; + } + + String getDisplayName() { + return this.displayName; + } + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java new file mode 100755 index 0000000000..89b224e06c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java @@ -0,0 +1,156 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.keywordsearch.Server; + +/** + * A keyword search query. + */ +@Immutable +final class SearchQuery { + + private static final String SEARCH_TERM_CHARS_TO_ESCAPE = "/+-&|!(){}[]^\"~*?:\\"; + private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS + private final String searchTerm; + + /** + * Constructs a multicase keyword search query. + * + * @param queryType The query type. + * @param searchTerm The search term for the query. + */ + SearchQuery(QueryType queryType, String searchTerm) { + switch (queryType) { + case EXACT_MATCH: + this.searchTerm = prepareExactMatchSearchTerm(searchTerm); + break; + case SUBSTRING: + this.searchTerm = prepareSubstringSearchTerm(searchTerm); + break; + case REGEX: + this.searchTerm = prepareRegexSearchTerm(searchTerm); + break; + default: + this.searchTerm = searchTerm; + break; + } + } + + /** + * Gets the search term. + * + * @return The query. + */ + String getSearchTerm() { + return searchTerm; + } + + /** + * Escapes and quotes a given search term as required for an exact match + * search query. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for an exact match query. + */ + private static String prepareExactMatchSearchTerm(String searchTerm) { + String escapedSearchTerm = escapeSearchTerm(searchTerm); + if (!searchTerm.startsWith("\"")) { + escapedSearchTerm = "\"" + escapedSearchTerm; + } + if (!searchTerm.endsWith("\"")) { + escapedSearchTerm += "\""; + } + return escapedSearchTerm; + } + + /** + * Adds delimiters and possibly wildcards to a given search terms as + * required for a regular expression search query. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for a regex query. + */ + private static String prepareRegexSearchTerm(String searchTerm) { + /* + * Add slash delimiters and, if necessary, wildcards (.*) at the + * beginning and end of the search term. The wildcards are added because + * Lucerne automatically adds a '^' prefix and '$' suffix to the search + * terms for regex searches. Without the '.*' wildcards, the search term + * will have to match the entire content_str field, which is not + * generally the intent of the user. + */ + String regexSearchTerm = SOLR_DOC_CONTENT_STR_FIELD + + ":/" + + (searchTerm.startsWith(".*") ? "" : ".*") + + searchTerm.toLowerCase() + + (searchTerm.endsWith(".*") ? "" : ".*") + + "/"; + return regexSearchTerm; + } + + /** + * Escapes and adds delimiters and wpossibly wildcards to a given search + * term as required for a substring search. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for a substring query. + */ + private static String prepareSubstringSearchTerm(String searchTerm) { + String escapedSearchTerm = escapeSearchTerm(searchTerm); + return prepareRegexSearchTerm(escapedSearchTerm); + } + + /** + * Escapes a search term as required for a Lucene query. + * + * @param searchTerm A "raw" input search term. + * + * @return An escaped version of the "raw" input search term. + */ + public static String escapeSearchTerm(String searchTerm) { + String rawSearchTerm = searchTerm.trim(); + if (0 == rawSearchTerm.length()) { + return rawSearchTerm; + } + StringBuilder escapedSearchTerm = new StringBuilder(rawSearchTerm.length()); + for (int i = 0; i < rawSearchTerm.length(); ++i) { + final char nextChar = rawSearchTerm.charAt(i); + if (SEARCH_TERM_CHARS_TO_ESCAPE.contains(Character.toString(nextChar))) { + escapedSearchTerm.append("\\"); + } + escapedSearchTerm.append(nextChar); + } + return escapedSearchTerm.toString(); + } + + /** + * An enumeration of the supported query types for keywod searches. + */ + enum QueryType { + EXACT_MATCH, + SUBSTRING, + REGEX; + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java new file mode 100755 index 0000000000..a4345e2f41 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrowserCustomizer; + +/** + * Customizer for SelectMultiUserCasesPanel. Displays the 'Create date' and 'Directory' columns + */ +public class SelectMultiUserCaseDialogCustomizer implements MultiUserCaseBrowserCustomizer { + + @Override + public List getColumns() { + List properties = new ArrayList<>(); + properties.add(Column.CREATE_DATE); + properties.add(Column.DIRECTORY); + return properties; } + + @Override + public List getSortColumns() { + List sortColumns = new ArrayList<>(); + sortColumns.add(new SortColumn(Column.CREATE_DATE, false, 1)); + return sortColumns; } + + @Override + public boolean allowMultiSelect() { + return true; + } + + @Override + public List getActions(CaseNodeData nodeData) { + return new ArrayList<>(); + } + + @Override + public Action getPreferredAction(CaseNodeData nodeData) { + return null; + } + + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java new file mode 100755 index 0000000000..0bcb40bb14 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java @@ -0,0 +1,90 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; +import java.awt.Dialog; +import java.awt.event.ActionListener; +import java.beans.PropertyVetoException; +import org.openide.nodes.Node; +import org.openide.windows.WindowManager; + +/** + * Dialog that will display the SelectMultiUserCasesPanel + */ +public class SelectMultiUserCasesDialog extends javax.swing.JDialog { + private static final long serialVersionUID = 1L; + private static SelectMultiUserCasesDialog instance; + private static SelectMultiUserCasesPanel multiUserCasesPanel; + + /** + * Gets the singleton JDialog that allows a user to open a multi-user case. + * + * @return The singleton JDialog instance. + */ + public synchronized static SelectMultiUserCasesDialog getInstance() { + if (instance == null) { + instance = new SelectMultiUserCasesDialog(); + instance.init(); + } + return instance; + } + + /** + * Listen for new case selections from the user. + * + * @param l Listener on new case selection events + */ + void subscribeToNewCaseSelections(ActionListener l) { + multiUserCasesPanel.subscribeToNewCaseSelections(l); + } + + /** + * Set the node selections for the window + * + * @param selections Nodes to be automatically selected in the explorer view + */ + void setNodeSelections(Node[] selections) { + try { + multiUserCasesPanel.setSelections(selections); + } catch (PropertyVetoException ex) { + + } + } + + /** + * Constructs a singleton JDialog that allows a user to open a multi-user + * case. + */ + private SelectMultiUserCasesDialog() { + super(WindowManager.getDefault().getMainWindow(), "Select Multi-User Cases", 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() { + multiUserCasesPanel = new SelectMultiUserCasesPanel(this); + add(multiUserCasesPanel); + pack(); + setResizable(false); + multiUserCasesPanel.refreshDisplay(); + } +} \ No newline at end of file diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form new file mode 100755 index 0000000000..161898a806 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form @@ -0,0 +1,110 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java new file mode 100755 index 0000000000..14c419e927 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java @@ -0,0 +1,206 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 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.keywordsearch.multicase; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyVetoException; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JDialog; +import org.openide.explorer.ExplorerManager; +import org.openide.nodes.Node; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCasesBrowserPanel; + +/** + * Panel for multi-user case selection + */ +public class SelectMultiUserCasesPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private final JDialog parentDialog; + private final MultiUserCasesBrowserPanel caseBrowserPanel; + private final List listeners; + + /** + * 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. + */ + SelectMultiUserCasesPanel(JDialog parentDialog) { + initComponents(); + this.parentDialog = parentDialog; + initComponents(); // Machine generated code + caseBrowserPanel = new MultiUserCasesBrowserPanel(new ExplorerManager(), new SelectMultiUserCaseDialogCustomizer()); + multiUserCaseScrollPane.add(caseBrowserPanel); + multiUserCaseScrollPane.setViewportView(caseBrowserPanel); + listeners = new ArrayList<>(); + } + + /** + * Refreshes the child component that displays the multi-user cases known to + * the coordination service.. + */ + void refreshDisplay() { + caseBrowserPanel.displayCases(); + } + + /** + * Subscribes to the selections when the user presses the OK button. + * + * @param listener + */ + void subscribeToNewCaseSelections(ActionListener listener) { + listeners.add(listener); + } + + /** + * Sets the selections in the panel + * + * @param selections + * @throws PropertyVetoException + */ + void setSelections(Node[] selections) throws PropertyVetoException { + caseBrowserPanel.getExplorerManager().setSelectedNodes(selections); + caseBrowserPanel.requestFocus(); + } + + /** + * 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() { + + multiUserCaseScrollPane = new javax.swing.JScrollPane(); + selectAllButton = new javax.swing.JButton(); + deselectAllButton = new javax.swing.JButton(); + jLabel1 = new javax.swing.JLabel(); + confirmSelections = new javax.swing.JButton(); + cancelButton = new javax.swing.JButton(); + + org.openide.awt.Mnemonics.setLocalizedText(selectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.selectAllButton.text")); // NOI18N + selectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(deselectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.deselectAllButton.text")); // NOI18N + deselectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deselectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.jLabel1.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(confirmSelections, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.confirmSelections.text")); // NOI18N + confirmSelections.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + confirmSelectionsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.cancelButton.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(multiUserCaseScrollPane) + .addGroup(layout.createSequentialGroup() + .addComponent(selectAllButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deselectAllButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 365, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 367, Short.MAX_VALUE) + .addComponent(confirmSelections, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(multiUserCaseScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 486, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel1) + .addComponent(selectAllButton) + .addComponent(deselectAllButton) + .addComponent(confirmSelections) + .addComponent(cancelButton)) + .addContainerGap(15, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + private void selectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectAllButtonActionPerformed + try { + caseBrowserPanel.getExplorerManager().setSelectedNodes(caseBrowserPanel.getExplorerManager().getRootContext().getChildren().getNodes()); + } catch (PropertyVetoException ex) { + Exceptions.printStackTrace(ex); + } + }//GEN-LAST:event_selectAllButtonActionPerformed + + private void deselectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deselectAllButtonActionPerformed + try { + caseBrowserPanel.getExplorerManager().setSelectedNodes(new Node[0]); + } catch (PropertyVetoException ex) { + Exceptions.printStackTrace(ex); + } + }//GEN-LAST:event_deselectAllButtonActionPerformed + + private void confirmSelectionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_confirmSelectionsActionPerformed + listeners.forEach((l) -> { + //Pass along the selected nodes in the event. + l.actionPerformed(new ActionEvent(caseBrowserPanel.getExplorerManager().getSelectedNodes(), -1, "")); + }); + parentDialog.setVisible(false); + }//GEN-LAST:event_confirmSelectionsActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + parentDialog.setVisible(false); + }//GEN-LAST:event_cancelButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JButton confirmSelections; + private javax.swing.JButton deselectAllButton; + private javax.swing.JLabel jLabel1; + private javax.swing.JScrollPane multiUserCaseScrollPane; + private javax.swing.JButton selectAllButton; + // End of variables declaration//GEN-END:variables +}