contentHighlights = responseHighlightID.get("content");
+ if (contentHighlights == null) {
+ return NO_MATCHES;
+ } else {
+ // extracted content (minus highlight tags) is HTML-escaped
+ String highlightedContent = contentHighlights.get(0).trim();
+ highlightedContent = insertAnchors(highlightedContent);
+ return "" + highlightedContent + "
";
+ }
+ } catch (SolrServerException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Search Matches";
+ }
+
+ @Override
+ public boolean isSearchable() {
+ return true;
+ }
+
+ @Override
+ public String getAnchorPrefix() {
+ return ANCHOR_PREFIX;
+ }
+
+ @Override
+ public int getNumberHits() {
+ return numberHits;
+ }
+
+ private String insertAnchors(String searchableContent) {
+ int searchOffset = 0;
+ int index = -1;
+
+ StringBuilder buf = new StringBuilder(searchableContent);
+
+ final String searchToken = HIGHLIGHT_PRE;
+ final int indexSearchTokLen = searchToken.length();
+ final String insertPre = "";
+ int count = 0;
+ while ((index = buf.indexOf(searchToken, searchOffset)) >= 0) {
+ String insertString = insertPre + Integer.toString(count) + insertPost;
+ int insertStringLen = insertString.length();
+ buf.insert(index, insertString);
+ searchOffset = index + indexSearchTokLen + insertStringLen; //next offset past this anchor
+ ++count;
+ }
+
+ this.numberHits = count;
+ return buf.toString();
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexContentFilesAction.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexContentFilesAction.java
new file mode 100755
index 0000000000..daab674705
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexContentFilesAction.java
@@ -0,0 +1,307 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.AbstractAction;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.openide.util.lookup.ServiceProvider;
+import org.sleuthkit.autopsy.casemodule.AddImageAction;
+import org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.FsContent;
+import org.sleuthkit.datamodel.Image;
+import org.sleuthkit.datamodel.TskException;
+
+/**
+ * Action adds all supported files from the given Content object and its
+ * children to the Solr index.
+ */
+public class IndexContentFilesAction extends AbstractAction {
+
+ private static final Logger logger = Logger.getLogger(IndexContentFilesAction.class.getName());
+ private static final int MAX_STRING_EXTRACT_SIZE = 10 * (1 << 10) * (1 << 10);
+ private Content c;
+ private String name;
+ private Server.Core solrCore;
+
+ public enum IngestStatus {
+
+ NOT_INGESTED, INGESTED, EXTRACTED_INGESTED, SKIPPED_EXTRACTION,};
+ //keep track of ingest status for various types of content
+ //could also be useful for reporting
+ private Map ingestStatus;
+ private int problemFilesCount;
+
+ /**
+ * New action
+ * @param c source Content object to get files from
+ * @param name name to refer to the source by when displaying progress
+ */
+ public IndexContentFilesAction(Content c, String name) {
+ this(c, name, KeywordSearch.getServer().getCore());
+ }
+
+ IndexContentFilesAction(Content c, String name, Server.Core solrCore) {
+ super("Index files...");
+ this.c = c;
+ this.name = name;
+ this.solrCore = solrCore;
+ ingestStatus = new HashMap();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+
+ // create the popUp window to display progress
+ String title = "Indexing files in " + name;
+
+ final JFrame frame = new JFrame(title);
+ final JDialog popUpWindow = new JDialog(frame, title, true); // to make the popUp Window modal
+
+ // initialize panel
+ final IndexProgressPanel panel = new IndexProgressPanel();
+
+ final SwingWorker task = new SwingWorker() {
+
+ @Override
+ protected Integer doInBackground() throws Exception {
+ Ingester ingester = solrCore.getIngester();
+
+ this.publish("Categorizing files to index. ");
+
+ GetFilesContentVisitor ingestableV = new GetIngestableFilesContentVisitor();
+ GetFilesContentVisitor allV = new GetAllFilesContentVisitor();
+
+ Collection ingestableFiles = c.accept(ingestableV);
+ Collection allFiles = c.accept(allV);
+
+ //calculate non ingestable Collection (complement of allFiles / ingestableFiles
+ //TODO implement a facility that selects different categories of FsContent
+ Collection nonIngestibleFiles = new LinkedHashSet();
+
+ for (FsContent fs : allFiles) {
+ if (! ingestableFiles.contains(fs) ) {
+ nonIngestibleFiles.add(fs);
+ }
+ }
+
+ // track number complete or with errors
+ problemFilesCount = 0;
+ ingestStatus.clear();
+
+ //work on known files first
+ Collection ingestFailedFiles = processIngestible(ingester, ingestableFiles);
+ nonIngestibleFiles.addAll(ingestFailedFiles);
+
+ //work on unknown files
+ //TODO should be an option somewhere in GUI (known vs unknown files)
+ processNonIngestible(ingester, nonIngestibleFiles);
+
+ ingester.commit();
+
+ //signal a potential change in number of indexed files
+ try {
+ final int numIndexedFiles = KeywordSearch.getServer().getCore().queryNumIndexedFiles();
+ KeywordSearch.changeSupport.firePropertyChange(KeywordSearch.NUM_FILES_CHANGE_EVT, null, new Integer(numIndexedFiles));
+ } catch (SolrServerException se) {
+ logger.log(Level.SEVERE, "Error executing Solr query to check number of indexed files: ", se);
+ }
+
+ return problemFilesCount;
+ }
+
+ private Collection processIngestible(Ingester ingester, Collection fscc) {
+ Collection ingestFailedCol = new ArrayList();
+
+ setProgress(0);
+ int finishedFiles = 0;
+ final int totalFilesCount = fscc.size();
+ for (FsContent f : fscc) {
+ if (isCancelled()) {
+ return ingestFailedCol;
+ }
+ this.publish("Indexing " + (finishedFiles + 1) + "/" + totalFilesCount + ": " + f.getName());
+ try {
+ ingester.ingest(f);
+ ingestStatus.put(f.getId(), IngestStatus.INGESTED);
+ } catch (IngesterException ex) {
+ ingestFailedCol.add(f);
+ ingestStatus.put(f.getId(), IngestStatus.NOT_INGESTED);
+ logger.log(Level.INFO, "Ingester failed with file '" + f.getName() + "' (id: " + f.getId() + ").", ex);
+ }
+ setProgress(++finishedFiles * 100 / totalFilesCount);
+ }
+ return ingestFailedCol;
+ }
+
+ private void processNonIngestible(Ingester ingester, Collection fscc) {
+ setProgress(0);
+ int finishedFiles = 0;
+ final int totalFilesCount = fscc.size();
+
+ for (FsContent f : fscc) {
+ if (isCancelled()) {
+ return;
+ }
+ this.publish("String extracting/Indexing " + (finishedFiles + 1) + "/" + totalFilesCount + ": " + f.getName());
+
+ if (f.getSize() < MAX_STRING_EXTRACT_SIZE) {
+ if (!extractAndIngest(ingester, f)) {
+ ingestStatus.put(f.getId(), IngestStatus.NOT_INGESTED);
+ problemFilesCount++;
+ logger.log(Level.INFO, "Failed to extract strings and ingest, file '" + f.getName() + "' (id: " + f.getId() + ").");
+ } else {
+ ingestStatus.put(f.getId(), IngestStatus.EXTRACTED_INGESTED);
+ }
+ } else {
+ ingestStatus.put(f.getId(), IngestStatus.SKIPPED_EXTRACTION);
+ }
+
+ setProgress(++finishedFiles * 100 / totalFilesCount);
+ }
+ }
+
+ @Override
+ protected void done() {
+ int problemFiles = 0;
+
+ try {
+ if (!this.isCancelled()) {
+ problemFiles = get();
+ }
+
+ } catch (InterruptedException ex) {
+ // shouldn't be interrupted except by cancel
+ throw new RuntimeException(ex);
+ } catch (ExecutionException ex) {
+ logger.log(Level.SEVERE, "Fatal error during ingest.", ex);
+ } finally {
+ popUpWindow.setVisible(false);
+ popUpWindow.dispose();
+
+ // notify user if there were problem files
+ if (problemFiles > 0) {
+ displayProblemFilesDialog(problemFiles);
+ }
+ }
+ }
+
+ @Override
+ protected void process(List messages) {
+
+ // display the latest message
+ if (!messages.isEmpty()) {
+ panel.setStatusText(messages.get(messages.size() - 1));
+ }
+
+ panel.setProgressBar(getProgress());
+ }
+ };
+
+ panel.addCancelButtonActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ task.cancel(true);
+ }
+ });
+
+ popUpWindow.add(panel);
+ popUpWindow.pack();
+ popUpWindow.setResizable(false);
+
+ // set the location of the popUp Window on the center of the screen
+ Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
+ double w = popUpWindow.getSize().getWidth();
+ double h = popUpWindow.getSize().getHeight();
+ popUpWindow.setLocation((int) ((screenDimension.getWidth() - w) / 2), (int) ((screenDimension.getHeight() - h) / 2));
+
+ popUpWindow.addWindowListener(new WindowAdapter() {
+
+ @Override
+ public void windowClosing(WindowEvent e) {
+ // deal with being Xed out of
+ if (!task.isDone()) {
+ task.cancel(true);
+ }
+ }
+ });
+
+
+ task.execute();
+ // display the window
+ popUpWindow.setVisible(true);
+ }
+
+ private boolean extractAndIngest(Ingester ingester, FsContent f) {
+ boolean success = false;
+ FsContentStringStream fscs = new FsContentStringStream(f, FsContentStringStream.Encoding.ASCII);
+ try {
+ fscs.convert();
+ ingester.ingest(fscs);
+ success = true;
+ } catch (TskException tskEx) {
+ logger.log(Level.INFO, "Problem extracting string from file: '" + f.getName() + "' (id: " + f.getId() + ").", tskEx);
+ } catch (IngesterException ingEx) {
+ logger.log(Level.INFO, "Ingester had a problem with extracted strings from file '" + f.getName() + "' (id: " + f.getId() + ").", ingEx);
+ }
+ return success;
+ }
+
+ private void displayProblemFilesDialog(int problemFiles) {
+ final Component parentComponent = null; // Use default window frame.
+ final String message = "Had trouble indexing " + problemFiles + " of the files. See the log for details.";
+ final String title = "Problem indexing some files";
+ final int messageType = JOptionPane.WARNING_MESSAGE;
+ JOptionPane.showMessageDialog(
+ parentComponent,
+ message,
+ title,
+ messageType);
+ }
+
+ @ServiceProvider(service = AddImageAction.IndexImageTask.class)
+ public static class IndexImageTask implements AddImageAction.IndexImageTask {
+
+ @Override
+ public void runTask(Image newImage) {
+ (new IndexContentFilesAction(newImage, "new image")).actionPerformed(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.form
new file mode 100644
index 0000000000..bed4cafbe3
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.form
@@ -0,0 +1,62 @@
+
+
+
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.java
new file mode 100644
index 0000000000..6150d17d3d
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexProgressPanel.java
@@ -0,0 +1,100 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.event.ActionListener;
+
+/**
+ * Displays progress as files are indexed
+ */
+class IndexProgressPanel extends javax.swing.JPanel {
+
+ /** Creates new form IndexProgressPanel */
+ IndexProgressPanel() {
+ initComponents();
+ progressBar.setMinimum(0);
+ progressBar.setMaximum(100);
+ progressBar.setIndeterminate(true);
+ statusText.setText("Starting...");
+ }
+
+ /** 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() {
+
+ progressBar = new javax.swing.JProgressBar();
+ statusText = new javax.swing.JLabel();
+ cancelButton = new javax.swing.JButton();
+
+ statusText.setText(org.openide.util.NbBundle.getMessage(IndexProgressPanel.class, "IndexProgressPanel.statusText.text")); // NOI18N
+
+ cancelButton.setText(org.openide.util.NbBundle.getMessage(IndexProgressPanel.class, "IndexProgressPanel.cancelButton.text")); // NOI18N
+
+ 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(statusText)
+ .addComponent(progressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 420, Short.MAX_VALUE)
+ .addComponent(cancelButton, javax.swing.GroupLayout.Alignment.TRAILING))
+ .addContainerGap())
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addComponent(statusText)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(progressBar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(cancelButton)
+ .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+ );
+ }// //GEN-END:initComponents
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton cancelButton;
+ private javax.swing.JProgressBar progressBar;
+ private javax.swing.JLabel statusText;
+ // End of variables declaration//GEN-END:variables
+
+ /**
+ * Sets a listener for the Cancel button
+ * @param e The action listener
+ */
+ void addCancelButtonActionListener(ActionListener e) {
+ this.cancelButton.addActionListener(e);
+ }
+
+ void setProgressBar(int percent) {
+ progressBar.setIndeterminate(false);
+ progressBar.setValue(percent);
+ }
+
+ void setStatusText(String text) {
+ statusText.setText(text);
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
new file mode 100755
index 0000000000..0ade621189
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
@@ -0,0 +1,226 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import org.apache.solr.client.solrj.SolrServer;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.util.ContentStream;
+import org.sleuthkit.datamodel.FsContent;
+
+/**
+ * Handles indexing files on a Solr core.
+ */
+class Ingester {
+
+ private static final Logger logger = Logger.getLogger(Ingester.class.getName());
+ private SolrServer solrCore;
+ private boolean uncommitedIngests = false;
+
+ Ingester(SolrServer solrCore) {
+ this.solrCore = solrCore;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+
+ // Warn if files might have been left uncommited.
+ if (uncommitedIngests) {
+ logger.warning("Ingester was used to add files that it never committed.");
+ }
+ }
+
+
+ /**
+ * Sends a file to Solr to have its content extracted and added to the
+ * index. commit() should be called once you're done ingesting files.
+ *
+ * @param fcs File FsContentStringStream to ingest
+ * @throws IngesterException if there was an error processing a specific
+ * file, but the Solr server is probably fine.
+ */
+ public void ingest(FsContentStringStream fcs) throws IngesterException {
+ ingest(fcs, getFsContentFields(fcs.getFsContent()));
+ }
+
+ /**
+ * Sends a file to Solr to have its content extracted and added to the
+ * index. commit() should be called once you're done ingesting files.
+ *
+ * @param f File to ingest
+ * @throws IngesterException if there was an error processing a specific
+ * file, but the Solr server is probably fine.
+ */
+ public void ingest(FsContent f) throws IngesterException {
+ ingest(new FscContentStream(f), getFsContentFields(f));
+ }
+
+ /**
+ * Creates a field map from FsContent, that is later sent to Solr
+ * @param fsc FsContent to get fields from
+ * @return the map
+ */
+ private Map getFsContentFields(FsContent fsc) {
+ Map fields = new HashMap();
+ fields.put("id", Long.toString(fsc.getId()));
+ fields.put("file_name", fsc.getName());
+ fields.put("ctime", fsc.getCtimeAsDate());
+ fields.put("atime", fsc.getAtimeAsDate());
+ fields.put("mtime", fsc.getMtimeAsDate());
+ fields.put("crtime", fsc.getMtimeAsDate());
+ return fields;
+ }
+
+
+ /**
+ * Common delegate method actually doing the work for objects implementing ContentStream
+ *
+ * @param ContentStream to ingest
+ * @param fields content specific fields
+ * @throws IngesterException if there was an error processing a specific
+ * content, but the Solr server is probably fine.
+ */
+ private void ingest(ContentStream cs, Map fields) throws IngesterException {
+ ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update/extract");
+ up.addContentStream(cs);
+ setFields(up, fields);
+ up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+
+ logger.log(Level.INFO, "Ingesting " + fields.get("file_name"));
+ up.setParam("commit", "false");
+
+ try {
+ solrCore.request(up);
+ // should't get any checked exceptions,
+ } catch (IOException ex) {
+ // It's possible that we will have IO errors
+ throw new IngesterException("Problem reading file.", ex);
+ } catch (SolrServerException ex) {
+ // If there's a problem talking to Solr, something is fundamentally
+ // wrong with ingest
+ throw new RuntimeException(ex);
+ } catch (SolrException ex) {
+ // Tika problems result in an unchecked SolrException
+ ErrorCode ec = ErrorCode.getErrorCode(ex.code());
+
+ // When Tika has problems with a document, it throws a server error
+ // but it's okay to continue with other documents
+ if (ec.equals(ErrorCode.SERVER_ERROR)) {
+ throw new IngesterException("Problem posting file contents to Solr. SolrException error code: " + ec, ex);
+ } else {
+ // shouldn't get any other error codes
+ throw ex;
+ }
+ }
+
+ uncommitedIngests = true;
+ }
+
+ /**
+ * Tells Solr to commit (necessary before ingested files will appear in
+ * searches)
+ */
+ void commit() {
+ try {
+ solrCore.commit();
+ uncommitedIngests = false;
+ // if commit doesn't work, something's broken
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ } catch (SolrServerException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ /**
+ * Helper to set document fields
+ * @param up request with document
+ * @param fields map of field-names->values
+ */
+ private static void setFields(ContentStreamUpdateRequest up, Map fields) {
+ for (Entry field : fields.entrySet()) {
+ up.setParam("literal." + field.getKey(), field.getValue());
+ }
+ }
+
+ /**
+ * ContentStream to read() the data from a FsContent object
+ */
+ private static class FscContentStream implements ContentStream {
+
+ FsContent f;
+
+ FscContentStream(FsContent f) {
+ this.f = f;
+ }
+
+ @Override
+ public String getName() {
+ return f.getName();
+ }
+
+ @Override
+ public String getSourceInfo() {
+ return "File:" + f.getId();
+ }
+
+ @Override
+ public String getContentType() {
+ return null;
+ }
+
+ @Override
+ public Long getSize() {
+ return f.getSize();
+ }
+
+ @Override
+ public InputStream getStream() throws IOException {
+ return new ReadContentInputStream(f);
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+ }
+
+ /**
+ * Indicates that there was an error with the specific ingest operation,
+ * but it's still okay to continue ingesting files.
+ */
+ static class IngesterException extends Exception {
+
+ IngesterException(String message, Throwable ex) {
+ super(message, ex);
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java
new file mode 100755
index 0000000000..61f968fc07
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java
@@ -0,0 +1,71 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.openide.modules.ModuleInstall;
+import org.sleuthkit.autopsy.casemodule.Case;
+
+/**
+ * Starts up the Solr server when the module is loaded, and stops it when the
+ * application is closed.
+ */
+public class Installer extends ModuleInstall {
+
+ @Override
+ public void restored() {
+
+ Logger logger = Logger.getLogger(Installer.class.getName());
+
+ Case.addPropertyChangeListener(new KeywordSearch.CaseChangeListener());
+
+ Server server = KeywordSearch.getServer();
+
+ if (server.isRunning()) {
+
+ logger.log(Level.WARNING, "Already a Solr server running, maybe leftover from a previous run. Trying to shut it down...");
+
+ // Send the stop message in case there's a solr server lingering from
+ // a previous run of Autopsy that didn't exit cleanly
+ server.stop();
+
+ if (server.isRunning()) {
+ throw new IllegalStateException("There's already a server running on our port that can't be shutdown.");
+ } else {
+ logger.log(Level.INFO, "Old Solr server shutdown successfully.");
+ }
+ }
+
+ server.start();
+ try {
+ Thread.sleep(1000); // give it a sec
+ //TODO: idle loop while waiting for it to start
+ } catch (InterruptedException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ }
+
+ @Override
+ public boolean closing() {
+ KeywordSearch.getServer().stop();
+ return true;
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java
new file mode 100644
index 0000000000..d28765132e
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java
@@ -0,0 +1,75 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import org.sleuthkit.autopsy.casemodule.Case;
+
+/**
+ * Static class to track singletons for KeywordSearch module
+ */
+class KeywordSearch {
+
+ private static final String BASE_URL = "http://localhost:8983/solr/";
+ private static final Server SERVER = new Server(BASE_URL);
+
+ public enum QueryType {WORD, REGEX};
+
+ public static final String NUM_FILES_CHANGE_EVT = "NUM_FILES_CHANGE_EVT";
+
+ static PropertyChangeSupport changeSupport = new PropertyChangeSupport(KeywordSearch.class);
+
+
+ static Server getServer() {
+ return SERVER;
+ }
+
+ // don't instantiate
+ private KeywordSearch() {
+ throw new AssertionError();
+ }
+
+ /**
+ * Listener to swap cores when the case changes
+ */
+ static class CaseChangeListener implements PropertyChangeListener {
+
+ CaseChangeListener() {
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ String changed = evt.getPropertyName();
+ Object oldValue = evt.getOldValue();
+ Object newValue = evt.getNewValue();
+
+ if (changed.equals(Case.CASE_CURRENT_CASE)) {
+ if (newValue != null) {
+ // new case is open
+ SERVER.openCore();
+ } else if (oldValue != null) {
+ // a case was closed
+ SERVER.closeCore();
+ }
+ }
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchDataExplorer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchDataExplorer.java
new file mode 100644
index 0000000000..455b3f574b
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchDataExplorer.java
@@ -0,0 +1,125 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.Cursor;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import org.openide.util.lookup.ServiceProvider;
+import org.sleuthkit.autopsy.corecomponentinterfaces.DataExplorer;
+import org.sleuthkit.autopsy.keywordsearch.KeywordSearch.QueryType;
+import org.sleuthkit.autopsy.keywordsearch.KeywordSearchQueryManager.Presentation;
+
+/**
+ * Provides a data explorer to perform Solr searches with
+ */
+@ServiceProvider(service = DataExplorer.class, position = 300)
+public class KeywordSearchDataExplorer implements DataExplorer {
+
+ private static KeywordSearchDataExplorer theInstance;
+ private KeywordSearchTabsTopComponent tc;
+
+ public KeywordSearchDataExplorer() {
+ this.setTheInstance();
+ this.tc = new KeywordSearchTabsTopComponent();
+
+ this.tc.addSearchButtonListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ tc.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+
+ try {
+ search();
+ } finally {
+ tc.setCursor(null);
+ }
+ }
+ });
+
+ KeywordSearch.changeSupport.addPropertyChangeListener(KeywordSearch.NUM_FILES_CHANGE_EVT, new IndexChangeListener());
+ }
+
+ private synchronized void setTheInstance() {
+ if (theInstance == null) {
+ theInstance = this;
+ } else {
+ throw new RuntimeException("Tried to instantiate mulitple instances of KeywordSearchTopComponent.");
+ }
+ }
+
+ /**
+ * Executes a query and populates a DataResult tab with the results
+ * @param solrQuery
+ */
+ private void search() {
+ KeywordSearchQueryManager man = null;
+ if (tc.isMultiwordQuery()) {
+ man = new KeywordSearchQueryManager(tc.getQueryList(), Presentation.COLLAPSE);
+ } else {
+ QueryType queryType = null;
+ if (tc.isLuceneQuerySelected()) {
+ queryType = QueryType.WORD;
+ } else {
+ queryType = QueryType.REGEX;
+ }
+ man = new KeywordSearchQueryManager(tc.getQueryText(), queryType, Presentation.COLLAPSE);
+ }
+
+ if (man.validate()) {
+ man.execute();
+ } else {
+ KeywordSearchUtil.displayDialog("Keyword Search Error", "Invalid query syntax.", KeywordSearchUtil.DIALOG_MESSAGE_TYPE.ERROR);
+ }
+
+ }
+
+ @Override
+ public org.openide.windows.TopComponent getTopComponent() {
+ return this.tc;
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ }
+
+ class IndexChangeListener implements PropertyChangeListener {
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+
+ String changed = evt.getPropertyName();
+ //Object oldValue = evt.getOldValue();
+ Object newValue = evt.getNewValue();
+
+ if (newValue != null) {
+ if (changed.equals(KeywordSearch.NUM_FILES_CHANGE_EVT)) {
+ int newFilesIndexed = ((Integer) newValue).intValue();
+ tc.setFilesIndexed(newFilesIndexed);
+
+ } else {
+ String msg = "Unsupported change event: " + changed;
+ throw new UnsupportedOperationException(msg);
+ }
+ }
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java
new file mode 100644
index 0000000000..0d1932a9c2
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java
@@ -0,0 +1,130 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.openide.nodes.FilterNode;
+import org.openide.nodes.Node;
+import org.openide.nodes.Node.Property;
+import org.openide.nodes.PropertySupport;
+import org.openide.nodes.Sheet;
+import org.openide.util.lookup.Lookups;
+import org.openide.util.lookup.ProxyLookup;
+import org.sleuthkit.autopsy.keywordsearch.Server.Core;
+import org.sleuthkit.datamodel.Content;
+
+/**
+ * Filter Node to add a "Snippet" property containing the first snippet of
+ * content matching the search that the Node was found with, and to provide
+ * the full highlighted content as a MarkupSource
+ */
+class KeywordSearchFilterNode extends FilterNode {
+
+ private static final int SNIPPET_LENGTH = 45;
+ String solrQuery;
+
+ KeywordSearchFilterNode(HighlightedMatchesSource highlights, Node original, String solrQuery) {
+ super(original, null, new ProxyLookup(Lookups.singleton(highlights), original.getLookup()));
+ this.solrQuery = solrQuery;
+ }
+
+ String getSnippet() {
+ Core solrCore = KeywordSearch.getServer().getCore();
+
+ Content content = this.getOriginal().getLookup().lookup(Content.class);
+
+ SolrQuery q = new SolrQuery();
+ q.setQuery(solrQuery);
+ q.addFilterQuery("id:" + content.getId());
+ q.addHighlightField("content");
+ q.setHighlightSimplePre("«");
+ q.setHighlightSimplePost("»");
+ q.setHighlightSnippets(1);
+ q.setHighlightFragsize(SNIPPET_LENGTH);
+
+ try {
+ QueryResponse response = solrCore.query(q);
+ Map>>responseHighlight = response.getHighlighting();
+ long contentID = content.getId();
+ Map>responseHighlightID = responseHighlight.get(Long.toString(contentID));
+ if (responseHighlightID == null)
+ return "";
+ List contentHighlights = responseHighlightID.get("content");
+ if (contentHighlights == null) {
+ return "";
+ } else {
+ // extracted content is HTML-escaped, but snippet goes in a plain text field
+ return StringEscapeUtils.unescapeHtml(contentHighlights.get(0)).trim();
+ }
+ } catch (SolrServerException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ Property getSnippetProperty() {
+
+ Property prop = new PropertySupport.ReadOnly("snippet",
+ String.class, "Context", "Snippet of matching content.") {
+
+ @Override
+ public Object getValue() {
+ return getSnippet();
+ }
+ };
+
+ prop.setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button
+
+ return prop;
+ }
+
+ @Override
+ public Node.PropertySet[] getPropertySets() {
+ Node.PropertySet[] propertySets = super.getPropertySets();
+
+ for (int i = 0; i < propertySets.length; i++) {
+ Node.PropertySet ps = propertySets[i];
+
+ if (ps.getName().equals(Sheet.PROPERTIES)) {
+ Sheet.Set newPs = new Sheet.Set();
+ newPs.setName(ps.getName());
+ newPs.setDisplayName(ps.getDisplayName());
+ newPs.setShortDescription(ps.getShortDescription());
+
+ Property[] oldProperties = ps.getProperties();
+
+ int j = 0;
+ for (Property p : oldProperties) {
+ if (j++ == 1) {
+ newPs.put(getSnippetProperty());
+ }
+ newPs.put(p);
+ }
+
+ propertySets[i] = newPs;
+ }
+ }
+
+ return propertySets;
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.form
new file mode 100644
index 0000000000..a952affca6
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.form
@@ -0,0 +1,222 @@
+
+
+
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.java
new file mode 100644
index 0000000000..477f56b6d8
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListTopComponent.java
@@ -0,0 +1,677 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.Component;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.swing.JOptionPane;
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableColumn;
+import org.openide.util.NbBundle;
+import org.openide.windows.TopComponent;
+import org.netbeans.api.settings.ConvertAsProperties;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+
+/**
+ * Top component which displays something.
+ */
+@ConvertAsProperties(dtd = "-//org.sleuthkit.autopsy.keywordsearch//KeywordSearchList//EN",
+autostore = false)
+@TopComponent.Description(preferredID = "KeywordSearchListTopComponent",
+//iconBase="SET/PATH/TO/ICON/HERE",
+persistenceType = TopComponent.PERSISTENCE_NEVER)
+@TopComponent.Registration(mode = "output", openAtStartup = false)
+@ActionID(category = "Window", id = "org.sleuthkit.autopsy.keywordsearch.KeywordSearchListTopComponent")
+@ActionReference(path = "Menu/Window" /*, position = 333 */)
+@TopComponent.OpenActionRegistration(displayName = "#CTL_KeywordSearchListAction",
+preferredID = "KeywordSearchListTopComponent")
+public final class KeywordSearchListTopComponent extends TopComponent implements KeywordSearchTopComponentInterface {
+
+ private static Logger logger = Logger.getLogger(KeywordSearchListTopComponent.class.getName());
+ private KeywordTableModel tableModel;
+ private String currentKeywordList;
+
+ public KeywordSearchListTopComponent() {
+ tableModel = new KeywordTableModel();
+ initComponents();
+ customizeComponents();
+ setName(NbBundle.getMessage(KeywordSearchListTopComponent.class, "CTL_KeywordSearchListTopComponent"));
+ setToolTipText(NbBundle.getMessage(KeywordSearchListTopComponent.class, "HINT_KeywordSearchListTopComponent"));
+
+ }
+
+ private void customizeComponents() {
+ chLiteralWord.setToolTipText("Literal word (auto-escape special characters)");
+ addWordButton.setToolTipText(("Add a new word to the keyword search list"));
+ addWordField.setToolTipText("Enter a new word or regex to search");
+
+ loadListButton.setToolTipText("Load a new keyword list from file");
+ saveListButton.setToolTipText("Save the current keyword list to a file");
+ searchButton.setToolTipText("Execute the keyword list search using the current list");
+ deleteWordButton.setToolTipText("Delete selected keyword(s) from the list");
+ deleteAllWordsButton.setToolTipText("Delete all keywords from the list (clear it)");
+
+ keywordTable.setAutoscrolls(true);
+ keywordTable.setTableHeader(null);
+ keywordTable.setShowHorizontalLines(false);
+ keywordTable.setShowVerticalLines(false);
+
+ keywordTable.getParent().setBackground(keywordTable.getBackground());
+
+ //customize column witdhs
+ keywordTable.setSize(260, 200);
+ final int width = keywordTable.getSize().width;
+ TableColumn column = null;
+ for (int i = 0; i < 2; i++) {
+ column = keywordTable.getColumnModel().getColumn(i);
+ if (i == 1) {
+ column.setPreferredWidth(((int) (width * 0.2)));
+ //column.setCellRenderer(new CellTooltipRenderer());
+ } else {
+ column.setCellRenderer(new CellTooltipRenderer());
+ column.setPreferredWidth(((int) (width * 0.75)));
+ }
+ }
+ keywordTable.setCellSelectionEnabled(false);
+
+ loadDefaultKeywords();
+
+ if (KeywordSearchListsXML.getInstance().getNumberLists() == 0) {
+ loadListButton.setEnabled(false);
+ }
+ }
+
+ private void loadDefaultKeywords() {
+ //some hardcoded keywords for testing
+
+ //phone number
+ tableModel.addKeyword("\\d\\d\\d[\\.-]\\d\\d\\d[\\.-]\\d\\d\\d\\d");
+ tableModel.addKeyword("\\d{8,10}");
+ tableModel.addKeyword("phone|fax");
+ //IP address
+ tableModel.addKeyword("(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])");
+ //email
+ tableModel.addKeyword("[e\\-]{0,2}mail");
+ tableModel.addKeyword("[A-Z0-9._%-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}");
+ //URL
+ tableModel.addKeyword("ftp|sftp|ssh|http|https|www");
+ //escaped literal word \d\d\d
+ tableModel.addKeyword("\\Q\\d\\d\\d\\E");
+ }
+
+ /** 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() {
+
+ searchButton = new javax.swing.JButton();
+ filesIndexedNameLabel = new javax.swing.JLabel();
+ filesIndexedValLabel = new javax.swing.JLabel();
+ titleLabel = new javax.swing.JLabel();
+ listLabel = new javax.swing.JLabel();
+ addWordField = new javax.swing.JTextField();
+ addWordLabel = new javax.swing.JLabel();
+ addWordButton = new javax.swing.JButton();
+ loadListButton = new javax.swing.JButton();
+ deleteWordButton = new javax.swing.JButton();
+ deleteAllWordsButton = new javax.swing.JButton();
+ saveListButton = new javax.swing.JButton();
+ chLiteralWord = new javax.swing.JCheckBox();
+ jScrollPane1 = new javax.swing.JScrollPane();
+ keywordTable = new javax.swing.JTable();
+
+ org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.searchButton.text")); // NOI18N
+ searchButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ searchButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(filesIndexedNameLabel, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.filesIndexedNameLabel.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(filesIndexedValLabel, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.filesIndexedValLabel.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.titleLabel.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(listLabel, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.listLabel.text")); // NOI18N
+
+ addWordField.setText(org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.addWordField.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(addWordLabel, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.addWordLabel.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(addWordButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.addWordButton.text")); // NOI18N
+ addWordButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ addWordButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(loadListButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.loadListButton.text")); // NOI18N
+ loadListButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ loadListButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(deleteWordButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.deleteWordButton.text")); // NOI18N
+ deleteWordButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ deleteWordButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(deleteAllWordsButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.deleteAllWordsButton.text")); // NOI18N
+ deleteAllWordsButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ deleteAllWordsButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(saveListButton, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.saveListButton.text")); // NOI18N
+ saveListButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ saveListButtonActionPerformed(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(chLiteralWord, org.openide.util.NbBundle.getMessage(KeywordSearchListTopComponent.class, "KeywordSearchListTopComponent.chLiteralWord.text")); // NOI18N
+ chLiteralWord.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ chLiteralWordActionPerformed(evt);
+ }
+ });
+
+ keywordTable.setModel(tableModel);
+ keywordTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF);
+ keywordTable.setShowHorizontalLines(false);
+ keywordTable.setShowVerticalLines(false);
+ jScrollPane1.setViewportView(keywordTable);
+
+ 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(chLiteralWord)
+ .addComponent(titleLabel)
+ .addComponent(loadListButton)
+ .addComponent(addWordLabel)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(deleteWordButton)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(deleteAllWordsButton)
+ .addGap(18, 18, 18)
+ .addComponent(saveListButton))
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(addWordField, javax.swing.GroupLayout.PREFERRED_SIZE, 152, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addGap(31, 31, 31)
+ .addComponent(addWordButton))
+ .addComponent(listLabel)
+ .addComponent(searchButton)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(filesIndexedNameLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(filesIndexedValLabel))
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 272, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addContainerGap(15, Short.MAX_VALUE))
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addComponent(titleLabel)
+ .addGap(18, 18, 18)
+ .addComponent(loadListButton)
+ .addGap(19, 19, 19)
+ .addComponent(addWordLabel)
+ .addGap(18, 18, 18)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(addWordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(addWordButton))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(chLiteralWord)
+ .addGap(9, 9, 9)
+ .addComponent(listLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 220, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addGap(28, 28, 28)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(deleteWordButton)
+ .addComponent(deleteAllWordsButton)
+ .addComponent(saveListButton))
+ .addGap(29, 29, 29)
+ .addComponent(searchButton)
+ .addGap(38, 38, 38)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(filesIndexedNameLabel)
+ .addComponent(filesIndexedValLabel))
+ .addGap(46, 46, 46))
+ );
+ }// //GEN-END:initComponents
+
+ private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed
+ }//GEN-LAST:event_searchButtonActionPerformed
+
+ private void addWordButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addWordButtonActionPerformed
+
+ String newWord = addWordField.getText();
+ String newWordEscaped = Pattern.quote(newWord);
+
+ if (newWord.trim().equals("")) {
+ return;
+ } else if (keywordExists(newWord) || keywordExists(newWordEscaped)) {
+ KeywordSearchUtil.displayDialog("New Keyword Entry", "Keyword already exists in the list.", KeywordSearchUtil.DIALOG_MESSAGE_TYPE.INFO);
+ return;
+ }
+
+ String toAdd = null;
+ if (chLiteralWord.isSelected()) {
+ toAdd = newWordEscaped;
+ } else {
+ toAdd = newWord;
+ }
+
+ //check if valid
+ boolean valid = true;
+ try {
+ Pattern.compile(toAdd);
+ } catch (PatternSyntaxException ex1) {
+ valid = false;
+ } catch (IllegalArgumentException ex2) {
+ valid = false;
+ }
+ if (!valid) {
+ KeywordSearchUtil.displayDialog("New Keyword Entry", "Invalid keyword pattern. Use words or a correct regex pattern.", KeywordSearchUtil.DIALOG_MESSAGE_TYPE.ERROR);
+ return;
+ }
+
+ //add & reset checkbox
+ chLiteralWord.setSelected(false);
+ tableModel.addKeyword(toAdd);
+ addWordField.setText("");
+
+ }//GEN-LAST:event_addWordButtonActionPerformed
+
+ private void saveListButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveListButtonActionPerformed
+ final String FEATURE_NAME = "Save Keyword List";
+ KeywordSearchListsXML writer = KeywordSearchListsXML.getInstance();
+
+ String listName = (String) JOptionPane.showInputDialog(
+ null,
+ "New keyword list name:",
+ FEATURE_NAME,
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ null,
+ currentKeywordList != null ? currentKeywordList : "");
+ if (listName == null || listName.equals("")) {
+ return;
+ }
+
+ List keywords = tableModel.getAllKeywords();
+ boolean shouldWrite = false;
+ boolean written = false;
+ if (writer.listExists(listName)) {
+ boolean replace = KeywordSearchUtil.displayConfirmDialog(FEATURE_NAME, "Keyword List <" + listName + "> already exists, do you want to replace it?",
+ KeywordSearchUtil.DIALOG_MESSAGE_TYPE.WARN);
+ if (replace) {
+ shouldWrite = true;
+ }
+
+ } else {
+ shouldWrite = true;
+ }
+
+ if (shouldWrite) {
+ writer.addList(listName, keywords);
+ written = writer.save();
+ }
+
+ if (written) {
+ currentKeywordList = listName;
+ KeywordSearchUtil.displayDialog(FEATURE_NAME, "Keyword List <" + listName + "> saved", KeywordSearchUtil.DIALOG_MESSAGE_TYPE.INFO);
+ //enable load button if it was previously disabled, as lists now exist
+ if (loadListButton.isEnabled() == false) {
+ loadListButton.setEnabled(true);
+ }
+ }
+ }//GEN-LAST:event_saveListButtonActionPerformed
+
+ private void chLiteralWordActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chLiteralWordActionPerformed
+ }//GEN-LAST:event_chLiteralWordActionPerformed
+
+ private void deleteWordButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteWordButtonActionPerformed
+ tableModel.deleteSelected();
+ }//GEN-LAST:event_deleteWordButtonActionPerformed
+
+ private void deleteAllWordsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteAllWordsButtonActionPerformed
+ tableModel.deleteAll();
+ }//GEN-LAST:event_deleteAllWordsButtonActionPerformed
+
+ private void loadListButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loadListButtonActionPerformed
+
+ final String FEATURE_NAME = "Load Keyword List";
+
+ KeywordSearchListsXML loader = KeywordSearchListsXML.getInstance();
+
+ String listName = (String) JOptionPane.showInputDialog(
+ null,
+ "Keyword list to load:",
+ FEATURE_NAME,
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ loader.getListNames().toArray(),
+ currentKeywordList);
+
+ if(listName == null || listName.equals(""))
+ return;
+
+ KeywordSearchList list = loader.getList(listName);
+ if (list != null) {
+ List keywords = list.getKeywords();
+
+ //TODO clear/append option ?
+ tableModel.deleteAll();
+ tableModel.addKeywords(keywords);
+ currentKeywordList = listName;
+ KeywordSearchUtil.displayDialog(FEATURE_NAME, "Keyword List <" + listName + "> loaded", KeywordSearchUtil.DIALOG_MESSAGE_TYPE.INFO);
+ }
+
+ }//GEN-LAST:event_loadListButtonActionPerformed
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton addWordButton;
+ private javax.swing.JTextField addWordField;
+ private javax.swing.JLabel addWordLabel;
+ private javax.swing.JCheckBox chLiteralWord;
+ private javax.swing.JButton deleteAllWordsButton;
+ private javax.swing.JButton deleteWordButton;
+ private javax.swing.JLabel filesIndexedNameLabel;
+ private javax.swing.JLabel filesIndexedValLabel;
+ private javax.swing.JScrollPane jScrollPane1;
+ private javax.swing.JTable keywordTable;
+ private javax.swing.JLabel listLabel;
+ private javax.swing.JButton loadListButton;
+ private javax.swing.JButton saveListButton;
+ private javax.swing.JButton searchButton;
+ private javax.swing.JLabel titleLabel;
+ // End of variables declaration//GEN-END:variables
+
+ @Override
+ public void componentOpened() {
+ }
+
+ @Override
+ public void componentClosed() {
+ }
+
+ void writeProperties(java.util.Properties p) {
+ p.setProperty("version", "1.0");
+ }
+
+ void readProperties(java.util.Properties p) {
+ }
+
+ @Override
+ public boolean isMultiwordQuery() {
+ return true;
+ }
+
+ @Override
+ public void addSearchButtonListener(ActionListener l) {
+ searchButton.addActionListener(l);
+ }
+
+ @Override
+ public String getQueryText() {
+ return null;
+ }
+
+ @Override
+ public Map getQueryList() {
+ List selected = getSelectedKeywords();
+ //filter out blank just in case
+ Map ret = new LinkedHashMap();
+ for (String s : selected) {
+ if (!s.trim().equals("")) {
+ //use false for isLiteral because we are currently escaping
+ //the keyword earlier as it is stored
+ //might need to change and pass isLiteral
+ //if the query object needs to treat it specially
+ ret.put(s, false);
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean isLuceneQuerySelected() {
+ return false;
+ }
+
+ @Override
+ public boolean isRegexQuerySelected() {
+ return true;
+ }
+
+ @Override
+ public void setFilesIndexed(int filesIndexed) {
+ filesIndexedValLabel.setText(Integer.toString(filesIndexed));
+ if (filesIndexed == 0) {
+ searchButton.setEnabled(false);
+ } else {
+ searchButton.setEnabled(true);
+ }
+ }
+
+ public List getAllKeywords() {
+ return tableModel.getAllKeywords();
+ }
+
+ public List getSelectedKeywords() {
+ return tableModel.getSelectedKeywords();
+ }
+
+ private boolean keywordExists(String keyword) {
+
+ return tableModel.keywordExists(keyword);
+ }
+
+ static class KeywordTableModel extends AbstractTableModel {
+
+ private static Logger logger = Logger.getLogger(KeywordTableModel.class.getName());
+ //data
+ private Set keywordData = new TreeSet();
+
+ @Override
+ public int getColumnCount() {
+ return 2;
+ }
+
+ @Override
+ public int getRowCount() {
+ return keywordData.size();
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ Object ret = null;
+ TableEntry entry = null;
+ //iterate until row
+ Iterator it = keywordData.iterator();
+ for (int i = 0; i <= rowIndex; ++i) {
+ entry = it.next();
+ }
+ switch (columnIndex) {
+ case 0:
+ ret = (Object) entry.keyword;
+ break;
+ case 1:
+ ret = (Object) entry.isActive;
+ break;
+ default:
+ logger.log(Level.SEVERE, "Invalid table column index: " + columnIndex);
+ break;
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ return columnIndex == 1 ? true : false;
+ }
+
+ @Override
+ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+ if (columnIndex == 1) {
+ TableEntry entry = null;
+ //iterate until row
+ Iterator it = keywordData.iterator();
+ for (int i = 0; i <= rowIndex; ++i) {
+ entry = it.next();
+ }
+ entry.isActive = (Boolean) aValue;
+ }
+ }
+
+ @Override
+ public Class getColumnClass(int c) {
+ return getValueAt(0, c).getClass();
+ }
+
+ List getAllKeywords() {
+ List ret = new ArrayList();
+ for (TableEntry e : keywordData) {
+ ret.add(e.keyword);
+ }
+ return ret;
+ }
+
+ List getSelectedKeywords() {
+ List ret = new ArrayList();
+ for (TableEntry e : keywordData) {
+ if (e.isActive && !e.keyword.equals("")) {
+ ret.add(e.keyword);
+ }
+ }
+ return ret;
+ }
+
+ boolean keywordExists(String keyword) {
+ List all = getAllKeywords();
+ return all.contains(keyword);
+ }
+
+ void addKeyword(String keyword) {
+ if (!keywordExists(keyword)) {
+ keywordData.add(new TableEntry(keyword));
+ }
+ fireTableDataChanged();
+ }
+
+ void addKeywords(List keywords) {
+ for (String keyword : keywords) {
+ if (!keywordExists(keyword)) {
+ keywordData.add(new TableEntry(keyword));
+ }
+ }
+ fireTableDataChanged();
+ }
+
+ void deleteAll() {
+ keywordData.clear();
+ fireTableDataChanged();
+ }
+
+ void deleteSelected() {
+ List toDel = new ArrayList();
+ int i = 0;
+ for (TableEntry e : keywordData) {
+ if (e.isActive && !e.keyword.equals("")) {
+ toDel.add(e);
+ }
+ }
+ for (TableEntry del : toDel) {
+ keywordData.remove(del);
+ }
+ fireTableDataChanged();
+
+ }
+
+ class TableEntry implements Comparable {
+
+ String keyword;
+ Boolean isActive;
+
+ TableEntry(String keyword, Boolean isActive) {
+ this.keyword = keyword;
+ this.isActive = isActive;
+ }
+
+ TableEntry(String keyword) {
+ this.keyword = keyword;
+ this.isActive = false;
+ }
+
+ @Override
+ public int compareTo(Object o) {
+ return this.keyword.compareTo(((TableEntry) o).keyword);
+ }
+ }
+ }
+
+ /**
+ * tooltips that show entire query string
+ */
+ public static class CellTooltipRenderer extends DefaultTableCellRenderer {
+
+ @Override
+ public Component getTableCellRendererComponent(
+ JTable table, Object value,
+ boolean isSelected, boolean hasFocus,
+ int row, int column) {
+
+ if (column == 0) {
+ String val = (String) table.getModel().getValueAt(row, column);
+ setToolTipText(val);
+ setText(val);
+ }
+
+ return this;
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListsXML.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListsXML.java
new file mode 100644
index 0000000000..4e2e105a46
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchListsXML.java
@@ -0,0 +1,350 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.sleuthkit.autopsy.coreutils.AutopsyPropFile;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/**
+ * Manages reading and writing of keyword lists to user settings XML file keywords.xml
+ */
+public class KeywordSearchListsXML {
+
+ private static final String ROOT_EL = "keyword_lists";
+ private static final String LIST_EL = "keyword_list";
+ private static final String LIST_NAME_ATTR = "name";
+ private static final String LIST_CREATE_ATTR = "created";
+ private static final String LIST_MOD_ATTR = "modified";
+ private static final String KEYWORD_EL = "keyword";
+ private static final String LISTS_FILE_NAME = "keywords.xml";
+ private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
+ private static final String ENCODING = "UTF-8";
+ private String LISTS_FILE = AutopsyPropFile.getUserDirPath() + File.separator + LISTS_FILE_NAME;
+ private static final Logger logger = Logger.getLogger(KeywordSearchListsXML.class.getName());
+
+ Map theLists; //the keyword data
+
+ static KeywordSearchListsXML theInstance = null;
+
+ private KeywordSearchListsXML() {
+ }
+
+ static KeywordSearchListsXML getInstance() {
+ if (theInstance == null) {
+ theInstance = new KeywordSearchListsXML();
+ theInstance.reload();
+ }
+ return theInstance;
+ }
+
+ /**
+ * load the file or create new
+ */
+ public void reload() {
+ boolean created = false;
+ theLists = new LinkedHashMap();
+ if (!this.listFileExists()) {
+ //create new if it doesn't exist
+ save();
+ created = true;
+ }
+
+ if (!load() && !created) {
+ //create new if failed to load
+ save();
+ }
+
+ }
+
+ /**
+ * get all loaded keyword lists
+ * @return List of keyword list objects
+ */
+ Map getLists() {
+ return theLists;
+ }
+
+ /**
+ * get list of all loaded keyword list names
+ * @return List of keyword list names
+ */
+ ListgetListNames() {
+ return new ArrayList(theLists.keySet());
+ }
+
+ /**
+ * get number of lists currently stored
+ * @return number of lists currently stored
+ */
+ int getNumberLists() {
+ return theLists.size();
+ }
+
+ /**
+ * get list by name or null
+ * @param name id of the list
+ * @return keyword list representation
+ */
+ KeywordSearchList getList(String name) {
+ return theLists.get(name);
+ }
+
+ /**
+ * check if list with given name id exists
+ * @param name id to check
+ * @return true if list already exists or false otherwise
+ */
+ boolean listExists(String name) {
+ return getList(name) != null;
+ }
+
+ /**
+ * adds the new word list using name id
+ * replacing old one if exists with the same name
+ * requires following call to save() to make permanent changes
+ * @param name the name of the new list or list to replace
+ * @param newList list of keywords
+ * @return true if old list was replaced
+ */
+ boolean addList(String name, List newList) {
+ boolean replaced = false;
+ KeywordSearchList curList = getList(name);
+ final Date now = new Date();
+ if (curList == null) {
+ theLists.put(name, new KeywordSearchList(name, now, now, newList));
+ } else {
+ theLists.put(name, new KeywordSearchList(name, curList.getDateCreated(), now, newList));
+ replaced = true;
+ }
+ return replaced;
+ }
+
+ /**
+ * writes out current list replacing the last lists file
+ */
+ boolean save() {
+ boolean success = false;
+ DateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
+ DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
+
+ try {
+ DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
+ Document doc = docBuilder.newDocument();
+
+ Element rootEl = doc.createElement(ROOT_EL);
+ doc.appendChild(rootEl);
+
+ for (String listName : theLists.keySet()) {
+ KeywordSearchList list = theLists.get(listName);
+ String created = dateFormatter.format(list.getDateCreated());
+ String modified = dateFormatter.format(list.getDateModified());
+ List keywords = list.getKeywords();
+
+ Element listEl = doc.createElement(LIST_EL);
+ listEl.setAttribute(LIST_NAME_ATTR, listName);
+ listEl.setAttribute(LIST_CREATE_ATTR, created);
+ listEl.setAttribute(LIST_MOD_ATTR, modified);
+
+ for (String keyword : keywords) {
+ Element keywordEl = doc.createElement(KEYWORD_EL);
+ keywordEl.setTextContent(keyword);
+ listEl.appendChild(keywordEl);
+ }
+ rootEl.appendChild(listEl);
+ }
+
+ success = saveDoc(doc);
+ } catch (ParserConfigurationException e) {
+ logger.log(Level.SEVERE, "Error saving keyword list: can't initialize parser.", e);
+ }
+ return success;
+ }
+
+
+
+ /**
+ * load and parse XML, then dispose
+ */
+ private boolean load() {
+ final Document doc = loadDoc();
+ if (doc == null) {
+ return false;
+ }
+ DateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
+
+
+ Element root = doc.getDocumentElement();
+ if (root == null) {
+ logger.log(Level.SEVERE, "Error loading keyword list: invalid file format.");
+ return false;
+ }
+ try {
+ NodeList listsNList = root.getElementsByTagName(LIST_EL);
+ int numLists = listsNList.getLength();
+ for (int i = 0; i < numLists; ++i) {
+ Element listEl = (Element) listsNList.item(i);
+ final String name = listEl.getAttribute(LIST_NAME_ATTR);
+ final String created = listEl.getAttribute(LIST_CREATE_ATTR);
+ final String modified = listEl.getAttribute(LIST_MOD_ATTR);
+ Date createdDate = dateFormatter.parse(created);
+ Date modDate = dateFormatter.parse(modified);
+ List words = new ArrayList();
+ KeywordSearchList list = new KeywordSearchList(name, createdDate, modDate, words);
+
+ //parse all words
+ NodeList wordsNList = listEl.getElementsByTagName(KEYWORD_EL);
+ final int numKeywords = wordsNList.getLength();
+ for (int j = 0; j < numKeywords; ++j) {
+ Element wordEl = (Element) wordsNList.item(j);
+ words.add(wordEl.getTextContent());
+
+ }
+ theLists.put(name, list);
+ }
+ } catch (ParseException e) {
+ //error parsing dates
+ logger.log(Level.SEVERE, "Error loading keyword list: can't parse dates.", e);
+ return false;
+ }
+ return true;
+ }
+
+ private boolean listFileExists() {
+ File f = new File(LISTS_FILE);
+ return f.exists() && f.canRead() && f.canWrite();
+ }
+
+ private Document loadDoc() {
+ DocumentBuilderFactory builderFactory =
+ DocumentBuilderFactory.newInstance();
+
+ Document ret = null;
+
+
+ try {
+ DocumentBuilder builder = builderFactory.newDocumentBuilder();
+ ret = builder.parse(
+ new FileInputStream(LISTS_FILE));
+ } catch (ParserConfigurationException e) {
+ logger.log(Level.SEVERE, "Error loading keyword list: can't initialize parser.", e);
+
+ } catch (SAXException e) {
+ logger.log(Level.SEVERE, "Error loading keyword list: can't parse XML.", e);
+
+ } catch (IOException e) {
+ //error reading file
+ logger.log(Level.SEVERE, "Error loading keyword list: can't read file.", e);
+
+ }
+ return ret;
+
+ }
+
+ private boolean saveDoc(final Document doc) {
+ TransformerFactory xf = TransformerFactory.newInstance();
+ xf.setAttribute("indent-number", new Integer(1));
+ boolean success = false;
+ try {
+ Transformer xformer = xf.newTransformer();
+ xformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ xformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ xformer.setOutputProperty(OutputKeys.ENCODING, ENCODING);
+ xformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
+ xformer.setOutputProperty(OutputKeys.VERSION, "1.0");
+ Result out = new StreamResult(new OutputStreamWriter(new FileOutputStream(new File(LISTS_FILE)), ENCODING));
+ xformer.transform(new DOMSource(doc), out);
+ success = true;
+ } catch (UnsupportedEncodingException e) {
+ logger.log(Level.SEVERE, "Should not happen", e);
+ } catch (TransformerConfigurationException e) {
+ logger.log(Level.SEVERE, "Error writing keyword lists XML", e);
+ } catch (TransformerException e) {
+ logger.log(Level.SEVERE, "Error writing keyword lists XML", e);
+ } catch (FileNotFoundException e) {
+ logger.log(Level.SEVERE, "Error writing keyword lists XML: cannot write to file: " + LISTS_FILE, e);
+ }
+ return success;
+ }
+}
+
+/**
+ * a representation of a single keyword list
+ * created or loaded
+ */
+class KeywordSearchList {
+
+ private String name;
+ private Date created;
+ private Date modified;
+ private List keywords;
+
+ KeywordSearchList(String name, Date created, Date modified, List keywords) {
+ this.name = name;
+ this.created = created;
+ this.modified = modified;
+ this.keywords = keywords;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ Date getDateCreated() {
+ return created;
+ }
+
+ Date getDateModified() {
+ return modified;
+ }
+
+ List getKeywords() {
+ return keywords;
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchNode.java
new file mode 100644
index 0000000000..4db29b61dc
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchNode.java
@@ -0,0 +1,53 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.List;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.Node;
+import org.sleuthkit.autopsy.datamodel.RootContentChildren;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.FsContent;
+
+/**
+ * Root Node for keyword search results
+ */
+class KeywordSearchNode extends AbstractNode {
+
+ KeywordSearchNode(List keys, final String solrQuery) {
+ super(new RootContentChildren(keys) {
+
+ @Override
+ protected Node[] createNodes(Content key) {
+ Node[] originalNodes = super.createNodes(key);
+ Node[] filterNodes = new Node[originalNodes.length];
+
+ // Use filter node to add a MarkupSource for the search results
+ // to the lookup
+ int i = 0;
+ for (Node original : originalNodes) {
+ HighlightedMatchesSource markup = new HighlightedMatchesSource(key, solrQuery);
+ filterNodes[i++] = new KeywordSearchFilterNode(markup, original, solrQuery);
+ }
+
+ return filterNodes;
+ }
+ });
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java
new file mode 100644
index 0000000000..31351c3543
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java
@@ -0,0 +1,71 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.Collection;
+import java.util.List;
+import org.apache.solr.client.solrj.response.TermsResponse.Term;
+import org.sleuthkit.datamodel.FsContent;
+
+public interface KeywordSearchQuery {
+
+ /**
+ * validate the query pre execution
+ * @return true if the query passed validation
+ */
+ public boolean validate();
+
+
+ /**
+ * execute query and return results without publishing them
+ * @return
+ */
+ public List performQuery();
+
+
+ /**
+ * execute the query and publish results
+ */
+ public void execute();
+
+ /**
+ * escape the query string and use the escaped string in the query
+ */
+ public void escape();
+
+ /**
+ * return original query string
+ * @return the query String supplied originally
+ */
+ public String getQueryString();
+
+ /**
+ * return escaped query string if escaping was done
+ * @return the escaped query string, or original string if no escaping done
+ */
+ public String getEscapedQueryString();
+
+ /**
+ * get terms associated with the query if any
+ * @return collection of terms associated with the query
+ */
+ public CollectiongetTerms();
+
+
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryManager.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryManager.java
new file mode 100644
index 0000000000..b1ff1db304
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryManager.java
@@ -0,0 +1,204 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.solr.client.solrj.response.TermsResponse.Term;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.windows.TopComponent;
+import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent;
+import org.sleuthkit.autopsy.datamodel.KeyValueThing;
+import org.sleuthkit.autopsy.keywordsearch.KeywordSearch.QueryType;
+import org.sleuthkit.datamodel.FsContent;
+
+/**
+ * Query manager responsible for running appropriate queries and displaying results
+ * for single, multi keyword queries, with detailed or collapsed results
+ */
+public class KeywordSearchQueryManager implements KeywordSearchQuery {
+
+ public enum Presentation {
+
+ COLLAPSE, DETAIL
+ };
+ //map query->boolean (true if literal, false otherwise)
+ private Map queries;
+ private Presentation presentation;
+ private List queryDelegates;
+ private QueryType queryType;
+ private static Logger logger = Logger.getLogger(KeywordSearchQueryManager.class.getName());
+
+ public KeywordSearchQueryManager(Map queries, Presentation presentation) {
+ this.queries = queries;
+ this.presentation = presentation;
+ queryType = QueryType.REGEX;
+ init();
+ }
+
+ public KeywordSearchQueryManager(String query, QueryType qt, Presentation presentation) {
+ queries = new LinkedHashMap();
+ queries.put(query, false);
+ this.presentation = presentation;
+ queryType = qt;
+ init();
+ }
+
+ public KeywordSearchQueryManager(String query, boolean isLiteral, Presentation presentation) {
+ queries = new LinkedHashMap();
+ queries.put(query, isLiteral);
+ this.presentation = presentation;
+ queryType = QueryType.REGEX;
+ init();
+ }
+
+ private void init() {
+ queryDelegates = new ArrayList();
+ for (String query : queries.keySet()) {
+ KeywordSearchQuery del = null;
+ switch (queryType) {
+ case WORD:
+ del = new LuceneQuery(query);
+ break;
+ case REGEX:
+ del = new TermComponentQuery(query);
+ break;
+ default:
+ ;
+ }
+ queryDelegates.add(del);
+
+ }
+ escape();
+
+ }
+
+ @Override
+ public void execute() {
+ //execute and present the query
+ //delegate query to query objects and presentation child factories
+ if (queryType == QueryType.WORD || presentation == Presentation.DETAIL) {
+ for (KeywordSearchQuery q : queryDelegates) {
+ q.execute();
+ }
+ } else {
+ //Collapsed view
+ Collection things = new ArrayList();
+ int queryID = 0;
+ for (KeywordSearchQuery q : queryDelegates) {
+ Map kvs = new LinkedHashMap();
+ final String queryStr = q.getQueryString();
+ things.add(new KeyValueThingQuery(queryStr, kvs, ++queryID, q));
+ }
+
+ Node rootNode = null;
+ if (things.size() > 0) {
+ Children childThingNodes =
+ Children.create(new KeywordSearchResultFactory(queries.keySet(), things, Presentation.COLLAPSE), true);
+
+ rootNode = new AbstractNode(childThingNodes);
+ } else {
+ rootNode = Node.EMPTY;
+ }
+
+ final String pathText = "Keyword query";
+ TopComponent searchResultWin = DataResultTopComponent.createInstance("Keyword search", pathText, rootNode, things.size());
+ searchResultWin.requestActive();
+ }
+ }
+
+ @Override
+ public void escape() {
+ for (KeywordSearchQuery q : queryDelegates) {
+ boolean shouldEscape = queries.get(q.getQueryString());
+ if (shouldEscape) {
+ q.escape();
+ }
+ }
+
+ }
+
+ @Override
+ public List performQuery() {
+ //not done here
+ return null;
+ }
+
+ @Override
+ public boolean validate() {
+ boolean allValid = true;
+ for (KeywordSearchQuery tcq : queryDelegates) {
+ if (!tcq.validate()) {
+ logger.log(Level.WARNING, "Query has invalid syntax: " + tcq.getQueryString());
+ allValid = false;
+ break;
+ }
+ }
+ return allValid;
+ }
+
+ @Override
+ public String getEscapedQueryString() {
+ StringBuilder sb = new StringBuilder();
+ final String SEP = queryType == QueryType.WORD ? " " : "|";
+ for (KeywordSearchQuery q : queryDelegates) {
+ sb.append(q.getEscapedQueryString()).append(SEP);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String getQueryString() {
+ StringBuilder sb = new StringBuilder();
+ final String SEP = queryType == QueryType.WORD ? " " : "|";
+ for (KeywordSearchQuery q : queryDelegates) {
+ sb.append(q.getQueryString()).append(SEP);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public Collection getTerms() {
+ return null;
+ }
+}
+
+/*
+ * custom KeyValueThing that also stores query object to execute
+ */
+class KeyValueThingQuery extends KeyValueThing {
+
+ private KeywordSearchQuery query;
+
+ KeywordSearchQuery getQuery() {
+ return query;
+ }
+
+ public KeyValueThingQuery(String name, Map map, int id, KeywordSearchQuery query) {
+ super(name, map, id);
+ this.query = query;
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java
new file mode 100644
index 0000000000..8be35ba8a0
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java
@@ -0,0 +1,359 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.swing.SwingUtilities;
+import org.apache.solr.client.solrj.response.TermsResponse.Term;
+import org.openide.nodes.ChildFactory;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.util.Lookup;
+import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer;
+import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
+import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode;
+import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode.FsContentPropertyType;
+import org.sleuthkit.autopsy.datamodel.KeyValueNode;
+import org.sleuthkit.autopsy.datamodel.KeyValueThing;
+import org.sleuthkit.autopsy.keywordsearch.KeywordSearchQueryManager.Presentation;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.File;
+import org.sleuthkit.datamodel.FsContent;
+
+/**
+ *
+ * factory produces top level nodes with query
+ * responsible for assembling nodes and columns in the right way
+ * and performing lazy queries as needed
+ */
+public class KeywordSearchResultFactory extends ChildFactory {
+
+ //common properties (superset of all Node properties) to be displayed as columns
+ //these are merged with FsContentPropertyType defined properties
+ public static enum CommonPropertyTypes {
+
+ QUERY {
+
+ @Override
+ public String toString() {
+ return "Query";
+ }
+ },
+ MATCH {
+
+ @Override
+ public String toString() {
+ return "Match";
+ }
+ },}
+ private Presentation presentation;
+ private Collection queries;
+ private Collection things;
+ private static final Logger logger = Logger.getLogger(KeywordSearchResultFactory.class.getName());
+
+ KeywordSearchResultFactory(Collection queries, Collection things, Presentation presentation) {
+ this.queries = queries;
+ this.things = things;
+ this.presentation = presentation;
+ }
+
+ KeywordSearchResultFactory(String query, Collection things, Presentation presentation) {
+ queries = new ArrayList();
+ queries.add(query);
+ this.presentation = presentation;
+ this.things = things;
+ }
+
+ /**
+ * call this at least for the parent Node, to make sure all common
+ * properties are displayed as columns (since we are doing lazy child Node load
+ * we need to preinitialize properties when sending parent Node)
+ * @param toSet property set map for a Node
+ */
+ public static void initCommonProperties(Map toSet) {
+ CommonPropertyTypes[] commonTypes = CommonPropertyTypes.values();
+ final int COMMON_PROPS_LEN = commonTypes.length;
+ for (int i = 0; i < COMMON_PROPS_LEN; ++i) {
+ toSet.put(commonTypes[i].toString(), "");
+ }
+
+ FsContentPropertyType[] fsTypes = FsContentPropertyType.values();
+ final int FS_PROPS_LEN = fsTypes.length;
+ for (int i = 0; i < FS_PROPS_LEN; ++i) {
+ toSet.put(fsTypes[i].toString(), "");
+ }
+
+ }
+
+ public static void setCommonProperty(Map toSet, CommonPropertyTypes type, String value) {
+ final String typeStr = type.toString();
+ toSet.put(typeStr, value);
+ }
+
+ @Override
+ protected boolean createKeys(List toPopulate) {
+ int id = 0;
+ if (presentation == Presentation.DETAIL) {
+ for (String query : queries) {
+ Map map = new LinkedHashMap();
+ initCommonProperties(map);
+ setCommonProperty(map, CommonPropertyTypes.QUERY, query);
+ toPopulate.add(new KeyValueThing(query, map, ++id));
+ }
+ } else {
+ for (KeyValueThing thing : things) {
+ //Map map = new LinkedHashMap();
+ Map map = thing.getMap();
+ initCommonProperties(map);
+ final String query = thing.getName();
+ setCommonProperty(map, CommonPropertyTypes.QUERY, query);
+ //toPopulate.add(new KeyValueThing(query, map, ++id));
+ toPopulate.add(thing);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ protected Node createNodeForKey(KeyValueThing thing) {
+ ChildFactory childFactory = null;
+
+ if (presentation == Presentation.COLLAPSE) {
+ childFactory = new ResultCollapsedChildFactory(thing);
+ final Node ret = new KeyValueNode(thing, Children.create(childFactory, true));
+ SwingUtilities.invokeLater(new Runnable() {
+
+ @Override
+ public void run() {
+ //DataResultViewerTable view = Utilities.actionsGlobalContext().lookup(DataResultViewerTable.class);
+ for (DataResultViewer view : Lookup.getDefault().lookupAll(DataResultViewer.class)) {
+ view.expandNode(ret);
+ }
+ }
+ });
+ return ret;
+ } else {
+
+ childFactory = new ResulTermsMatchesChildFactory(things);
+ return new KeyValueNode(thing, Children.create(childFactory, true));
+ }
+ }
+
+ /**
+ * factory produces collapsed view of all fscontent matches per query
+ * the node produced is a child node
+ * The factory actually executes query.
+ */
+ class ResultCollapsedChildFactory extends ChildFactory {
+
+ KeyValueThing queryThing;
+
+ ResultCollapsedChildFactory(KeyValueThing queryThing) {
+ this.queryThing = queryThing;
+ }
+
+ @Override
+ protected boolean createKeys(List toPopulate) {
+ final String origQuery = queryThing.getName();
+ final KeyValueThingQuery queryThingQuery = (KeyValueThingQuery) queryThing;
+ final KeywordSearchQuery tcq = queryThingQuery.getQuery();
+
+ if (!tcq.validate()) {
+ //TODO mark the particular query node RED
+ return false;
+ }
+
+ //execute the query and get fscontents matching
+ List fsContents = tcq.performQuery();
+
+ //construct a Solr query using aggregated terms to get highlighting
+ //the query is executed later on demand
+ StringBuilder highlightQuery = new StringBuilder();
+ Collection terms = tcq.getTerms();
+ final int lastTerm = terms.size() - 1;
+ int curTerm = 0;
+ for (Term term : terms) {
+ final String termS = KeywordSearchUtil.escapeLuceneQuery(term.getTerm(), true, false);
+ if (!termS.contains("*")) {
+ highlightQuery.append(termS);
+ if (lastTerm != curTerm) {
+ highlightQuery.append(" "); //acts as OR ||
+ }
+ }
+ }
+ //String highlightQueryEscaped = KeywordSearchUtil.escapeLuceneQuery(highlightQuery.toString());
+ String highlightQueryEscaped = highlightQuery.toString();
+
+ int resID = 0;
+ for (FsContent f : fsContents) {
+ //get unique match result files
+ Map resMap = new LinkedHashMap();
+ AbstractFsContentNode.fillPropertyMap(resMap, f);
+ setCommonProperty(resMap, CommonPropertyTypes.MATCH, f.getName());
+ toPopulate.add(new KeyValueThingContent(f.getName(), resMap, ++resID, f, highlightQueryEscaped));
+ }
+
+ return true;
+ }
+
+ @Override
+ protected Node createNodeForKey(KeyValueThing thing) {
+ //return new KeyValueNode(thing, Children.LEAF);
+ //return new KeyValueNode(thing, Children.create(new ResultFilesChildFactory(thing), true));
+ final KeyValueThingContent thingContent = (KeyValueThingContent) thing;
+ final Content content = thingContent.getContent();
+ final String query = thingContent.getQuery();
+
+ Node kvNode = new KeyValueNode(thingContent, Children.LEAF);
+ //wrap in KeywordSearchFilterNode for the markup content, might need to override FilterNode for more customization
+ HighlightedMatchesSource highlights = new HighlightedMatchesSource(content, query);
+ return new KeywordSearchFilterNode(highlights, kvNode, query);
+
+ }
+ }
+
+ /**
+ * factory produces top level result nodes showing *exact* regex match result
+ */
+ class ResulTermsMatchesChildFactory extends ChildFactory {
+
+ Collection things;
+
+ ResulTermsMatchesChildFactory(Collection things) {
+ this.things = things;
+ }
+
+ @Override
+ protected boolean createKeys(List toPopulate) {
+ return toPopulate.addAll(things);
+ }
+
+ @Override
+ protected Node createNodeForKey(KeyValueThing thing) {
+ //return new KeyValueNode(thing, Children.LEAF);
+ return new KeyValueNode(thing, Children.create(new ResultFilesChildFactory(thing), true));
+ }
+
+ /**
+ * factory produces 2nd level child nodes showing files with *approximate* matches
+ * since they rely on underlying Lucene query to get details
+ * To implement exact regex match detail view, we need to extract files content
+ * returned by Lucene and further narrow down by applying a Java regex
+ */
+ class ResultFilesChildFactory extends ChildFactory {
+
+ private KeyValueThing thing;
+
+ ResultFilesChildFactory(KeyValueThing thing) {
+ this.thing = thing;
+ }
+
+ @Override
+ protected boolean createKeys(List toPopulate) {
+ //use Lucene query to get files with regular expression match result
+ final String keywordQuery = thing.getName();
+ LuceneQuery filesQuery = new LuceneQuery(keywordQuery);
+ filesQuery.escape();
+ List matches = filesQuery.performQuery();
+
+ //get unique match result files
+ Set uniqueMatches = new LinkedHashSet();
+ uniqueMatches.addAll(matches);
+
+ int resID = 0;
+ for (FsContent f : uniqueMatches) {
+ Map resMap = new LinkedHashMap();
+ AbstractFsContentNode.fillPropertyMap(resMap, (File) f);
+ toPopulate.add(new KeyValueThingContent(f.getName(), resMap, ++resID, f, keywordQuery));
+ }
+
+ return true;
+ }
+
+ @Override
+ protected Node createNodeForKey(KeyValueThing thing) {
+ final KeyValueThingContent thingContent = (KeyValueThingContent) thing;
+ final Content content = thingContent.getContent();
+ final String query = thingContent.getQuery();
+
+ final String contentStr = KeywordSearch.getServer().getCore().getSolrContent(content);
+
+ //postprocess
+ //make sure Solr result contains a match (this gets rid of large number of false positives)
+ boolean postprocess = true;
+ boolean matchFound = true;
+ if (postprocess) {
+ if (contentStr != null) {//if not null, some error getting from Solr, handle it by not filtering out
+ //perform java regex to validate match from Solr
+ String origQuery = thingContent.getQuery();
+
+ //since query is a match result, we can assume literal pattern
+ origQuery = Pattern.quote(origQuery);
+ Pattern p = Pattern.compile(origQuery, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+
+ Matcher m = p.matcher(contentStr);
+ matchFound = m.find();
+ }
+ }
+
+ if (matchFound) {
+ Node kvNode = new KeyValueNode(thingContent, Children.LEAF);
+ //wrap in KeywordSearchFilterNode for the markup content
+ HighlightedMatchesSource highlights = new HighlightedMatchesSource(content, query);
+ return new KeywordSearchFilterNode(highlights, kvNode, query);
+ } else {
+ return null;
+ }
+ }
+ }
+ }
+
+ /*
+ * custom KeyValueThing that also stores retrieved Content and query string used
+ */
+ class KeyValueThingContent extends KeyValueThing {
+
+ private Content content;
+ private String query;
+
+ Content getContent() {
+ return content;
+ }
+
+ String getQuery() {
+ return query;
+ }
+
+ public KeyValueThingContent(String name, Map map, int id, Content content, String query) {
+ super(name, map, id);
+ this.content = content;
+ this.query = query;
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.form
new file mode 100644
index 0000000000..157b833141
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.form
@@ -0,0 +1,130 @@
+
+
+
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.java
new file mode 100644
index 0000000000..ae47e9bce9
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchSimpleTopComponent.java
@@ -0,0 +1,192 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.event.ActionListener;
+import java.util.Map;
+import java.util.logging.Logger;
+import org.openide.windows.TopComponent;
+
+public class KeywordSearchSimpleTopComponent extends TopComponent implements KeywordSearchTopComponentInterface {
+
+ private Logger logger = Logger.getLogger(KeywordSearchSimpleTopComponent.class.getName());
+
+ /** Creates new form KeywordSearchSimpleTopComponent */
+ public KeywordSearchSimpleTopComponent() {
+ initComponents();
+ setName("Simple");
+ searchButton.setEnabled(false);
+
+ putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.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() {
+
+ buttonGroup1 = new javax.swing.ButtonGroup();
+ jScrollPane1 = new javax.swing.JScrollPane();
+ queryTextArea = new javax.swing.JTextArea();
+ searchButton = new javax.swing.JButton();
+ queryLabel = new javax.swing.JLabel();
+ filesIndexedNameLabel = new javax.swing.JLabel();
+ filesIndexedValLabel = new javax.swing.JLabel();
+ chRegex = new javax.swing.JCheckBox();
+
+ queryTextArea.setColumns(20);
+ queryTextArea.setRows(5);
+ jScrollPane1.setViewportView(queryTextArea);
+
+ searchButton.setText(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchSimpleTopComponent.searchButton.text")); // NOI18N
+
+ queryLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchSimpleTopComponent.queryLabel.text")); // NOI18N
+
+ filesIndexedNameLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchSimpleTopComponent.filesIndexedNameLabel.text")); // NOI18N
+
+ filesIndexedValLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchSimpleTopComponent.filesIndexedValLabel.text")); // NOI18N
+
+ chRegex.setText(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchSimpleTopComponent.chRegex.text")); // NOI18N
+ chRegex.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ chRegexActionPerformed(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(queryLabel)
+ .addGap(50, 50, 50)
+ .addComponent(chRegex))
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 599, Short.MAX_VALUE)
+ .addComponent(searchButton))
+ .addContainerGap())
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(filesIndexedNameLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(filesIndexedValLabel))))
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(queryLabel)
+ .addComponent(chRegex))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addGap(18, 18, 18)
+ .addComponent(searchButton)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(filesIndexedNameLabel)
+ .addComponent(filesIndexedValLabel))
+ .addContainerGap(106, Short.MAX_VALUE))
+ );
+
+ filesIndexedNameLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchTopComponent.filesIndexedNameLabel.AccessibleContext.accessibleName")); // NOI18N
+ filesIndexedValLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(KeywordSearchSimpleTopComponent.class, "KeywordSearchTopComponent.filesIndexedValLabel.AccessibleContext.accessibleName")); // NOI18N
+ }// //GEN-END:initComponents
+
+ private void chRegexActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chRegexActionPerformed
+ // TODO add your handling code here:
+ }//GEN-LAST:event_chRegexActionPerformed
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.ButtonGroup buttonGroup1;
+ private javax.swing.JCheckBox chRegex;
+ private javax.swing.JLabel filesIndexedNameLabel;
+ private javax.swing.JLabel filesIndexedValLabel;
+ private javax.swing.JScrollPane jScrollPane1;
+ private javax.swing.JLabel queryLabel;
+ private javax.swing.JTextArea queryTextArea;
+ private javax.swing.JButton searchButton;
+ // End of variables declaration//GEN-END:variables
+
+
+ @Override
+ protected void componentOpened() {
+ // clear old search
+ queryTextArea.setText("");
+ }
+
+ @Override
+ public boolean isMultiwordQuery() {
+ return false;
+ }
+
+ @Override
+ public void addSearchButtonListener(ActionListener l) {
+ searchButton.addActionListener(l);
+ }
+
+ @Override
+ public String getQueryText() {
+ return queryTextArea.getText();
+ }
+
+ @Override
+ public Map getQueryList() {
+ return null;
+ }
+
+ @Override
+ public boolean isLuceneQuerySelected() {
+ return !chRegex.isSelected();
+ }
+
+ @Override
+ public boolean isRegexQuerySelected() {
+ return chRegex.isSelected();
+ }
+
+ /**
+ * Overwrite when you want to change default persistence type. Default
+ * persistence type is PERSISTENCE_ALWAYS
+ *
+ * @return TopComponent.PERSISTENCE_NEVER
+ */
+ @Override
+ public int getPersistenceType() {
+ return TopComponent.PERSISTENCE_NEVER;
+ }
+
+ @Override
+ public void setFilesIndexed(int filesIndexed) {
+ filesIndexedValLabel.setText(Integer.toString(filesIndexed));
+ if (filesIndexed == 0) {
+ searchButton.setEnabled(false);
+ } else {
+ searchButton.setEnabled(true);
+ }
+ }
+
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.form
new file mode 100644
index 0000000000..747df8cbdb
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.form
@@ -0,0 +1,34 @@
+
+
+
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.java
new file mode 100644
index 0000000000..2b25a7f8a6
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTabsTopComponent.java
@@ -0,0 +1,208 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.openide.util.NbBundle;
+import org.openide.windows.TopComponent;
+import org.netbeans.api.settings.ConvertAsProperties;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+
+/**
+ * Keyword Search explorer top component, container for specific Keyword Search tabs
+ */
+@ConvertAsProperties(dtd = "-//org.sleuthkit.autopsy.keywordsearch//KeywordSearchTabsTopComponent//EN",
+autostore = false)
+@TopComponent.Description(preferredID = "KeywordSearchTabsTopComponent",
+//iconBase="SET/PATH/TO/ICON/HERE",
+persistenceType = TopComponent.PERSISTENCE_NEVER)
+@TopComponent.Registration(mode = "explorer", openAtStartup = false)
+@ActionID(category = "Window", id = "org.sleuthkit.autopsy.keywordsearch.KeywordSearchTabsTopComponentTopComponent")
+@ActionReference(path = "Menu/Window" /*, position = 333 */)
+@TopComponent.OpenActionRegistration(displayName = "#CTL_KeywordSearchTabsTopComponentAction",
+preferredID = "KeywordSearchTabsTopComponent")
+public final class KeywordSearchTabsTopComponent extends TopComponent implements KeywordSearchTopComponentInterface {
+
+ private Logger logger = Logger.getLogger(KeywordSearchTabsTopComponent.class.getName());
+ private PropertyChangeListener serverChangeListener;
+
+ public KeywordSearchTabsTopComponent() {
+ initComponents();
+ initTabs();
+ setName(NbBundle.getMessage(KeywordSearchTabsTopComponent.class, "CTL_KeywordSearchTabsTopComponentTopComponent"));
+ setToolTipText(NbBundle.getMessage(KeywordSearchTabsTopComponent.class, "HINT_KeywordSearchTabsTopComponentTopComponent"));
+
+
+ putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.TRUE);
+
+ //register with server Actions
+ serverChangeListener = new KeywordSearchServerListener();
+ KeywordSearch.getServer().addServerActionListener(serverChangeListener);
+ }
+
+ /** 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() {
+
+ tabs = new javax.swing.JTabbedPane();
+
+ javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+ this.setLayout(layout);
+ layout.setHorizontalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(tabs, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE)
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(tabs, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE)
+ );
+ }// //GEN-END:initComponents
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JTabbedPane tabs;
+ // End of variables declaration//GEN-END:variables
+
+ private void initTabs() {
+ tabs.addTab("Simple", null, new KeywordSearchSimpleTopComponent(), "Single keyword or regex search");
+ tabs.addTab("List", null, new KeywordSearchListTopComponent(), "Search for or load a saved list of keywords.");
+ }
+
+ @Override
+ public void componentOpened() {
+ }
+
+ @Override
+ public void componentClosed() {
+ }
+
+ void writeProperties(java.util.Properties p) {
+ // better to version settings since initial version as advocated at
+ // http://wiki.apidesign.org/wiki/PropertyFiles
+ p.setProperty("version", "1.0");
+ // store your settings
+ }
+
+ void readProperties(java.util.Properties p) {
+ String version = p.getProperty("version");
+ // read your settings according to their version
+ }
+
+ @Override
+ public boolean isMultiwordQuery() {
+ KeywordSearchTopComponentInterface selected = (KeywordSearchTopComponentInterface) tabs.getSelectedComponent();
+ if (selected == null) {
+ return false;
+ }
+ return selected.isMultiwordQuery();
+ }
+
+ @Override
+ public void addSearchButtonListener(ActionListener l) {
+ final int tabsCount = tabs.getTabCount();
+ for (int i = 0; i < tabsCount; ++i) {
+ KeywordSearchTopComponentInterface ks = (KeywordSearchTopComponentInterface) tabs.getComponentAt(i);
+ ks.addSearchButtonListener(l);
+ }
+ }
+
+ @Override
+ public String getQueryText() {
+ KeywordSearchTopComponentInterface selected = (KeywordSearchTopComponentInterface) tabs.getSelectedComponent();
+ if (selected == null) {
+ return "";
+ }
+ return selected.getQueryText();
+ }
+
+ @Override
+ public Map getQueryList() {
+ KeywordSearchTopComponentInterface selected = (KeywordSearchTopComponentInterface) tabs.getSelectedComponent();
+ if (selected == null) {
+ return null;
+ }
+ return selected.getQueryList();
+ }
+
+
+
+ @Override
+ public boolean isLuceneQuerySelected() {
+ KeywordSearchTopComponentInterface selected = (KeywordSearchTopComponentInterface) tabs.getSelectedComponent();
+ if (selected == null) {
+ return false;
+ }
+ return selected.isLuceneQuerySelected();
+ }
+
+ @Override
+ public boolean isRegexQuerySelected() {
+ KeywordSearchTopComponentInterface selected = (KeywordSearchTopComponentInterface) tabs.getSelectedComponent();
+ if (selected == null) {
+ return false;
+ }
+ return selected.isRegexQuerySelected();
+ }
+
+ @Override
+ public void setFilesIndexed(int filesIndexed) {
+ final int tabsCount = tabs.getTabCount();
+ for (int i = 0; i < tabsCount; ++i) {
+ KeywordSearchTopComponentInterface ks = (KeywordSearchTopComponentInterface) tabs.getComponentAt(i);
+ ks.setFilesIndexed(filesIndexed);
+ }
+
+ }
+
+ class KeywordSearchServerListener implements PropertyChangeListener {
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ String eventType = evt.getPropertyName();
+
+ if (eventType.equals(Server.CORE_EVT)) {
+ final Server.CORE_EVT_STATES state = (Server.CORE_EVT_STATES) evt.getNewValue();
+ switch (state) {
+ case STARTED:
+ try {
+ final int numIndexedFiles = KeywordSearch.getServer().getCore().queryNumIndexedFiles();
+ KeywordSearch.changeSupport.firePropertyChange(KeywordSearch.NUM_FILES_CHANGE_EVT, null, new Integer(numIndexedFiles));
+ //setFilesIndexed(numIndexedFiles);
+ } catch (SolrServerException se) {
+ logger.log(Level.SEVERE, "Error executing Solr query, " + se.getMessage());
+ }
+ break;
+ case STOPPED:
+ break;
+ default:
+
+ }
+ }
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTopComponentInterface.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTopComponentInterface.java
new file mode 100644
index 0000000000..83de113481
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchTopComponentInterface.java
@@ -0,0 +1,39 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.event.ActionListener;
+import java.util.Map;
+
+
+/**
+ * common methods for the KeywordSearch TCs / tabs
+ *
+ */
+public interface KeywordSearchTopComponentInterface {
+
+ boolean isMultiwordQuery();
+ boolean isLuceneQuerySelected();
+ boolean isRegexQuerySelected();
+ String getQueryText();
+ Map getQueryList();
+ void setFilesIndexed(int filesIndexed);
+ void addSearchButtonListener(ActionListener l);
+
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchUtil.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchUtil.java
new file mode 100755
index 0000000000..2940696605
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.awt.Component;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.JOptionPane;
+import org.sleuthkit.datamodel.Directory;
+import org.sleuthkit.datamodel.FsContent;
+import org.sleuthkit.datamodel.TskException;
+
+public class KeywordSearchUtil {
+
+ public enum DIALOG_MESSAGE_TYPE {
+
+ ERROR, WARN, INFO
+ };
+ private static final Logger logger = Logger.getLogger(KeywordSearchUtil.class.getName());
+
+ public static String buildDirName(FsContent f) {
+
+ String dirName = null;
+ StringBuilder dirNameB = new StringBuilder();
+ try {
+
+ Directory pd = f.getParentDirectory();
+
+ while (pd != null && pd.isRoot() == false) {
+ dirNameB.insert(0, "/");
+ dirNameB.insert(0, pd.getName());
+ pd = pd.getParentDirectory();
+ }
+ dirNameB.insert(0, "/");
+
+ } catch (TskException ex) {
+ logger.log(Level.WARNING, "Error getting path for fscontent id: " + Long.toString(f.getId()), ex);
+ } finally {
+ dirName = dirNameB.toString();
+ }
+ return dirName;
+ }
+
+ /**
+ * Perform standard escaping / encoding into UTF-8 before sending over net
+ * @param query to be encoded
+ * @param escapeLuceneChars if true perform first escaping of Lucene specific special chars
+ * such as /+-&|!(){}[]^"~*?:\ and treat the whole query as literal word
+ * @return encoded query
+ */
+ public static String escapeLuceneQuery(String query, boolean escapeLuceneChars, boolean encode) {
+ String queryEscaped = null;
+ String inputString = query;
+
+ if (escapeLuceneChars == true) {
+ final String ESCAPE_CHARS = "/+-&|!(){}[]^\"~*?:\\";
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < inputString.length(); ++i) {
+ char c = inputString.charAt(i);
+ if (ESCAPE_CHARS.contains(Character.toString(c))) {
+ sb.append("\\");
+ }
+ sb.append(c);
+ }
+ queryEscaped = inputString = sb.toString();
+ }
+
+ if (encode) {
+ try {
+ queryEscaped = URLEncoder.encode(inputString, "UTF-8");
+ } catch (UnsupportedEncodingException ex) {
+ logger.log(Level.SEVERE, "Error escaping URL query, should not happen.", ex);
+ queryEscaped = query;
+ }
+ }
+ return queryEscaped;
+ }
+
+ public static void displayDialog(final String title, final String message, final DIALOG_MESSAGE_TYPE type) {
+ int messageType;
+ if (type == DIALOG_MESSAGE_TYPE.ERROR) {
+ messageType = JOptionPane.ERROR_MESSAGE;
+ } else if (type == DIALOG_MESSAGE_TYPE.WARN) {
+ messageType = JOptionPane.WARNING_MESSAGE;
+ } else {
+ messageType = JOptionPane.INFORMATION_MESSAGE;
+ }
+
+ final Component parentComponent = null; // Use default window frame.
+ JOptionPane.showMessageDialog(
+ parentComponent,
+ message,
+ title,
+ messageType);
+ }
+
+ public static boolean displayConfirmDialog(final String title, final String message, final DIALOG_MESSAGE_TYPE type) {
+ int messageType;
+ if (type == DIALOG_MESSAGE_TYPE.ERROR) {
+ messageType = JOptionPane.ERROR_MESSAGE;
+ } else if (type == DIALOG_MESSAGE_TYPE.WARN) {
+ messageType = JOptionPane.WARNING_MESSAGE;
+ } else {
+ messageType = JOptionPane.INFORMATION_MESSAGE;
+ }
+ if (JOptionPane.showConfirmDialog(null, message, title, JOptionPane.YES_NO_OPTION, messageType) == JOptionPane.YES_OPTION) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java
new file mode 100644
index 0000000000..29e199b9e4
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java
@@ -0,0 +1,152 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrRequest.METHOD;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.TermsResponse.Term;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.openide.nodes.Node;
+import org.openide.windows.TopComponent;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent;
+import org.sleuthkit.autopsy.corecomponents.TableFilterNode;
+import org.sleuthkit.datamodel.FsContent;
+import org.sleuthkit.datamodel.SleuthkitCase;
+
+public class LuceneQuery implements KeywordSearchQuery {
+
+ private static final Logger logger = Logger.getLogger(LuceneQuery.class.getName());
+ private String query; //original unescaped query
+ private String queryEscaped;
+ private boolean isEscaped;
+
+ public LuceneQuery(String query) {
+ this.query = query;
+ this.queryEscaped = query;
+ isEscaped = false;
+ }
+
+ @Override
+ public void escape() {
+ queryEscaped = KeywordSearchUtil.escapeLuceneQuery(query, true, true);
+ isEscaped = true;
+ }
+
+ @Override
+ public String getEscapedQueryString() {
+ return this.queryEscaped;
+ }
+
+ @Override
+ public String getQueryString() {
+ return this.query;
+ }
+
+ @Override
+ public CollectiongetTerms() {
+ return null;
+ }
+
+ /**
+ * Just perform the query and return result without updating the GUI
+ * This utility is used in this class, can be potentially reused by other classes
+ * @param query
+ * @return matches List
+ */
+ @Override
+ public List performQuery() throws RuntimeException {
+ List matches = new ArrayList();
+
+ boolean allMatchesFetched = false;
+ final int ROWS_PER_FETCH = 10000;
+
+ Server.Core solrCore = KeywordSearch.getServer().getCore();
+
+ SolrQuery q = new SolrQuery();
+
+ q.setQuery(queryEscaped);
+ q.setRows(ROWS_PER_FETCH);
+ q.setFields("id");
+
+ for (int start = 0; !allMatchesFetched; start = start + ROWS_PER_FETCH) {
+
+ q.setStart(start);
+
+ try {
+ QueryResponse response = solrCore.query(q, METHOD.POST);
+ SolrDocumentList resultList = response.getResults();
+ long results = resultList.getNumFound();
+
+ allMatchesFetched = start + ROWS_PER_FETCH >= results;
+
+ for (SolrDocument resultDoc : resultList) {
+ long id = Long.parseLong((String) resultDoc.getFieldValue("id"));
+
+ SleuthkitCase sc = Case.getCurrentCase().getSleuthkitCase();
+
+ // TODO: has to be a better way to get files. Also, need to
+ // check that we actually get 1 hit for each id
+ ResultSet rs = sc.runQuery("select * from tsk_files where obj_id=" + id);
+ matches.addAll(sc.resultSetToFsContents(rs));
+ rs.close();
+ }
+
+ } catch (SolrServerException ex) {
+ logger.log(Level.WARNING, "Error executing Lucene Solr Query: " + query.substring(0,Math.min(query.length()-1, 200)), ex);
+ throw new RuntimeException(ex);
+ // TODO: handle bad query strings, among other issues
+ } catch (SQLException ex) {
+ logger.log(Level.WARNING, "Error interpreting results from Lucene Solr Query: " + query, ex);
+ }
+
+ }
+ return matches;
+ }
+
+ @Override
+ public void execute() {
+ escape();
+ List matches = performQuery();
+
+ String pathText = "Keyword query: " + query;
+
+ Node rootNode = new KeywordSearchNode(matches, query);
+ Node filteredRootNode = new TableFilterNode(rootNode, true);
+
+ TopComponent searchResultWin = DataResultTopComponent.createInstance("Keyword search", pathText, filteredRootNode, matches.size());
+ searchResultWin.requestActive(); // make it the active top component
+ }
+
+ @Override
+ public boolean validate() {
+ return query != null && ! query.equals("");
+ }
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java
new file mode 100644
index 0000000000..ae5b9c9456
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/MarkupSource.java
@@ -0,0 +1,56 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+/**
+ * Interface to provide HTML markup (to be displayed in ExtractedContentViewer)
+ * in a Node's lookup
+ */
+public interface MarkupSource {
+
+ /**
+ * @return text optionally marked up with the subsest of HTML that Swing
+ * components can handle in their setText() method.
+ */
+ String getMarkup();
+
+ /**
+ *
+ * @return true if markup is marked to be searchable
+ */
+ boolean isSearchable();
+
+ /**
+ * If searchable markup, returns prefix of anchor, otherwise return empty string
+ * @return
+ */
+ String getAnchorPrefix();
+
+ /**
+ * if searchable markup, returns number of hits found and encoded in the markup
+ * @return
+ */
+ int getNumberHits();
+
+ /**
+ * @return title of markup source
+ */
+ @Override
+ String toString();
+}
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ReadContentInputStream.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ReadContentInputStream.java
new file mode 100644
index 0000000000..9964cae612
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ReadContentInputStream.java
@@ -0,0 +1,90 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.TskException;
+
+/**
+ * InputStream to read bytes from a Content object's data
+ */
+class ReadContentInputStream extends InputStream {
+
+ private long position;
+ private long length;
+ private Content content;
+
+ ReadContentInputStream(Content content) {
+ this.content = content;
+ this.position = 0;
+ this.length = content.getSize();
+ }
+
+ @Override
+ public int read() throws IOException {
+ byte[] buff = new byte[1];
+ return (read(buff) != -1) ? buff[0] : -1;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+
+ // must return 0 for zero-length arrays
+ if (b.length == 0) {
+ return 0;
+ }
+
+ // will get an error from TSK if we try to read an empty file
+ if (this.length == 0) {
+ return -1;
+ }
+
+ if (position < length) {
+ // data remains to be read
+
+ int lenToRead = (int) Math.min(len, length - position);
+
+ try {
+ byte[] buff = content.read(position, lenToRead);
+ int lenRead = buff.length;
+
+ if (lenRead == 0) {
+ // TSK could not read the whole file, ending partway
+ return -1;
+ } else {
+ System.arraycopy(buff, 0, b, off, lenRead);
+ position += lenRead;
+ return lenRead;
+ }
+ } catch (TskException ex) {
+ throw new IOException(ex);
+ }
+ } else {
+ // at end of file
+ return -1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
new file mode 100755
index 0000000000..82569d645b
--- /dev/null
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
@@ -0,0 +1,333 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011 Basis Technology Corp.
+ * Contact: carrier