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
+}