diff --git a/.gitignore b/.gitignore index 53f67fc0b3..390529f34e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ /DataModel/release/modules/lib/libewf.dll /DataModel/release/modules/lib/libtsk_jni.dll /DataModel/release/modules/lib/zlib1.dll +/KeywordSearch/release/modules/ext/ +/KeywordSearch/release/solr/lib/ +/KeywordSearch/release/solr/solr/lib/ +/KeywordSearch/release/solr/start.jar +/KeywordSearch/release/solr/webapps/solr.war diff --git a/Case/nbproject/genfiles.properties b/Case/nbproject/genfiles.properties index 5938bebd5c..9c721222f6 100644 --- a/Case/nbproject/genfiles.properties +++ b/Case/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=8710dca5 +build.xml.data.CRC32=a2330d9e build.xml.script.CRC32=601bc2ba -build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +build.xml.stylesheet.CRC32=a56c6a5b@2.47.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=8710dca5 +nbproject/build-impl.xml.data.CRC32=a2330d9e nbproject/build-impl.xml.script.CRC32=65e93a36 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/Case/nbproject/project.xml b/Case/nbproject/project.xml index d5a1a2c076..ed0e258923 100644 --- a/Case/nbproject/project.xml +++ b/Case/nbproject/project.xml @@ -105,6 +105,15 @@ 1.0 + + org.sleuthkit.autopsy.coreutils + + + + 0-1 + 0.0 + + org.sleuthkit.autopsy.datamodel @@ -114,15 +123,6 @@ 1.0 - - org.sleuthkit.autopsy.logging - - - - 0 - 0.0 - - diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageAction.java index 5ecd791292..1d5d40ad72 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageAction.java @@ -35,11 +35,13 @@ import org.openide.DialogDisplayer; import org.openide.WizardDescriptor; import org.openide.util.ChangeSupport; import org.openide.util.HelpCtx; +import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.Presenter; import org.openide.util.lookup.ServiceProvider; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; +import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess; /** @@ -57,7 +59,7 @@ public final class AddImageAction extends CallableSystemAction implements Presen // : // String: time zone that the image is from static final String TIMEZONE_PROP = "timeZone"; - // String[]: task to clean up the database file if wizard errors/is cancelled after it is created + // String[]: array of paths to each image selected static final String IMGPATHS_PROP = "imgPaths"; // CleanupTask: task to clean up the database file if wizard errors/is cancelled after it is created static final String IMAGECLEANUPTASK_PROP = "finalFileCleanup"; @@ -65,6 +67,10 @@ public final class AddImageAction extends CallableSystemAction implements Presen static final String IMAGEID_PROP = "imageId"; // AddImageProcess: the next availble id for a new image static final String PROCESS_PROP = "process"; + // boolean: whether or not to index the image in Solr + static final String SOLR_PROP = "indexInSolr"; + // boolean: whether or not to lookup files in the hashDB + static final String LOOKUPFILES_PROP = "lookupFiles"; private WizardDescriptor wizardDescriptor; @@ -103,6 +109,7 @@ public final class AddImageAction extends CallableSystemAction implements Presen wizardDescriptor = new WizardDescriptor(iterator); wizardDescriptor.setTitle("Add Image"); wizardDescriptor.putProperty(NAME, e); + wizardDescriptor.putProperty(SOLR_PROP, false); if (dialog != null) { @@ -111,49 +118,13 @@ public final class AddImageAction extends CallableSystemAction implements Presen dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor); dialog.setVisible(true); dialog.toFront(); - - boolean cancelled = wizardDescriptor.getValue() != WizardDescriptor.FINISH_OPTION; - // @@@ Why don't we commit and revert in the same general area???? - if (!cancelled) { - // commit anything - try { - commitImage(wizardDescriptor); - } catch (Exception ex) { - // Log error/display warning - Logger logger = Logger.getLogger(AddImageAction.class.getName()); - logger.log(Level.SEVERE, "Error adding image to case.", ex); - } - } + // Do any cleanup that needs to happen (potentially: stopping the //add-image process, reverting an image) runCleanupTasks(); - } - - /** - * Commit the finished AddImageProcess, and cancel the CleanupTask that - * would have reverted it. - * @param settings property set to get AddImageProcess and CleanupTask from - * @throws Exception if commit or adding the image to the case failed - */ - private void commitImage(WizardDescriptor settings) throws Exception { - - String[] imgPaths = (String[]) settings.getProperty(AddImageAction.IMGPATHS_PROP); - String timezone = settings.getProperty(AddImageAction.TIMEZONE_PROP).toString(); - - AddImageProcess process = (AddImageProcess) settings.getProperty(PROCESS_PROP); - - try { - long imageId = process.commit(); - Case.getCurrentCase().addImage(imgPaths, imageId, timezone); - } finally { - // Can't bail and revert image add after commit, so disable image cleanup - // task - CleanupTask cleanupImage = (CleanupTask) settings.getProperty(IMAGECLEANUPTASK_PROP); - cleanupImage.disable(); - } - } - + } + /** * Closes the current dialog and wizard, and opens a new one. Used in the * "Add another image" action on the last panel @@ -174,6 +145,10 @@ public final class AddImageAction extends CallableSystemAction implements Presen } }); } + + public interface IndexImageTask { + void runTask(Image newImage); + } /** * This method does nothing. Use the "actionPerformed(ActionEvent e)" instead of this method. diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.form b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.form index fad3b1db95..2bdaad2ac9 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.form +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.form @@ -1,4 +1,4 @@ - +
@@ -23,29 +23,40 @@ - - - - - - - - - - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -73,12 +84,18 @@ - - - + + + - + + + + + + + @@ -201,5 +218,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.java index 8d95761848..04fcf76fbb 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel1.java @@ -23,10 +23,12 @@ import java.io.File; import java.util.Calendar; import java.util.SimpleTimeZone; import java.util.TimeZone; +import javax.swing.JCheckBox; import javax.swing.event.DocumentEvent; import javax.swing.filechooser.FileFilter; import javax.swing.JFileChooser; import javax.swing.JPanel; +import javax.swing.JTextField; import javax.swing.event.DocumentListener; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; @@ -102,6 +104,26 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { return new String[0]; } } + + public JTextField getImagePathTextField() { + return this.imgPathTextField; + } + + public JCheckBox getLookupFilesCheckbox() { + return this.lookupFilesCheckBox; + } + + public Boolean getLookupFilesCheckboxChecked() { + return this.lookupFilesCheckBox.isSelected(); + } + + public JCheckBox getIndexImageCheckbox() { + return this.indexImageCheckbox; + } + + public Boolean getIndexImageCheckboxChecked() { + return this.indexImageCheckbox.isSelected(); + } /** * Gets the type of the image that's selected. @@ -196,6 +218,9 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { jLabel1 = new javax.swing.JLabel(); timeZoneComboBox = new javax.swing.JComboBox(); timeZoneLabel = new javax.swing.JLabel(); + lookupFilesCheckBox = new javax.swing.JCheckBox(); + jLabel2 = new javax.swing.JLabel(); + indexImageCheckbox = new javax.swing.JCheckBox(); org.openide.awt.Mnemonics.setLocalizedText(rawSingle, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.rawSingle.text")); // NOI18N rawSingle.setRequestFocusEnabled(false); @@ -236,7 +261,7 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { } }); - imgInfoLabel.setFont(new java.awt.Font("Tahoma", 1, 14)); + imgInfoLabel.setFont(new java.awt.Font("Tahoma", 1, 14)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(imgInfoLabel, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.imgInfoLabel.text")); // NOI18N jLabel1.setForeground(new java.awt.Color(255, 0, 51)); @@ -246,6 +271,12 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { org.openide.awt.Mnemonics.setLocalizedText(timeZoneLabel, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.timeZoneLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lookupFilesCheckBox, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.lookupFilesCheckBox.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.jLabel2.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(indexImageCheckbox, org.openide.util.NbBundle.getMessage(AddImageVisualPanel1.class, "AddImageVisualPanel1.indexImageCheckbox.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -253,26 +284,34 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(imgTypeLabel) - .addGroup(layout.createSequentialGroup() - .addGap(10, 10, 10) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(rawSplit) - .addComponent(rawSingle) - .addComponent(encase))) - .addComponent(multipleSelectLabel) .addGroup(layout.createSequentialGroup() .addComponent(imgPathLabel) .addGap(18, 18, 18) - .addComponent(imgPathTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 415, Short.MAX_VALUE) + .addComponent(imgPathTextField) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(imgPathBrowserButton)) - .addComponent(imgInfoLabel) - .addComponent(jLabel1) .addGroup(layout.createSequentialGroup() - .addComponent(timeZoneLabel) - .addGap(10, 10, 10) - .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 315, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lookupFilesCheckBox) + .addComponent(imgTypeLabel) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rawSplit) + .addComponent(rawSingle) + .addComponent(encase))) + .addComponent(multipleSelectLabel) + .addComponent(imgInfoLabel) + .addComponent(jLabel1) + .addGroup(layout.createSequentialGroup() + .addComponent(timeZoneLabel) + .addGap(10, 10, 10) + .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 315, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(indexImageCheckbox)) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addContainerGap()) ); layout.setVerticalGroup( @@ -295,11 +334,17 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { .addComponent(imgPathBrowserButton)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(multipleSelectLabel) - .addGap(18, 18, 18) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(timeZoneLabel) - .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 26, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(timeZoneLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lookupFilesCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(indexImageCheckbox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(jLabel1) .addContainerGap()) ); @@ -394,7 +439,10 @@ final class AddImageVisualPanel1 extends JPanel implements DocumentListener { private javax.swing.JLabel imgPathLabel; private static javax.swing.JTextField imgPathTextField; private javax.swing.JLabel imgTypeLabel; + private javax.swing.JCheckBox indexImageCheckbox; private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JCheckBox lookupFilesCheckBox; private javax.swing.JLabel multipleSelectLabel; private static javax.swing.JRadioButton rawSingle; private javax.swing.JRadioButton rawSplit; diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.form b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.form index ddac189bc6..55ecf90507 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.form +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.form @@ -1,4 +1,4 @@ - +
@@ -19,21 +19,16 @@ - - + - - - - + + - - - + - - + + @@ -46,22 +41,12 @@ - - - - - - - - - - - - - - - - + + + + + + @@ -94,16 +79,6 @@ - - - - - - - - - - @@ -111,12 +86,5 @@ - - - - - - - diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.java index 8897c88e22..f6ea72fc69 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel2.java @@ -48,10 +48,6 @@ final class AddImageVisualPanel2 extends JPanel { return "Create Database"; } - public JButton getCrDbButton() { - return this.crDbButton; - } - public JProgressBar getCrDbProgressBar() { return this.crDbProgressBar; } @@ -59,10 +55,6 @@ final class AddImageVisualPanel2 extends JPanel { public JLabel getProgressLabel() { return this.progressLabel; } - - public JCheckBox getLookupFilesCheckbox() { - return this.lookupFilesCheckBox; - } /** * Changes the progress bar text and color. @@ -89,28 +81,17 @@ final class AddImageVisualPanel2 extends JPanel { jLabel5 = new javax.swing.JLabel(); crDbLabel = new javax.swing.JLabel(); progressLabel = new javax.swing.JLabel(); - crDbButton = new javax.swing.JButton(); jLabel1 = new javax.swing.JLabel(); - lookupFilesCheckBox = new javax.swing.JCheckBox(); org.openide.awt.Mnemonics.setLocalizedText(jLabel5, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.jLabel5.text")); // NOI18N - crDbLabel.setFont(new java.awt.Font("Tahoma", 1, 14)); + crDbLabel.setFont(new java.awt.Font("Tahoma", 1, 14)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(crDbLabel, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.crDbLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.progressLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(crDbButton, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.crDbButton.text")); // NOI18N - crDbButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - crDbButtonActionPerformed(evt); - } - }); - org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.jLabel1.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(lookupFilesCheckBox, org.openide.util.NbBundle.getMessage(AddImageVisualPanel2.class, "AddImageVisualPanel2.lookupFilesCheckBox.text")); // NOI18N - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -121,20 +102,12 @@ final class AddImageVisualPanel2 extends JPanel { .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(crDbLabel) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(crDbButton) - .addGap(189, 189, 189)) - .addComponent(jLabel5) - .addComponent(crDbProgressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 292, Short.MAX_VALUE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 272, Short.MAX_VALUE)) - .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 552, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addContainerGap()) - .addGroup(layout.createSequentialGroup() - .addComponent(lookupFilesCheckBox) - .addContainerGap(407, Short.MAX_VALUE)))) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 552, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel5)) + .addGap(0, 0, Short.MAX_VALUE)) + .addComponent(crDbProgressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 568, Short.MAX_VALUE) + .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 568, Short.MAX_VALUE)) + .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -143,39 +116,21 @@ final class AddImageVisualPanel2 extends JPanel { .addComponent(crDbLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(lookupFilesCheckBox) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(80, 80, 80) - .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 12, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(crDbButton) - .addGap(30, 30, 30) - .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(crDbProgressBar, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(18, 18, 18) + .addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(crDbProgressBar, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 12, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents - /** - * When the "Create Database" button is press. It starts the database - * creation process, disables the "Create Database" and "Back" button, and - * sets the progress bar text. - * - * @param evt the action event - */ - private void crDbButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_crDbButtonActionPerformed - }//GEN-LAST:event_crDbButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton crDbButton; private javax.swing.JLabel crDbLabel; private javax.swing.JProgressBar crDbProgressBar; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel5; - private javax.swing.JCheckBox lookupFilesCheckBox; private javax.swing.JLabel progressLabel; // End of variables declaration//GEN-END:variables } diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.form b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.form index 0cb69a29ae..b70dc7abc3 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.form +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.form @@ -1,4 +1,4 @@ - +
@@ -19,9 +19,9 @@ - + @@ -34,9 +34,9 @@ - + - + diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.java index 5357543353..7dd0be2102 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageVisualPanel3.java @@ -77,9 +77,9 @@ final class AddImageVisualPanel3 extends JPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(addImgButton) .addComponent(crDbLabel) - .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 549, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 549, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(addImgButton)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); layout.setVerticalGroup( @@ -91,7 +91,7 @@ final class AddImageVisualPanel3 extends JPanel { .addComponent(jLabel1) .addGap(18, 18, 18) .addComponent(addImgButton) - .addContainerGap(54, Short.MAX_VALUE)) + .addContainerGap(31, Short.MAX_VALUE)) ); }// //GEN-END:initComponents diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java index e3b2a929b1..599a276b09 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardIterator.java @@ -151,6 +151,8 @@ class AddImageWizardIterator implements WizardDescriptor.Iterator, */ private AddImageVisualPanel1 component; private boolean isNextEnable = false; - + private static final String PROP_LASTIMAGE = "LBL_LastImage_PATH"; + // paths to any set hash lookup databases (can be null) + private String NSRLPath, knownBadPath; + /** * Get the visual component for the panel. In this template, the component * is kept separate. This can be more efficient: if the wizard is created @@ -59,7 +72,7 @@ class AddImageWizardPanel1 implements WizardDescriptor.Panel, component.addPropertyChangeListener(this); return component; } - + /** * Help for this panel. When the panel is active, this is used as the help * for the wizard dialog. @@ -158,6 +171,39 @@ class AddImageWizardPanel1 implements WizardDescriptor.Panel, */ @Override public void readSettings(WizardDescriptor settings) { + AddImageVisualPanel1 component = getComponent(); + + // Prepopulate the image directory from the properties file + String lastImageDirectory = AutopsyPropFile.getInstance().getProperty(PROP_LASTIMAGE); + component.getImagePathTextField().setText(lastImageDirectory); + + // Load hash database settings, enable or disable the checkbox + this.NSRLPath = null; + this.knownBadPath = null; + try { + HashDbSettings hashDbs = HashDbSettings.getHashDbSettings(); + this.NSRLPath = hashDbs.getNSRLDatabasePath(); + this.knownBadPath = hashDbs.getKnownBadDatabasePath(); + } catch (IOException ex) { + Log.get(AddImageWizardPanel1.class).log(Level.WARNING, "Couldn't get hash database settings.", ex); + } + JCheckBox lookupFilesCheckbox = component.getLookupFilesCheckbox(); + lookupFilesCheckbox.setSelected(false); + lookupFilesCheckbox.setEnabled(this.NSRLPath != null || this.knownBadPath != null); + + // If there is a process object in the settings, revert it and remove it from the settings + AddImageAction.CleanupTask cleanupTask = (AddImageAction.CleanupTask) settings.getProperty(AddImageAction.IMAGECLEANUPTASK_PROP); + if(cleanupTask != null){ + try{ + cleanupTask.cleanup(); + }catch(Exception ex){ + Logger logger = Logger.getLogger(AddImageWizardPanel1.class.getName()); + logger.log(Level.WARNING, "Error cleaning up image task", ex); + }finally{ + cleanupTask.disable(); + } + } + } /** @@ -173,6 +219,13 @@ class AddImageWizardPanel1 implements WizardDescriptor.Panel, public void storeSettings(WizardDescriptor settings) { settings.putProperty(AddImageAction.IMGPATHS_PROP, getComponent().getImagePaths()); settings.putProperty(AddImageAction.TIMEZONE_PROP, getComponent().getSelectedTimezone()); // store the timezone + settings.putProperty(AddImageAction.LOOKUPFILES_PROP, getComponent().getLookupFilesCheckboxChecked()); + settings.putProperty(AddImageAction.SOLR_PROP, getComponent().getIndexImageCheckboxChecked()); + + // Store the path to the first image selected into the properties file + String firstImage = getComponent().getImagePaths()[0]; + String firstImagePath = firstImage.substring(0, firstImage.lastIndexOf(File.separator)+1); + AutopsyPropFile.getInstance().setProperty(PROP_LASTIMAGE, firstImagePath); } diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel2.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel2.java index 478d5edaee..ae58bae43b 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel2.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel2.java @@ -37,8 +37,8 @@ import javax.swing.event.ChangeListener; import org.openide.WizardDescriptor; import org.openide.util.HelpCtx; import org.openide.util.Lookup; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.autopsy.hashdatabase.HashDbSettings; -import org.sleuthkit.autopsy.logging.Log; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess; import org.sleuthkit.datamodel.TskException; @@ -56,7 +56,9 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { // paths to any set hash lookup databases (can be null) private String NSRLPath, knownBadPath; - + + private boolean lookupFilesCheckboxChecked; + // task that will clean up the created database file if the wizard is cancelled before it finishes private AddImageAction.CleanupTask cleanupImage; // initialized to null in readSettings() @@ -89,7 +91,6 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { public AddImageVisualPanel2 getComponent() { if (component == null) { component = new AddImageVisualPanel2(); - component.getCrDbButton().addActionListener(new CrDbButtonListener()); } return component; } @@ -122,20 +123,11 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { return imgAdded; } - class CrDbButtonListener implements ActionListener { - - @Override - public void actionPerformed(ActionEvent e) { - startAddImage(); - } - } - /** * Creates the database and adds the image to the XML configuration file. * */ private void startAddImage() { - component.getCrDbButton().setEnabled(false); component.getCrDbProgressBar().setIndeterminate(true); component.changeProgressBarTextAndColor("*Adding the image may take some time for large images.", 0, Color.black); @@ -207,38 +199,11 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { imgAdded = false; imgPaths = (String[]) settings.getProperty(AddImageAction.IMGPATHS_PROP); timeZone = settings.getProperty(AddImageAction.TIMEZONE_PROP).toString(); - - // If the user went back after the image was processed, it will be replaced so its cleanup task should be executed & removed - AddImageAction.CleanupTask oldImageCleanup = (AddImageAction.CleanupTask) settings.getProperty(AddImageAction.IMAGECLEANUPTASK_PROP); - if (oldImageCleanup != null) { - try { - oldImageCleanup.cleanup(); - } catch (Exception ex) { - Logger logger = Logger.getLogger(AddImageWizardPanel2.class.getName()); - logger.log(Level.WARNING, "Error removing previously added image", ex); - } finally { - oldImageCleanup.disable(); - } - } + lookupFilesCheckboxChecked = (Boolean) settings.getProperty(AddImageAction.LOOKUPFILES_PROP); component.changeProgressBarTextAndColor("", 0, Color.black); - component.getCrDbButton().setEnabled(true); - - // Load hash database settings - this.NSRLPath = null; - this.knownBadPath = null; - - try { - HashDbSettings hashDbs = Autopsy.getHashDbSettings(); - this.NSRLPath = hashDbs.getNSRLDatabasePath(); - this.knownBadPath = hashDbs.getKnownBadDatabasePath(); - } catch (IOException ex) { - Log.get(AddImageWizardPanel2.class).log(Level.WARNING, "Couldn't get hash database settings.", ex); - } - JCheckBox lookupFilesCheckbox = component.getLookupFilesCheckbox(); - lookupFilesCheckbox.setSelected(false); - lookupFilesCheckbox.setEnabled(this.NSRLPath != null || this.knownBadPath != null); + startAddImage(); } /** @@ -262,7 +227,6 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { */ private class AddImgTask extends SwingWorker { private JProgressBar progressBar; - private JCheckBox lookupFilesCheckbox; private Case currentCase; // true if the process was requested to stop @@ -270,7 +234,6 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { protected AddImgTask() { this.progressBar = getComponent().getCrDbProgressBar(); - this.lookupFilesCheckbox = getComponent().getLookupFilesCheckbox(); currentCase = Case.getCurrentCase(); } @@ -296,7 +259,7 @@ class AddImageWizardPanel2 implements WizardDescriptor.Panel { SleuthkitCase skCase = currentCase.getSleuthkitCase(); skCase.clearLookupDatabases(); - if (lookupFilesCheckbox.isSelected()) { + if (lookupFilesCheckboxChecked) { if (NSRLPath != null) { skCase.setNSRLDatabase(NSRLPath); } diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel3.java b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel3.java index f999115e44..67ec1ecd00 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel3.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/AddImageWizardPanel3.java @@ -20,9 +20,14 @@ package org.sleuthkit.autopsy.casemodule; import java.awt.Component; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.event.ChangeListener; import org.openide.WizardDescriptor; import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitJNI; /** * The "Add Image" wizard panel3. No major functionality, just presents the @@ -140,6 +145,19 @@ class AddImageWizardPanel3 implements WizardDescriptor.Panel { */ @Override public void readSettings(WizardDescriptor settings) { + if ((SleuthkitJNI.CaseDbHandle.AddImageProcess) settings.getProperty(AddImageAction.PROCESS_PROP) != null) { + // commit anything + try { + commitImage(settings); + } catch (Exception ex) { + // Log error/display warning + Logger logger = Logger.getLogger(AddImageAction.class.getName()); + logger.log(Level.SEVERE, "Error adding image to case.", ex); + } + }else{ + Logger logger = Logger.getLogger(AddImageAction.class.getName()); + logger.log(Level.SEVERE, "Missing image process object"); + } } /** @@ -154,4 +172,37 @@ class AddImageWizardPanel3 implements WizardDescriptor.Panel { @Override public void storeSettings(WizardDescriptor settings) { } + + + + /** + * Commit the finished AddImageProcess, and cancel the CleanupTask that + * would have reverted it. + * @param settings property set to get AddImageProcess and CleanupTask from + * @throws Exception if commit or adding the image to the case failed + */ + private void commitImage(WizardDescriptor settings) throws Exception { + + String[] imgPaths = (String[]) settings.getProperty(AddImageAction.IMGPATHS_PROP); + String timezone = settings.getProperty(AddImageAction.TIMEZONE_PROP).toString(); + boolean indexImage = (Boolean) settings.getProperty(AddImageAction.SOLR_PROP); + + SleuthkitJNI.CaseDbHandle.AddImageProcess process = (SleuthkitJNI.CaseDbHandle.AddImageProcess) settings.getProperty(AddImageAction.PROCESS_PROP); + + try { + long imageId = process.commit(); + Image newImage = Case.getCurrentCase().addImage(imgPaths, imageId, timezone); + + if (indexImage) { + // Must use a Lookup here to prevent a circular dependency + // between Case and KeywordSearch... + Lookup.getDefault().lookup(AddImageAction.IndexImageTask.class).runTask(newImage); + } + } finally { + // Can't bail and revert image add after commit, so disable image cleanup + // task + AddImageAction.CleanupTask cleanupImage = (AddImageAction.CleanupTask) settings.getProperty(AddImageAction.IMAGECLEANUPTASK_PROP); + cleanupImage.disable(); + } + } } diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/Autopsy.java b/Case/src/org/sleuthkit/autopsy/casemodule/Autopsy.java deleted file mode 100644 index 95bc01e3e4..0000000000 --- a/Case/src/org/sleuthkit/autopsy/casemodule/Autopsy.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.casemodule; - -import java.io.File; -import java.io.IOException; -import org.sleuthkit.autopsy.hashdatabase.HashDbSettings; -import org.sleuthkit.datamodel.SleuthkitJNI; - -/** - * Class to consolidate application-wide settings. - */ -public class Autopsy { - private final static String propFilePath = System.getProperty("netbeans.user") + File.separator + "autopsy.properties"; - private static boolean verboseLogging = false; - - /** - * Gets the property file where the user properties such as Recent Cases - * and selected Hash Databases are stored. - * @return A new file handle - */ - public static File getPropertyFile() { - return new File(propFilePath); - } - - /** - * Get the hash database settings as read from the property file. - * @return A new hash database settings object. - * @throws IOException if the property file can't be found - */ - public static HashDbSettings getHashDbSettings() throws IOException { - return new HashDbSettings(getPropertyFile()); - } - - /** - * Activate verbose logging for Sleuth Kit - */ - public static void startVerboseLogging() { - verboseLogging = true; - String logPath = System.getProperty("netbeans.user") + File.separator + "sleuthkit.txt"; - - SleuthkitJNI.startVerboseLogging(logPath); - } - - /** - * Checks if verbose logging has been enabled. - * @return true if verbose logging has been enabled. - */ - public static boolean verboseLoggingIsSet() { - return verboseLogging; - } - -} diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Case/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index d7d06b1e08..2f2ebeef2d 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Case/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -90,7 +90,6 @@ OpenRecentCasePanel.jLabel1.text=Recent Cases AddImageVisualPanel1.imgInfoLabel.text=Enter Disk Image Information: AddImageVisualPanel2.crDbLabel.text=Adding Image AddImageVisualPanel2.jLabel5.text=Processing Image and Adding to Database : -AddImageVisualPanel2.crDbButton.text=Process Image AddImageVisualPanel2.progressLabel.text= AddImageVisualPanel2.jLabel1.text= We will now analyze the disk image to extract volume and file system data and populate a local database. This may
take a while on large images. AddImageVisualPanel3.addImgButton.text=Add Another Image @@ -112,4 +111,7 @@ NewJPanel.jLabel5.text=The NSRL index is used to idenify "known" files. NewJPanel.jFormattedTextField1.text=jFormattedTextField1 NewJPanel.jButton1.text=Rename NewJPanel.jLabel4.text=Database: -AddImageVisualPanel2.lookupFilesCheckBox.text=Lookup files in hash databases +AddImageVisualPanel2.indexImageCheckBox.text=Index image for keyword search +AddImageVisualPanel1.lookupFilesCheckBox.text=Lookup files in hash databases +AddImageVisualPanel1.jLabel2.text= Press 'Next' to analyze the disk image to extract volume and file system data and populate a local database. This may
take a while on large images. +AddImageVisualPanel1.indexImageCheckbox.text=Index Image for Keyword Search diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/Case.java b/Case/src/org/sleuthkit/autopsy/casemodule/Case.java old mode 100644 new mode 100755 index ebc3ae8a29..fd2aafca01 --- a/Case/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -38,6 +38,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; +import javax.swing.SwingUtilities; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.util.Lookup; @@ -45,7 +46,7 @@ import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.SystemAction; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.corecomponentinterfaces.CoreComponentControl; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.*; import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess; @@ -230,7 +231,7 @@ public class Case { * @param imgId the ID of the image that being added * @param timeZone the timeZone of the image where it's added */ - void addImage(String[] imgPaths, long imgId, String timeZone) throws Exception { + Image addImage(String[] imgPaths, long imgId, String timeZone) throws Exception { Log.get(this.getClass()).log(Level.INFO, "Adding image to Case. imgPaths: {0} ID: {1} TimeZone: {2}", new Object[]{Arrays.toString(imgPaths), imgId, timeZone}); try { @@ -238,6 +239,7 @@ public class Case { xmlcm.writeFile(); // write any changes to the config file Image newImage = db.getImageById(imgId); pcs.firePropertyChange(CASE_ADD_IMAGE, null, newImage); // the new value is the instance of the image + return newImage; } catch (Exception ex) { // throw an error here throw ex; @@ -714,7 +716,7 @@ public class Case { * Call if there are no images in the case. Displays * a dialog offering to add one. */ - private void noRootObjectsNotification() throws TskException { + private static void noRootObjectsNotification() { NotifyDescriptor nd = new NotifyDescriptor( "This case contains no images. Would you like to add one?", "No images in case", NotifyDescriptor.YES_NO_OPTION, @@ -775,48 +777,47 @@ public class Case { Object oldValue = evt.getOldValue(); Object newValue = evt.getNewValue(); - if (changed.equals(Case.CASE_CURRENT_CASE)) { - try { - if (newValue != null) { // new case is open - Case newCase = (Case) newValue; + if (newValue != null) { // new case is open + Case newCase = (Case) newValue; - // clear the temp folder when the case is created / opened - Case.clearTempFolder(); + // clear the temp folder when the case is created / opened + Case.clearTempFolder(); - // enable these menus - CallableSystemAction.get(AddImageAction.class).setEnabled(true); - CallableSystemAction.get(CaseCloseAction.class).setEnabled(true); - CallableSystemAction.get(CasePropertiesAction.class).setEnabled(true); - CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); // Delete Case menu - - if (newCase.getRootObjectsCount() > 0) { - // open all top components - CoreComponentControl.openCoreWindows(); - } else { - // close all top components - CoreComponentControl.closeCoreWindows(); - // notify user - newCase.noRootObjectsNotification(); - } - } else { // case is closed - // disable these menus - CallableSystemAction.get(AddImageAction.class).setEnabled(false); // Add Image menu - CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); // Case Close menu - CallableSystemAction.get(CasePropertiesAction.class).setEnabled(false); // Case Properties menu - CallableSystemAction.get(CaseDeleteAction.class).setEnabled(false); // Delete Case menu + // enable these menus + CallableSystemAction.get(AddImageAction.class).setEnabled(true); + CallableSystemAction.get(CaseCloseAction.class).setEnabled(true); + CallableSystemAction.get(CasePropertiesAction.class).setEnabled(true); + CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); // Delete Case menu + if (newCase.getRootObjectsCount() > 0) { + // open all top components + CoreComponentControl.openCoreWindows(); + } else { // close all top components CoreComponentControl.closeCoreWindows(); - - Frame f = WindowManager.getDefault().getMainWindow(); - f.setTitle(Case.getAppName()); // set the window name to just application name + // prompt user to add an image + SwingUtilities.invokeLater(new Runnable() { + public void run() { + Case.noRootObjectsNotification(); + } + }); } - } catch (TskException ex) { - Log.get(CaseListener.class).log(Level.WARNING, "Error handling change in current case.", ex); - } - } + } else { // case is closed + // disable these menus + CallableSystemAction.get(AddImageAction.class).setEnabled(false); // Add Image menu + CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); // Case Close menu + CallableSystemAction.get(CasePropertiesAction.class).setEnabled(false); // Case Properties menu + CallableSystemAction.get(CaseDeleteAction.class).setEnabled(false); // Delete Case menu + // close all top components + CoreComponentControl.closeCoreWindows(); + + Frame f = WindowManager.getDefault().getMainWindow(); + f.setTitle(Case.getAppName()); // set the window name to just application name + } + + } // changed in the case name if (changed.equals(Case.CASE_NAME)) { diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java index fdfa3be36b..43021690b3 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java @@ -31,7 +31,7 @@ import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.Presenter; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * The action to close the current Case. This class should be disabled on diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java index 523b6c6249..7a1b6cee94 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java @@ -32,7 +32,7 @@ import org.openide.NotifyDescriptor; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * The action to delete the current Case. This class should be disabled on diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java index 89d51ea2a8..49c2b1c6e4 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java @@ -23,7 +23,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import org.openide.util.actions.SystemAction; import org.openide.util.lookup.ServiceProvider; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * The action to create a new case. This action class is always enabled. diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index 295a01cfc2..6f3fc7a762 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -28,7 +28,8 @@ import java.util.logging.Logger; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import org.openide.util.lookup.ServiceProvider; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.AutopsyPropFile; +import org.sleuthkit.autopsy.coreutils.Log; /** * The action to open a existing case. This class is always enabled. @@ -38,6 +39,8 @@ import org.sleuthkit.autopsy.logging.Log; @ServiceProvider(service = CaseOpenAction.class) public final class CaseOpenAction implements ActionListener { private static final Logger logger = Logger.getLogger(CaseOpenAction.class.getName()); + private static final String PROP_BASECASE = "LBL_BaseCase_PATH"; + AutopsyPropFile AutopsyProperties = AutopsyPropFile.getInstance(); JFileChooser fc = new JFileChooser(); GeneralFilter autFilter = new GeneralFilter(new String[]{".aut"}, "AUTOPSY File (*.aut)", false); @@ -48,6 +51,8 @@ public final class CaseOpenAction implements ActionListener { fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.setMultiSelectionEnabled(false); fc.addChoosableFileFilter(autFilter); + if(AutopsyProperties.getProperty(PROP_BASECASE) != null) + fc.setCurrentDirectory(new File(AutopsyProperties.getProperty(PROP_BASECASE))); } /** @@ -63,7 +68,8 @@ public final class CaseOpenAction implements ActionListener { int retval = fc.showOpenDialog((Component) e.getSource()); if (retval == JFileChooser.APPROVE_OPTION) { String path = fc.getSelectedFile().getPath(); - + String dirPath = fc.getSelectedFile().getParent(); + AutopsyProperties.setProperty(PROP_BASECASE, dirPath.substring(0, dirPath.lastIndexOf(File.separator))); // check if the file exists if (!new File(path).exists()) { JOptionPane.showMessageDialog(null, "Error: File doesn't exist.", "Error", JOptionPane.ERROR_MESSAGE); diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java index 6a5235e7e9..ecb18ebcf7 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java @@ -31,7 +31,7 @@ import javax.swing.JFrame; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * The action to pop up the Case Properties Form window. By using this form, diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form index 1fb4b5286f..ab1236901b 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form +++ b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form @@ -1,4 +1,4 @@ - + diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java index 111f331134..0ce1e09ddb 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java @@ -23,6 +23,7 @@ import java.awt.Component; import java.io.File; import javax.swing.JFileChooser; import javax.swing.JPanel; +import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -71,6 +72,10 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener{ public String getCaseParentDir(){ return this.caseParentDirTextField.getText(); } + + public JTextField getCaseParentDirTextField(){ + return this.caseParentDirTextField; + } /** 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 @@ -168,7 +173,7 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener{ // show the directory chooser where the case directory will be created fc.setDragEnabled(false); if(!caseParentDirTextField.getText().trim().equals("")){ - fc.setSelectedFile(new File(caseParentDirTextField.getText())); + fc.setCurrentDirectory(new File(caseParentDirTextField.getText())); } fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); //fc.setSelectedFile(new File("C:\\Program Files\\")); diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java index 9475e1e547..84a35fc166 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java @@ -32,7 +32,7 @@ import org.openide.WizardDescriptor; import org.openide.util.HelpCtx; import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.SystemAction; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * Action to open the New Case wizard. diff --git a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java index b28724dabe..814a5e3940 100644 --- a/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java +++ b/Case/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java @@ -33,6 +33,7 @@ import org.openide.NotifyDescriptor; import org.openide.WizardDescriptor; import org.openide.WizardValidationException; import org.openide.util.HelpCtx; +import org.sleuthkit.autopsy.coreutils.AutopsyPropFile; /** * The "New Case" wizard panel with a component on it. This class represents @@ -49,6 +50,7 @@ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel + @@ -20,7 +20,7 @@ - + diff --git a/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbPanel.java b/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbPanel.java index 49f934d9d4..aa45400925 100644 --- a/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbPanel.java +++ b/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbPanel.java @@ -30,7 +30,7 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFileChooser; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.TskException; /** diff --git a/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbSettings.java b/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbSettings.java index 370e039451..189b20e9b8 100644 --- a/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbSettings.java +++ b/Case/src/org/sleuthkit/autopsy/hashdatabase/HashDbSettings.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Properties; +import org.sleuthkit.autopsy.coreutils.AutopsyPropFile; /** * Loads and stores Hash Database settings from/to a property file @@ -110,6 +111,15 @@ public class HashDbSettings { HashDb getNSRLDatabase() { return this.NSRLDatabase; } + + /** + * Get the hash database settings as read from the property file. + * @return A new hash database settings object. + * @throws IOException if the property file can't be found + */ + public static HashDbSettings getHashDbSettings() throws IOException { + return new HashDbSettings(AutopsyPropFile.getPropertyFile()); + } /** * Gets known bad database if there is one diff --git a/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataContentViewer.java b/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataContentViewer.java index 4ee9f424cd..f77479e6ea 100644 --- a/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataContentViewer.java +++ b/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataContentViewer.java @@ -37,6 +37,12 @@ public interface DataContentViewer { * Returns the title of this viewer. */ public String getTitle(); + + /** + * Returns a short description of this viewer to use as a tool tip for + * its tab. + */ + public String getToolTip(); /** * Get new DataContentViewer instance. (This method is weird. We use the @@ -62,5 +68,14 @@ public interface DataContentViewer { * @return True if supported, else false */ public boolean isSupported(Node node); + + /** + * Checks whether the given viewer is preferred for the Node + * @param node Node to check for preference + * @param isSupported, true if the viewer is supported by the node + * as determined by a previous check + * @return True if viewer preferred, else false + */ + public boolean isPreferred(Node node, boolean isSupported); } diff --git a/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataResultViewer.java b/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataResultViewer.java index 0e9b73df79..d8ada5858a 100644 --- a/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataResultViewer.java +++ b/CoreComponentInterfaces/src/org/sleuthkit/autopsy/corecomponentinterfaces/DataResultViewer.java @@ -61,4 +61,10 @@ public interface DataResultViewer { * preparation for permanently disposing of it. */ public void clearComponent(); + + /** + * Expand node, if supported by the viewed + * @param n Node to expand + */ + public void expandNode(Node n); } diff --git a/CoreComponents/nbproject/genfiles.properties b/CoreComponents/nbproject/genfiles.properties index e60c0a9b59..b85a42ccb6 100644 --- a/CoreComponents/nbproject/genfiles.properties +++ b/CoreComponents/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=8f8b60b5 +build.xml.data.CRC32=f7f11023 build.xml.script.CRC32=d7506201 -build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +build.xml.stylesheet.CRC32=a56c6a5b@2.47.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=8f8b60b5 +nbproject/build-impl.xml.data.CRC32=f7f11023 nbproject/build-impl.xml.script.CRC32=c3845be2 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/CoreComponents/nbproject/project.xml b/CoreComponents/nbproject/project.xml index bec41115ad..93290ef079 100644 --- a/CoreComponents/nbproject/project.xml +++ b/CoreComponents/nbproject/project.xml @@ -132,6 +132,15 @@ 1.0 + + org.sleuthkit.autopsy.coreutils + + + + 0-1 + 0.0 + + org.sleuthkit.autopsy.datamodel @@ -141,15 +150,6 @@ 1.0 - - org.sleuthkit.autopsy.logging - - - - 0-1 - 0.0 - - org.sleuthkit.autopsy.corecomponents diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/CustomAboutAction.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/CustomAboutAction.java index fd691327ca..32c6cbdb5a 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/CustomAboutAction.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/CustomAboutAction.java @@ -23,7 +23,7 @@ import org.openide.util.NbBundle; import org.netbeans.core.actions.AboutAction; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * Action to open custom implementation of the "About" window from the Help menu. diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentTopComponent.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentTopComponent.java old mode 100644 new mode 100755 index 9a50602602..f86ac48eb5 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentTopComponent.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentTopComponent.java @@ -97,6 +97,10 @@ public final class DataContentTopComponent extends TopComponent implements DataC boolean isSupported(Node node) { return this.wrapped.isSupported(node); } + + boolean isPreferred(Node node, boolean isSupported) { + return this.wrapped.isPreferred(node, isSupported); + } } /** @@ -191,11 +195,12 @@ public final class DataContentTopComponent extends TopComponent implements DataC for (DataContentViewer factory : Lookup.getDefault().lookupAll(DataContentViewer.class)) { DataContentViewer dcv = factory.getInstance(); this.viewers.add(new UpdateWrapper(dcv)); - dataContentTabbedPane.addTab(dcv.getTitle(), dcv.getComponent()); + dataContentTabbedPane.addTab(dcv.getTitle(), null, + dcv.getComponent(), dcv.getToolTip()); } } - resetTabs(currentNode); + setupTabs(currentNode); } @Override @@ -243,14 +248,7 @@ public final class DataContentTopComponent extends TopComponent implements DataC currentNode = selectedNode; - resetTabs(selectedNode); - - // set the display on the current active tab - int currentActiveTab = dataContentTabbedPane.getSelectedIndex(); - if (currentActiveTab != -1) { - UpdateWrapper dcv = viewers.get(currentActiveTab); - dcv.setNode(selectedNode); - } + setupTabs(selectedNode); } finally { this.setCursor(null); } @@ -291,7 +289,7 @@ public final class DataContentTopComponent extends TopComponent implements DataC * * @param selectedNode the selected content Node */ - public void resetTabs(Node selectedNode) { + public void setupTabs(Node selectedNode) { int totalTabs = dataContentTabbedPane.getTabCount(); @@ -299,10 +297,11 @@ public final class DataContentTopComponent extends TopComponent implements DataC int tempIndex = dataContentTabbedPane.getSelectedIndex(); for (int i = 0; i < totalTabs; i++) { UpdateWrapper dcv = viewers.get(i); - dcv.resetComponent(); + dcv.resetComponent(); // disable an unsupported tab (ex: picture viewer) - if (!dcv.isSupported(selectedNode)) { + boolean dcvSupported = dcv.isSupported(selectedNode); + if (! dcvSupported) { dataContentTabbedPane.setEnabledAt(i, false); // change the tab selection if it's the current selection @@ -315,6 +314,9 @@ public final class DataContentTopComponent extends TopComponent implements DataC } } else { dataContentTabbedPane.setEnabledAt(i, true); + if (dcv.isPreferred(selectedNode, dcvSupported)) + dataContentTabbedPane.setSelectedIndex(i); + } } int newIndex = dataContentTabbedPane.getSelectedIndex(); diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.form b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.form index d454394bd5..e7147d0e76 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.form +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.form @@ -1,4 +1,4 @@ - + diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java index 9ee484eb72..2507ae331b 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java @@ -223,11 +223,9 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont } // set the data on the bottom and show it - String text = ""; Boolean setVisible = false; if (data != null) { - text = DataConversion.getString(data, 4); setVisible = true; } @@ -288,6 +286,12 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont public String getTitle() { return "Hex View"; } + + @Override + public String getToolTip() { + return "Displays the binary contents of a file as hexidecimal, with " + + "bytes that are displayable as ASCII characters on the right."; + } @Override public DataContentViewer getInstance() { @@ -297,8 +301,8 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont @Override public void resetComponent() { // clear / reset the fields - currentOffset = 0; currentPage = 1; + currentOffset = 0; this.dataSource = null; currentPageLabel.setText(""); totalPageLabel.setText(""); @@ -326,6 +330,11 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont public boolean isSupported(Node node) { return true; } + + @Override + public boolean isPreferred(Node node, boolean isSupported) { + return false; + } @Override public Component getComponent() { diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerPicture.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerPicture.java index 3a7aeaddc4..c89d18bf84 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerPicture.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerPicture.java @@ -118,6 +118,11 @@ public class DataContentViewerPicture extends javax.swing.JPanel implements Data public String getTitle() { return "Picture View"; } + + @Override + public String getToolTip() { + return "Displays supported image files."; + } @Override public DataContentViewer getInstance() { @@ -148,6 +153,11 @@ public class DataContentViewerPicture extends javax.swing.JPanel implements Data return false; } } + + @Override + public boolean isPreferred(Node node, boolean isSupported) { + return isSupported; + } @Override public Component getComponent() { diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java index 94c0eaf2f7..422258f8de 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java @@ -282,6 +282,11 @@ public class DataContentViewerString extends javax.swing.JPanel implements DataC public String getTitle() { return "String View"; } + + @Override + public String getToolTip() { + return "Displays ASCII strings extracted from the file."; + } @Override public DataContentViewer getInstance() { @@ -291,8 +296,8 @@ public class DataContentViewerString extends javax.swing.JPanel implements DataC @Override public void resetComponent() { // clear / reset the fields - currentOffset = 0; currentPage = 1; + currentOffset = 0; this.dataSource = null; currentPageLabel.setText(""); totalPageLabel.setText(""); @@ -305,6 +310,11 @@ public class DataContentViewerString extends javax.swing.JPanel implements DataC public boolean isSupported(Node node) { return true; } + + @Override + public boolean isPreferred(Node node, boolean isSupported) { + return false; + } @Override public Component getComponent() { diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index bedf96df7c..ff9ff4aa8a 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -25,7 +25,9 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import javax.swing.JTable; import javax.swing.ListSelectionModel; import org.openide.explorer.ExplorerManager; @@ -36,6 +38,7 @@ import org.openide.nodes.Node; import org.openide.nodes.Node.Property; import org.openide.nodes.Node.PropertySet; import org.openide.nodes.Sheet; +import org.openide.util.lookup.AbstractLookup; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; @@ -62,6 +65,18 @@ public class DataResultViewerTable extends AbstractDataResultViewer { this.em.addPropertyChangeListener(this); } + + /** + * Expand node + * @param n Node to expand + */ + @Override + public void expandNode(Node n) { + if ( this.tableScrollPanel != null) { + OutlineView ov = ((OutlineView) this.tableScrollPanel); + ov.expandNode(n); + } + } /** This method is called from within the constructor to * initialize the form. @@ -135,6 +150,41 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } + /** + * Gets regular Bean property set properties from all first children and, recursively, subchildren of Node. + * Note: won't work out the box for lazy load - you need to set all children props for the parent by hand + * @param parent Node with at least one child to get properties from + * @return Properties, + */ + private Node.Property[] getAllChildPropertyHeaders(Node parent) { + Node firstChild = parent.getChildren().getNodeAt(0); + + Property[] properties = null; + + if (firstChild == null) { + throw new IllegalArgumentException("Couldn't get a child Node from the given parent."); + } else { + Set allProperties = new LinkedHashSet(); + while (firstChild != null) { + for (PropertySet ps : firstChild.getPropertySets()) { + //if (ps.getName().equals(Sheet.PROPERTIES)) { + //return ps.getProperties(); + final Property [] props = ps.getProperties(); + final int propsNum = props.length; + for (int i = 0; i< propsNum; ++i) + allProperties.add(props[i]); + //} + } + firstChild = firstChild.getChildren().getNodeAt(0); + } + + properties = allProperties.toArray(new Property[0]); + //throw new IllegalArgumentException("Child Node doesn't have the regular PropertySet."); + } + return properties; + + } + @Override public void setNode(Node selectedNode) { // change the cursor to "waiting cursor" for this operation @@ -152,15 +202,18 @@ public class DataResultViewerTable extends AbstractDataResultViewer { if (hasChildren) { Node root = selectedNode; - if (!(root instanceof TableFilterNode)) { - root = new TableFilterNode(root, true); - } + //wrap to filter out children + //note: this breaks the tree view mode in this generic viewer, + //so wrap nodes earlier if want 1 level view + //if (!(root instanceof TableFilterNode)) { + /// root = new TableFilterNode(root, true); + //} em.setRootContext(root); OutlineView ov = ((OutlineView) this.tableScrollPanel); - List tempProps = new ArrayList(Arrays.asList(getChildPropertyHeaders(selectedNode))); + List tempProps = new ArrayList(Arrays.asList(getAllChildPropertyHeaders(selectedNode))); tempProps.remove(0); @@ -242,12 +295,11 @@ public class DataResultViewerTable extends AbstractDataResultViewer { this.setCursor(null); } } - - + private static Object[][] getRowValues(Node node, int rows) { // how many rows are we returning int maxRows = Math.min(rows, node.getChildren().getNodesCount()); - + Object[][] objs = new Object[maxRows][]; for (int i = 0; i < maxRows; i++) { @@ -263,7 +315,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { objs[i][j] = "n/a"; } catch (InvocationTargetException ignore) { objs[i][j] = "n/a"; - } + } } } return objs; diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java index b9a1d156ca..bac44db247 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java @@ -91,6 +91,16 @@ public class DataResultViewerThumbnail extends AbstractDataResultViewer { return result; } + + /** + * Expand node + * @param n Node to expand + */ + @Override + public void expandNode(Node n) { + + } + @Override public void setNode(Node givenNode) { // change the cursor to "waiting cursor" for this operation diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/OutputViewPanel.form b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/OutputViewPanel.form index 3ac931b2dc..1e218bb0ad 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/OutputViewPanel.form +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/OutputViewPanel.form @@ -1,4 +1,4 @@ - + @@ -20,7 +20,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.form b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.form index 1479125a3e..22a4af6f58 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.form +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.form @@ -1,4 +1,4 @@ - + diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.java index 49a71ce744..765575a021 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ProductInformationPanel.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.corecomponents; import java.awt.Cursor; import java.awt.Window; +import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; @@ -33,7 +34,7 @@ import javax.swing.event.HyperlinkListener; import org.netbeans.core.actions.HTMLViewAction; import org.openide.awt.HtmlBrowser; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Autopsy; +import org.sleuthkit.autopsy.coreutils.AutopsyPropFile; import org.sleuthkit.datamodel.SleuthkitJNI; /** @@ -43,6 +44,7 @@ class ProductInformationPanel extends JPanel implements HyperlinkListener { private URL url = null; private Icon about; + private boolean verboseLogging; ProductInformationPanel() { about = new ImageIcon(org.netbeans.core.startup.Splash.loadContent(true)); @@ -54,7 +56,7 @@ class ProductInformationPanel extends JPanel implements HyperlinkListener { description.addHyperlinkListener(this); copyright.addHyperlinkListener(this); copyright.setBackground(getBackground()); - if (Autopsy.verboseLoggingIsSet()) { + if (verboseLoggingIsSet()) { disableVerboseLoggingButton(); } @@ -174,7 +176,7 @@ private void jLabel1MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:eve }//GEN-LAST:event_jLabel1MouseClicked private void activateVerboseLogging(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_activateVerboseLogging - Autopsy.startVerboseLogging(); + startVerboseLogging(); disableVerboseLoggingButton(); }//GEN-LAST:event_activateVerboseLogging // Variables declaration - do not modify//GEN-BEGIN:variables @@ -253,4 +255,22 @@ private void jLabel1MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:eve url = null; } } + + /** + * Activate verbose logging for Sleuth Kit + */ + public void startVerboseLogging() { + verboseLogging = true; + String logPath = AutopsyPropFile.getUserDirPath() + File.separator + "sleuthkit.txt"; + + SleuthkitJNI.startVerboseLogging(logPath); + } + + /** + * Checks if verbose logging has been enabled. + * @return true if verbose logging has been enabled. + */ + public boolean verboseLoggingIsSet() { + return verboseLogging; + } } diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java index 4db898654d..535d541a02 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java @@ -27,7 +27,7 @@ import org.openide.nodes.Node; * * @author jantonius */ -class TableFilterNode extends FilterNode { +public class TableFilterNode extends FilterNode { private boolean createChild; diff --git a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java index c356ed3823..6e1916609d 100644 --- a/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java +++ b/CoreComponents/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java @@ -30,7 +30,7 @@ import javax.swing.ImageIcon; import javax.swing.JFrame; import org.openide.nodes.FilterNode; import org.openide.nodes.Node; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskException; diff --git a/Logging/build.xml b/CoreUtils/build.xml similarity index 77% rename from Logging/build.xml rename to CoreUtils/build.xml index fbf2530e25..fbf810a3a4 100644 --- a/Logging/build.xml +++ b/CoreUtils/build.xml @@ -2,7 +2,7 @@ - - Builds, tests, and runs the project org.sleuthkit.autopsy.logging. + + Builds, tests, and runs the project org.sleuthkit.autopsy.coreutils. diff --git a/CoreUtils/manifest.mf b/CoreUtils/manifest.mf new file mode 100644 index 0000000000..2fd5a80363 --- /dev/null +++ b/CoreUtils/manifest.mf @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +OpenIDE-Module: org.sleuthkit.autopsy.coreutils/0 +OpenIDE-Module-Implementation-Version: 2 +OpenIDE-Module-Install: org/sleuthkit/autopsy/coreutils/Installer.class +OpenIDE-Module-Layer: org/sleuthkit/autopsy/coreutils/layer.xml +OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/coreutils/Bundle.properties diff --git a/Logging/nbproject/build-impl.xml b/CoreUtils/nbproject/build-impl.xml similarity index 97% rename from Logging/nbproject/build-impl.xml rename to CoreUtils/nbproject/build-impl.xml index 0b474da861..09e05dfb7f 100644 --- a/Logging/nbproject/build-impl.xml +++ b/CoreUtils/nbproject/build-impl.xml @@ -3,7 +3,7 @@ *** GENERATED FROM project.xml - DO NOT EDIT *** *** EDIT ../build.xml INSTEAD *** --> - + diff --git a/CoreUtils/nbproject/genfiles.properties b/CoreUtils/nbproject/genfiles.properties new file mode 100644 index 0000000000..a97765af1c --- /dev/null +++ b/CoreUtils/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=53a3cee0 +build.xml.script.CRC32=b3e56256 +build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=53a3cee0 +nbproject/build-impl.xml.script.CRC32=849ea2ba +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 diff --git a/CoreUtils/nbproject/platform.properties b/CoreUtils/nbproject/platform.properties new file mode 100644 index 0000000000..38ecd5a92e --- /dev/null +++ b/CoreUtils/nbproject/platform.properties @@ -0,0 +1,99 @@ +cluster.path=\ + ${nbplatform.active.dir}/java:\ + ${nbplatform.active.dir}/platform +disabled.modules=\ + org.apache.tools.ant.module,\ + org.netbeans.api.debugger.jpda,\ + org.netbeans.api.java,\ + org.netbeans.libs.cglib,\ + org.netbeans.libs.javacapi,\ + org.netbeans.libs.javacimpl,\ + org.netbeans.libs.jsr223,\ + org.netbeans.libs.springframework,\ + org.netbeans.modules.ant.browsetask,\ + org.netbeans.modules.ant.debugger,\ + org.netbeans.modules.ant.freeform,\ + org.netbeans.modules.ant.grammar,\ + org.netbeans.modules.ant.kit,\ + org.netbeans.modules.beans,\ + org.netbeans.modules.classfile,\ + org.netbeans.modules.dbschema,\ + org.netbeans.modules.debugger.jpda,\ + org.netbeans.modules.debugger.jpda.ant,\ + org.netbeans.modules.debugger.jpda.projects,\ + org.netbeans.modules.debugger.jpda.ui,\ + org.netbeans.modules.form,\ + org.netbeans.modules.form.j2ee,\ + org.netbeans.modules.form.kit,\ + org.netbeans.modules.hibernate,\ + org.netbeans.modules.hibernatelib,\ + org.netbeans.modules.hudson.ant,\ + org.netbeans.modules.hudson.maven,\ + org.netbeans.modules.i18n,\ + org.netbeans.modules.i18n.form,\ + org.netbeans.modules.j2ee.core.utilities,\ + org.netbeans.modules.j2ee.eclipselink,\ + org.netbeans.modules.j2ee.eclipselinkmodelgen,\ + org.netbeans.modules.j2ee.jpa.refactoring,\ + org.netbeans.modules.j2ee.jpa.verification,\ + org.netbeans.modules.j2ee.metadata,\ + org.netbeans.modules.j2ee.metadata.model.support,\ + org.netbeans.modules.j2ee.persistence,\ + org.netbeans.modules.j2ee.persistence.kit,\ + org.netbeans.modules.j2ee.persistenceapi,\ + org.netbeans.modules.j2ee.toplinklib,\ + org.netbeans.modules.java.api.common,\ + org.netbeans.modules.java.debug,\ + org.netbeans.modules.java.editor,\ + org.netbeans.modules.java.editor.lib,\ + org.netbeans.modules.java.examples,\ + org.netbeans.modules.java.freeform,\ + org.netbeans.modules.java.guards,\ + org.netbeans.modules.java.helpset,\ + org.netbeans.modules.java.hints,\ + org.netbeans.modules.java.hints.processor,\ + org.netbeans.modules.java.j2seplatform,\ + org.netbeans.modules.java.j2seproject,\ + org.netbeans.modules.java.kit,\ + org.netbeans.modules.java.lexer,\ + org.netbeans.modules.java.navigation,\ + org.netbeans.modules.java.platform,\ + org.netbeans.modules.java.preprocessorbridge,\ + org.netbeans.modules.java.project,\ + org.netbeans.modules.java.source,\ + org.netbeans.modules.java.source.ant,\ + org.netbeans.modules.java.sourceui,\ + org.netbeans.modules.javadoc,\ + org.netbeans.modules.javawebstart,\ + org.netbeans.modules.jellytools,\ + org.netbeans.modules.jellytools.java,\ + org.netbeans.modules.junit,\ + org.netbeans.modules.maven,\ + org.netbeans.modules.maven.coverage,\ + org.netbeans.modules.maven.embedder,\ + org.netbeans.modules.maven.grammar,\ + org.netbeans.modules.maven.graph,\ + org.netbeans.modules.maven.hints,\ + org.netbeans.modules.maven.indexer,\ + org.netbeans.modules.maven.junit,\ + org.netbeans.modules.maven.kit,\ + org.netbeans.modules.maven.model,\ + org.netbeans.modules.maven.osgi,\ + org.netbeans.modules.maven.persistence,\ + org.netbeans.modules.maven.repository,\ + org.netbeans.modules.maven.search,\ + org.netbeans.modules.maven.spring,\ + org.netbeans.modules.projectimport.eclipse.core,\ + org.netbeans.modules.projectimport.eclipse.j2se,\ + org.netbeans.modules.refactoring.java,\ + org.netbeans.modules.spellchecker.bindings.java,\ + org.netbeans.modules.spring.beans,\ + org.netbeans.modules.swingapp,\ + org.netbeans.modules.websvc.jaxws21,\ + org.netbeans.modules.websvc.jaxws21api,\ + org.netbeans.modules.websvc.saas.codegen.java,\ + org.netbeans.modules.xml.jaxb,\ + org.netbeans.modules.xml.tools.java,\ + org.openide.compat,\ + org.openide.util.enumerations +nbplatform.active=default diff --git a/Logging/nbproject/project.properties b/CoreUtils/nbproject/project.properties similarity index 100% rename from Logging/nbproject/project.properties rename to CoreUtils/nbproject/project.properties diff --git a/Logging/nbproject/project.xml b/CoreUtils/nbproject/project.xml similarity index 85% rename from Logging/nbproject/project.xml rename to CoreUtils/nbproject/project.xml index 0adbf513ee..14c4c58258 100644 --- a/Logging/nbproject/project.xml +++ b/CoreUtils/nbproject/project.xml @@ -3,7 +3,7 @@ org.netbeans.modules.apisupport.project - org.sleuthkit.autopsy.logging + org.sleuthkit.autopsy.coreutils @@ -20,7 +20,7 @@ - 7.23.1 + 7.31.1 @@ -28,7 +28,7 @@ - 7.15.1 + 7.20.1 @@ -36,7 +36,7 @@ - 7.17.2 + 7.23.1 @@ -44,7 +44,7 @@ - 8.6.2 + 8.15.1 @@ -52,12 +52,12 @@ - 8.3.1 + 8.8.1 - org.sleuthkit.autopsy.logging + org.sleuthkit.autopsy.coreutils diff --git a/Logging/nbproject/suite.properties b/CoreUtils/nbproject/suite.properties similarity index 100% rename from Logging/nbproject/suite.properties rename to CoreUtils/nbproject/suite.properties diff --git a/Logging/src/org/sleuthkit/autopsy/logging/AutopsyExceptionHandler.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java similarity index 99% rename from Logging/src/org/sleuthkit/autopsy/logging/AutopsyExceptionHandler.java rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java index 22b7779e00..779362127e 100644 --- a/Logging/src/org/sleuthkit/autopsy/logging/AutopsyExceptionHandler.java +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.sleuthkit.autopsy.logging; +package org.sleuthkit.autopsy.coreutils; import java.awt.Component; import java.util.logging.Filter; diff --git a/CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyPropFile.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyPropFile.java new file mode 100644 index 0000000000..5c9e17bbd8 --- /dev/null +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/AutopsyPropFile.java @@ -0,0 +1,132 @@ +/* + * 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.coreutils; + +import java.io.*; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +/** + * This class contains the framework to read, add, update, and remove + * from the properties file located at %USERDIR%/autopsy.properties + * + * @author dfickling + */ +public class AutopsyPropFile { + + // The directory where the properties file is lcoated + private final static String userDirPath = System.getProperty("netbeans.user"); + private final static String propFilePath = userDirPath + File.separator + "autopsy.properties"; + + // The AutopsyPropFile singleton + private static final AutopsyPropFile INSTANCE = new AutopsyPropFile(); + + private Properties properties; + + /** + * Get the instance of the AutopsyPropFile singleton + */ + public static AutopsyPropFile getInstance(){ + return INSTANCE; + } + + /** the constructor */ + private AutopsyPropFile() { + properties = new Properties(); + try { + // try to load all the properties from the properties file in the home directory + InputStream inputStream = new FileInputStream(propFilePath); + properties.load(inputStream); + inputStream.close(); + } catch (Exception ignore) { + // if cannot load it, we create a new properties file without any data inside it + try { + // create the directory and property file to store it + File output = new File(propFilePath); + if (!output.exists()) { + File parent = new File(output.getParent()); + if (!parent.exists()) { + parent.mkdirs(); + } + output.createNewFile(); + FileOutputStream fos = new FileOutputStream(output); + properties.store(fos, ""); + fos.close(); + } else { + // if the property file already exist, throw an error that says cannot load that file + Logger.getLogger(AutopsyPropFile.class.getName()).log(Level.WARNING, "Error: Could not load the property file.", new Exception("The properties file already exist and can't load that file.")); + } + } catch (IOException ex2) { + Logger.getLogger(AutopsyPropFile.class.getName()).log(Level.WARNING, "Error: Could not create the property file.", ex2); + } + } + } + + + private void storeProperties() throws IOException { + FileOutputStream fos = new FileOutputStream(new File(propFilePath)); + properties.store(fos, ""); + fos.close(); + } + + /** + * Gets the properties file paths. + * + * @return propertyPath + */ + public static String getPropertiesFilePath() { + return propFilePath; + } + + public void setProperty(String key, String val){ + properties.setProperty(key, val); + try { + storeProperties(); + } catch (Exception ex) { + Logger.getLogger(AutopsyPropFile.class.getName()).log(Level.WARNING, "Error: Could not update the properties file.", ex); + } + } + + public String getProperty(String key){ + return properties.getProperty(key); + } + + public void removeProperty(String key){ + properties.setProperty(key, null); + try { + storeProperties(); + } catch (Exception ex) { + Logger.getLogger(AutopsyPropFile.class.getName()).log(Level.WARNING, "Error: Could not update the properties file.", ex); + } + } + + /** + * Gets the property file where the user properties such as Recent Cases + * and selected Hash Databases are stored. + * @return A new file handle + */ + public static File getPropertyFile() { + return new File(propFilePath); + } + + public static File getUserDirPath() { + return new File(userDirPath); + } +} diff --git a/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Bundle.properties b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Bundle.properties new file mode 100644 index 0000000000..fbb4be8eb9 --- /dev/null +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Bundle.properties @@ -0,0 +1 @@ +OpenIDE-Module-Name=CoreUtils diff --git a/Logging/src/org/sleuthkit/autopsy/logging/Installer.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Installer.java similarity index 97% rename from Logging/src/org/sleuthkit/autopsy/logging/Installer.java rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/Installer.java index f8fa016254..3eb46168a5 100644 --- a/Logging/src/org/sleuthkit/autopsy/logging/Installer.java +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Installer.java @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.sleuthkit.autopsy.logging; +package org.sleuthkit.autopsy.coreutils; import java.io.IOException; import java.util.logging.FileHandler; diff --git a/Logging/src/org/sleuthkit/autopsy/logging/Log.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Log.java similarity index 96% rename from Logging/src/org/sleuthkit/autopsy/logging/Log.java rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/Log.java index a8df07d91b..4dfda17f84 100644 --- a/Logging/src/org/sleuthkit/autopsy/logging/Log.java +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/Log.java @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.sleuthkit.autopsy.logging; +package org.sleuthkit.autopsy.coreutils; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/Logging/src/org/sleuthkit/autopsy/logging/LogIcon.png b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/LogIcon.png similarity index 100% rename from Logging/src/org/sleuthkit/autopsy/logging/LogIcon.png rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/LogIcon.png diff --git a/Logging/src/org/sleuthkit/autopsy/logging/TestLogger.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/TestLogger.java similarity index 97% rename from Logging/src/org/sleuthkit/autopsy/logging/TestLogger.java rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/TestLogger.java index 54dd06b40a..d233f6b753 100644 --- a/Logging/src/org/sleuthkit/autopsy/logging/TestLogger.java +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/TestLogger.java @@ -17,7 +17,7 @@ * limitations under the License. */ -package org.sleuthkit.autopsy.logging; +package org.sleuthkit.autopsy.coreutils; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/Logging/src/org/sleuthkit/autopsy/logging/TimestampingFormatter.java b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java similarity index 97% rename from Logging/src/org/sleuthkit/autopsy/logging/TimestampingFormatter.java rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java index 5e52520e0c..469722aa48 100644 --- a/Logging/src/org/sleuthkit/autopsy/logging/TimestampingFormatter.java +++ b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java @@ -18,7 +18,7 @@ */ -package org.sleuthkit.autopsy.logging; +package org.sleuthkit.autopsy.coreutils; import java.text.DateFormat; import java.util.Date; diff --git a/Logging/src/org/sleuthkit/autopsy/logging/layer.xml b/CoreUtils/src/org/sleuthkit/autopsy/coreutils/layer.xml similarity index 100% rename from Logging/src/org/sleuthkit/autopsy/logging/layer.xml rename to CoreUtils/src/org/sleuthkit/autopsy/coreutils/layer.xml diff --git a/DataModel/nbproject/genfiles.properties b/DataModel/nbproject/genfiles.properties index 1b1b7ff913..80fe34e47b 100644 --- a/DataModel/nbproject/genfiles.properties +++ b/DataModel/nbproject/genfiles.properties @@ -3,6 +3,6 @@ build.xml.script.CRC32=3bd58878 build.xml.stylesheet.CRC32=a56c6a5b@1.42.2 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=c861ba34 +nbproject/build-impl.xml.data.CRC32=d5d42932 nbproject/build-impl.xml.script.CRC32=b0a13adb -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/DataModel/nbproject/project.xml b/DataModel/nbproject/project.xml index 4e76ac7974..873a58d40b 100644 --- a/DataModel/nbproject/project.xml +++ b/DataModel/nbproject/project.xml @@ -47,7 +47,7 @@ - org.sleuthkit.autopsy.logging + org.sleuthkit.autopsy.coreutils diff --git a/DataModel/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java b/DataModel/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java index 0cd4d2bca5..c2b8d3db3d 100644 --- a/DataModel/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java +++ b/DataModel/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.util.LinkedHashMap; +import java.util.Map; import org.openide.nodes.Sheet; import org.sleuthkit.datamodel.FsContent; @@ -25,16 +27,139 @@ import org.sleuthkit.datamodel.FsContent; * Abstract class that implements the commonality between File and Directory * Nodes (same properties). */ -abstract class AbstractFsContentNode extends AbstractContentNode { +public abstract class AbstractFsContentNode extends AbstractContentNode { - /** - * Name of the property that holds the name. - */ - public static final String PROPERTY_NAME = "Name"; - /** - * Name of the property that holds the path. - */ - public static final String PROPERTY_LOCATION = "Location"; + // Note: this order matters for the search result, changed it if the order of property headers on the "KeywordSearchNode"changed + + public static enum FsContentPropertyType { + + NAME { + + @Override + public String toString() { + return "Name"; + } + }, + LOCATION { + + + @Override + public String toString() { + return "Location"; + } + }, + MOD_TIME { + + + @Override + public String toString() { + return "Mod. Time"; + } + }, + CHANGED_TIME { + + + @Override + public String toString() { + return "Change Time"; + } + }, + ACCESS_TIME { + + + @Override + public String toString() { + return "Access Time"; + } + }, + CREATED_TIME { + + + @Override + public String toString() { + return "Created Time"; + } + }, + SIZE { + + @Override + public String toString() { + return "Size"; + } + }, + FLAGS_DIR { + @Override + public String toString() { + return "Flags(Dir)"; + } + }, + FLAGS_META { + + @Override + public String toString() { + return "Flags(Meta)"; + } + }, + MODE { + + @Override + public String toString() { + return "Mode"; + } + }, + USER_ID { + + @Override + public String toString() { + return "UserID"; + } + }, + GROUP_ID { + @Override + public String toString() { + return "GroupID"; + } + }, + META_ADDR { + + @Override + public String toString() { + return "Meta Addr."; + } + }, + ATTR_ADDR { + + + @Override + public String toString() { + return "Attr. Addr."; + } + }, + TYPE_DIR { + + + + @Override + public String toString() { + return "Type(Dir)"; + } + }, + TYPE_META { + + @Override + public String toString() { + return "Type(Meta)"; + } + }, + KNOWN { + + + @Override + public String toString() { + return "Known"; + } + }, + } AbstractFsContentNode(T fsContent) { super(fsContent); @@ -49,25 +174,43 @@ abstract class AbstractFsContentNode extends AbstractConten s.put(ss); } - // Note: this order matters for the search result, changed it if the order of property headers on the "KeywordSearchNode"changed - ss.put(new NodeProperty(PROPERTY_NAME, "Name", "no description", content.getName())); - ss.put(new NodeProperty(PROPERTY_LOCATION, "Location", "no description", DataConversion.getformattedPath(ContentUtils.getDisplayPath(content), 0))); - ss.put(new NodeProperty("Modified Time", "Modified Time", "no description", content.getMtimeAsDate())); - ss.put(new NodeProperty("Changed Time", "Changed Time", "no description", content.getCtimeAsDate())); - ss.put(new NodeProperty("Access Time", "Access Time", "no description", content.getAtimeAsDate())); - ss.put(new NodeProperty("Created Time", "Created Time", "no description", content.getCrtimeAsDate())); - ss.put(new NodeProperty("Size", "Size", "no description", content.getSize())); - ss.put(new NodeProperty("Flags (Directory)", "Flags (Directory)", "no description", content.getDirFlagsAsString())); - ss.put(new NodeProperty("Flags (Meta)", "Flags (Meta)", "no description", content.getMetaFlagsAsString())); - ss.put(new NodeProperty("Mode ", "Mode", "no description", content.getModeAsString())); - ss.put(new NodeProperty("User ID", "User ID", "no description", content.getUid())); - ss.put(new NodeProperty("Group ID", "Group ID", "no description", content.getGid())); - ss.put(new NodeProperty("Metadata Address", "Metadata Addr", "no description", content.getMeta_addr())); - ss.put(new NodeProperty("Attribute Address", "Attribute Addr", "no description", Long.toString(content.getAttr_type()) + "-" + Long.toString(content.getAttr_id()))); - ss.put(new NodeProperty("Type (Directory)", "Type (Directory)", "no description", content.getDirTypeAsString())); - ss.put(new NodeProperty("Type (Meta)", "Type (Meta)", "no description", content.getMetaTypeAsString())); - ss.put(new NodeProperty("Known", "Known", "no description", content.getKnown().getName())); + Map map = new LinkedHashMap(); + fillPropertyMap(map, content); + + FsContentPropertyType[] fsTypes = FsContentPropertyType.values(); + final int FS_PROPS_LEN = fsTypes.length; + final String NO_DESCR = "no description"; + for (int i = 0; i < FS_PROPS_LEN; ++i) { + final FsContentPropertyType propType = FsContentPropertyType.values()[i]; + final String propString = propType.toString(); + ss.put(new NodeProperty(propString, propString, NO_DESCR, map.get(propString))); + } return s; } + + /** + * Fill map with FsContent properties + * @param map, with preserved ordering, where property names/values are put + * @param content to extract properties from + */ + public static void fillPropertyMap(Map map, FsContent content) { + map.put(FsContentPropertyType.NAME.toString(), content.getName()); + map.put(FsContentPropertyType.LOCATION.toString(), DataConversion.getformattedPath(ContentUtils.getDisplayPath(content), 0)); + map.put(FsContentPropertyType.MOD_TIME.toString(), content.getMtimeAsDate()); + map.put(FsContentPropertyType.CHANGED_TIME.toString(), content.getCtimeAsDate()); + map.put(FsContentPropertyType.ACCESS_TIME.toString(), content.getAtimeAsDate()); + map.put(FsContentPropertyType.CREATED_TIME.toString(), content.getCrtimeAsDate()); + map.put(FsContentPropertyType.SIZE.toString(), Long.toString(content.getSize())); + map.put(FsContentPropertyType.FLAGS_DIR.toString(), content.getDirFlagsAsString()); + map.put(FsContentPropertyType.FLAGS_META.toString(), content.getMetaFlagsAsString()); + map.put(FsContentPropertyType.MODE.toString(), content.getModeAsString()); + map.put(FsContentPropertyType.USER_ID.toString(), Long.toString(content.getUid())); + map.put(FsContentPropertyType.GROUP_ID.toString(), Long.toString(content.getGid())); + map.put(FsContentPropertyType.META_ADDR.toString(), Long.toString(content.getMeta_addr())); + map.put(FsContentPropertyType.ATTR_ADDR.toString(), Long.toString(content.getAttr_type()) + "-" + Long.toString(content.getAttr_id())); + map.put(FsContentPropertyType.TYPE_DIR.toString(), content.getDirTypeAsString()); + map.put(FsContentPropertyType.TYPE_META.toString(), content.getMetaTypeAsString()); + map.put(FsContentPropertyType.KNOWN.toString(), content.getKnown().getName()); + } } diff --git a/DataModel/src/org/sleuthkit/autopsy/datamodel/DataConversion.java b/DataModel/src/org/sleuthkit/autopsy/datamodel/DataConversion.java index e4a54cf5a3..1aa38b49f3 100644 --- a/DataModel/src/org/sleuthkit/autopsy/datamodel/DataConversion.java +++ b/DataModel/src/org/sleuthkit/autopsy/datamodel/DataConversion.java @@ -31,7 +31,7 @@ public class DataConversion { return ""; } else { String base = new String(array); - + StringBuilder buff = new StringBuilder(); int count = 0; int extra = base.length() % 16; @@ -133,40 +133,38 @@ public class DataConversion { Charset.forName("UTF-8").newEncoder(); */ - String result = ""; - String temp = ""; + + final StringBuilder result = new StringBuilder(); + StringBuilder temp = new StringBuilder(); int counter = 0; //char[] converted = new java.lang.System.Text.Encoding.ASCII.GetString(args).ToCharArray(); - char NL = (char) 10; // ASCII char for new line - + final char NL = (char) 10; // ASCII char for new line + final String NLS = Character.toString(NL); for (int i = 0; i < args.length; i++) { - char tempChar = (char) args[i]; - int dec = (int) tempChar; + char curChar = (char) args[i]; + int curCharInt = (int) curChar; - // the printable ASCII chars are dec 32-126 - // and we want to include TAB as well (dec 9) - if (!((dec < 32 || dec > 126) && dec != 9)) { - temp = temp + Character.toString(tempChar); - counter = counter + 1; - } else { + // ignore non-printable ASCII chars + // 32-126 and TAB ( 9) + if (((curCharInt < 32) && (curCharInt != 9)) || (curCharInt > 126)) { if (counter >= parameter) { // add to the result and also add the new line at the end - result = result + temp + Character.toString(NL); - - // reset the temp and counter - temp = ""; - counter = 0; + result.append(temp); + result.append(NLS); } // reset the temp and counter - temp = ""; + temp = new StringBuilder(); counter = 0; } + else { + temp.append(curChar); + ++counter; + } } - result = result + temp; - - return result; + result.append(temp); + return result.toString(); } /** diff --git a/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueNode.java b/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueNode.java index 028cdedb48..6d6bfeb730 100644 --- a/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueNode.java +++ b/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueNode.java @@ -1,3 +1,21 @@ +/* + * 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.datamodel; diff --git a/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueThing.java b/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueThing.java old mode 100644 new mode 100755 index 2c439db15f..3441bcb9ec --- a/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueThing.java +++ b/DataModel/src/org/sleuthkit/autopsy/datamodel/KeyValueThing.java @@ -1,3 +1,22 @@ +/* + * 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.datamodel; import java.util.Map; diff --git a/DirectoryTree/nbproject/genfiles.properties b/DirectoryTree/nbproject/genfiles.properties index e779ce6c0c..02dbf0546d 100644 --- a/DirectoryTree/nbproject/genfiles.properties +++ b/DirectoryTree/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=db477856 +build.xml.data.CRC32=f54ab67d build.xml.script.CRC32=6ec7becb -build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +build.xml.stylesheet.CRC32=a56c6a5b@2.47.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=db477856 +nbproject/build-impl.xml.data.CRC32=f54ab67d nbproject/build-impl.xml.script.CRC32=8c5007a7 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/DirectoryTree/nbproject/project.xml b/DirectoryTree/nbproject/project.xml index 6af52a56ca..9205363ac6 100644 --- a/DirectoryTree/nbproject/project.xml +++ b/DirectoryTree/nbproject/project.xml @@ -107,7 +107,7 @@ - org.sleuthkit.autopsy.datamodel + org.sleuthkit.autopsy.coreutils @@ -116,7 +116,7 @@ - org.sleuthkit.autopsy.logging + org.sleuthkit.autopsy.datamodel diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ChangeViewAction.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ChangeViewAction.java index 028c1e2e0b..c4efd8d872 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ChangeViewAction.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ChangeViewAction.java @@ -27,7 +27,7 @@ import org.openide.util.actions.Presenter; import org.sleuthkit.autopsy.corecomponents.DataContentTopComponent; import org.sleuthkit.autopsy.corecomponents.DataContentViewerHex; import org.sleuthkit.autopsy.corecomponents.DataContentViewerString; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * The actions to change between the "Hex View" and "String View". diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/CollapseAction.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/CollapseAction.java index 7b18336427..4d1acb9c43 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/CollapseAction.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/CollapseAction.java @@ -24,7 +24,7 @@ import org.openide.explorer.ExplorerManager; import org.openide.explorer.view.BeanTreeView; import org.openide.nodes.Children; import org.openide.nodes.Node; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 34b0dbbeba..c63f433735 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.sleuthkit.autopsy.datamodel.VolumeNode; -import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.datamodel.DirectoryNode; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,6 +33,7 @@ import org.openide.explorer.ExplorerManager; import org.openide.nodes.FilterNode; import org.openide.nodes.Node; import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode.FsContentPropertyType; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; import org.sleuthkit.datamodel.Directory; @@ -41,6 +41,7 @@ import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.Volume; + /** * This class wraps nodes as they are passed to the DataResult viewers. It * defines the actions that the node should have. @@ -52,11 +53,12 @@ public class DataResultFilterNode extends FilterNode{ /** the constructor */ - public DataResultFilterNode(Node arg, ExplorerManager em) { - super(arg, new DataResultFilterChildren(arg, em)); + public DataResultFilterNode(Node node, ExplorerManager em) { + super(node, new DataResultFilterChildren(node, em)); this.sourceEm = em; getActionsCV = new GetActionsContentVisitor(); } + /** * Right click action for the nodes that we want to pass to the directory @@ -74,6 +76,8 @@ public class DataResultFilterNode extends FilterNode{ Content nodeContent = this.getOriginal().getLookup().lookup(Content.class); actions.addAll(nodeContent.accept(getActionsCV)); + + //actions.add(new IndexContentFilesAction(nodeContent, "Index")); return actions.toArray(new Action[actions.size()]); } @@ -105,7 +109,7 @@ public class DataResultFilterNode extends FilterNode{ public List visit(File f) { List actions = new ArrayList(); actions.add(new ExternalViewerAction("Open in External Viewer", getOriginal())); - actions.add(new ExtractAction("Extract", getOriginal())); + actions.add(new ExtractAction("Extract File", getOriginal())); return actions; } @@ -191,7 +195,7 @@ public class DataResultFilterNode extends FilterNode{ newPs.setShortDescription(ps.getShortDescription()); newPs.put(ps.getProperties()); - newPs.remove(FileNode.PROPERTY_LOCATION); + newPs.remove(FsContentPropertyType.LOCATION.toString() ); propertySets[i] = newPs; } } diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java index 3e87fd6531..d34c0294ca 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java @@ -45,6 +45,7 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; +import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.datamodel.DataConversion; import org.sleuthkit.autopsy.datamodel.RootContentChildren; @@ -555,7 +556,9 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat DirectoryTreeTopComponent.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } DirectoryTreeTopComponent.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - DirectoryTreeTopComponent.this.dataResult.setNode(new DataResultFilterNode(originNode, DirectoryTreeTopComponent.this.em)); + //set node, wrap in filter node first to filter out children + Node drfn = new DataResultFilterNode(originNode, DirectoryTreeTopComponent.this.em); + DirectoryTreeTopComponent.this.dataResult.setNode(new TableFilterNode(drfn, true)); String path = DataConversion.getformattedPath(ContentUtils.getDisplayPath(originNode.getLookup().lookup(Content.class)), 0); DirectoryTreeTopComponent.this.dataResult.setPath(path); diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java index f772b4528b..56a0bfc7fe 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java @@ -28,7 +28,7 @@ import javax.swing.AbstractAction; import org.openide.nodes.Node; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** * Extracts a File object to a temporary file in the case directory, and then diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java index 3ea8aa522f..8dbbabbfa5 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java @@ -27,7 +27,7 @@ import javax.swing.JOptionPane; import org.openide.nodes.Node; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.datamodel.ContentUtils.ExtractFscContentVisitor; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; import org.sleuthkit.datamodel.Directory; @@ -106,6 +106,11 @@ public final class ExtractAction extends AbstractAction { } ExtractFscContentVisitor.extract(fsContent, destination); + if(fsContent.isDir()) + JOptionPane.showMessageDialog((Component) e.getSource(), "Directory extracted."); + else if(fsContent.isFile()){ + JOptionPane.showMessageDialog((Component) e.getSource(), "File extracted."); + } } } } diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/NewWindowViewAction.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/NewWindowViewAction.java index 60f0ff1718..977e8c2e94 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/NewWindowViewAction.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/NewWindowViewAction.java @@ -28,7 +28,7 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.datamodel.DataConversion; import org.sleuthkit.autopsy.corecomponents.DataContentTopComponent; import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.Content; /** diff --git a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ShowDetailActionVisitor.java b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ShowDetailActionVisitor.java index d4fa64a89c..f32e2f326b 100644 --- a/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ShowDetailActionVisitor.java +++ b/DirectoryTree/src/org/sleuthkit/autopsy/directorytree/ShowDetailActionVisitor.java @@ -35,7 +35,7 @@ import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.table.DefaultTableModel; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; import org.sleuthkit.datamodel.FileSystem; diff --git a/FileSearch/nbproject/genfiles.properties b/FileSearch/nbproject/genfiles.properties index 3b8b881e93..0f6adbd481 100644 --- a/FileSearch/nbproject/genfiles.properties +++ b/FileSearch/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=efa3ed67 +build.xml.data.CRC32=ad8ce357 build.xml.script.CRC32=c0009852 -build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +build.xml.stylesheet.CRC32=a56c6a5b@2.47.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=efa3ed67 +nbproject/build-impl.xml.data.CRC32=ad8ce357 nbproject/build-impl.xml.script.CRC32=2e520747 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/FileSearch/nbproject/project.xml b/FileSearch/nbproject/project.xml index f421cd12d1..431162f7f3 100644 --- a/FileSearch/nbproject/project.xml +++ b/FileSearch/nbproject/project.xml @@ -98,6 +98,15 @@ 1.0 + + org.sleuthkit.autopsy.coreutils + + + + 0-1 + 0.0 + + org.sleuthkit.autopsy.datamodel @@ -116,15 +125,6 @@ 1.0 - - org.sleuthkit.autopsy.logging - - - - 0-1 - 0.0 - - org.sleuthkit.autopsy.filesearch diff --git a/FileSearch/src/org/sleuthkit/autopsy/filesearch/DataResultFilterNode.java b/FileSearch/src/org/sleuthkit/autopsy/filesearch/DataResultFilterNode.java index a2fc84d708..6bba0a15e1 100644 --- a/FileSearch/src/org/sleuthkit/autopsy/filesearch/DataResultFilterNode.java +++ b/FileSearch/src/org/sleuthkit/autopsy/filesearch/DataResultFilterNode.java @@ -60,6 +60,7 @@ public class DataResultFilterNode extends FilterNode { @Override public Action[] visit(Directory dir) { return new Action[]{ + new ExtractAction("Extract Directory", getOriginal()), new ChangeViewAction("View", 0, getOriginal()), new OpenParentFolderAction("Open Parent Directory", ContentUtils.getSystemPath(dir)) }; @@ -69,7 +70,7 @@ public class DataResultFilterNode extends FilterNode { public Action[] visit(File f) { return new Action[]{ new ExternalViewerAction("Open in External Viewer", getOriginal()), - new ExtractAction("Extract", getOriginal()), + new ExtractAction("Extract File", getOriginal()), new ChangeViewAction("View", 0, getOriginal()), new OpenParentFolderAction("Open Parent Directory", ContentUtils.getSystemPath(f)) }; diff --git a/FileSearch/src/org/sleuthkit/autopsy/filesearch/FileSearchTopComponent.java b/FileSearch/src/org/sleuthkit/autopsy/filesearch/FileSearchTopComponent.java index 7e15a79aa9..6316e30a69 100644 --- a/FileSearch/src/org/sleuthkit/autopsy/filesearch/FileSearchTopComponent.java +++ b/FileSearch/src/org/sleuthkit/autopsy/filesearch/FileSearchTopComponent.java @@ -48,6 +48,7 @@ import org.openide.NotifyDescriptor; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponentinterfaces.DataExplorer; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; +import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.filesearch.FileSearchFilter.FilterValidationException; import org.sleuthkit.datamodel.FsContent; import org.sleuthkit.datamodel.SleuthkitCase; @@ -174,7 +175,7 @@ public final class FileSearchTopComponent extends TopComponent implements DataEx logger.log(Level.WARNING, "Error while trying to get the number of matches.", ex); } - TopComponent searchResultWin = DataResultTopComponent.createInstance(title, pathText, new SearchNode(fsContentList), totalMatches); + TopComponent searchResultWin = DataResultTopComponent.createInstance(title, pathText, new TableFilterNode(new SearchNode(fsContentList), true), totalMatches); searchResultWin.requestActive(); // make it the active top component diff --git a/FileSearch/src/org/sleuthkit/autopsy/filesearch/OpenParentFolderAction.java b/FileSearch/src/org/sleuthkit/autopsy/filesearch/OpenParentFolderAction.java index 781e136642..81e95e6c15 100644 --- a/FileSearch/src/org/sleuthkit/autopsy/filesearch/OpenParentFolderAction.java +++ b/FileSearch/src/org/sleuthkit/autopsy/filesearch/OpenParentFolderAction.java @@ -31,7 +31,7 @@ import org.openide.nodes.Node; import org.openide.nodes.NodeOp; import org.openide.windows.TopComponent; import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; -import org.sleuthkit.autopsy.logging.Log; +import org.sleuthkit.autopsy.coreutils.Log; /** diff --git a/KeywordSearch/build.xml b/KeywordSearch/build.xml new file mode 100644 index 0000000000..e99df8ddb2 --- /dev/null +++ b/KeywordSearch/build.xml @@ -0,0 +1,46 @@ + + + + + + Builds, tests, and runs the project org.sleuthkit.autopsy.keywordsearch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/ivy.xml b/KeywordSearch/ivy.xml new file mode 100644 index 0000000000..3d0a84af01 --- /dev/null +++ b/KeywordSearch/ivy.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/manifest.mf b/KeywordSearch/manifest.mf new file mode 100644 index 0000000000..08495144a6 --- /dev/null +++ b/KeywordSearch/manifest.mf @@ -0,0 +1,8 @@ +Manifest-Version: 1.0 +OpenIDE-Module: org.sleuthkit.autopsy.keywordsearch/0 +OpenIDE-Module-Implementation-Version: 1 +OpenIDE-Module-Install: org/sleuthkit/autopsy/keywordsearch/Installer.class +OpenIDE-Module-Layer: org/sleuthkit/autopsy/keywordsearch/layer.xml +OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/keywordsearch/Bundle.properties +OpenIDE-Module-Requires: org.openide.windows.WindowManager + diff --git a/KeywordSearch/nbproject/build-impl.xml b/KeywordSearch/nbproject/build-impl.xml new file mode 100644 index 0000000000..a0f2871dd4 --- /dev/null +++ b/KeywordSearch/nbproject/build-impl.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + You must set 'suite.dir' to point to your containing module suite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/nbproject/genfiles.properties b/KeywordSearch/nbproject/genfiles.properties new file mode 100644 index 0000000000..cc36a74245 --- /dev/null +++ b/KeywordSearch/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=eaa84b46 +build.xml.script.CRC32=87b97b04 +build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=957d4757 +nbproject/build-impl.xml.script.CRC32=fe1f48d2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/KeywordSearch/nbproject/project.properties b/KeywordSearch/nbproject/project.properties new file mode 100644 index 0000000000..de2ff39a4f --- /dev/null +++ b/KeywordSearch/nbproject/project.properties @@ -0,0 +1,12 @@ +file.reference.commons-codec-1.5.jar=release/modules/ext/commons-codec-1.5.jar +file.reference.commons-httpclient-3.1.jar=release/modules/ext/commons-httpclient-3.1.jar +file.reference.commons-io-1.4.jar=release/modules/ext/commons-io-1.4.jar +file.reference.commons-lang-2.4.jar=release/modules/ext/commons-lang-2.4.jar +file.reference.jcl-over-slf4j-1.6.1.jar=release/modules/ext/jcl-over-slf4j-1.6.1.jar +file.reference.slf4j-api-1.6.1.jar=release/modules/ext/slf4j-api-1.6.1.jar +file.reference.solr-solrj-3.5.0.jar=release/modules/ext/solr-solrj-3.5.0.jar +javac.source=1.6 +javac.compilerargs=-Xlint -Xlint:-serial +javadoc.reference.solr-solrj-3.5.0.jar=release/modules/ext/solr-solrj-3.5.0-javadoc.jar +source.reference.solr-solrj-3.5.0.jar=release/modules/ext/solr-solrj-3.5.0-sources.jar +spec.version.base=0.0 diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml new file mode 100644 index 0000000000..3dad2b3c4c --- /dev/null +++ b/KeywordSearch/nbproject/project.xml @@ -0,0 +1,154 @@ + + + org.netbeans.modules.apisupport.project + + + org.sleuthkit.autopsy.keywordsearch + + + + org.netbeans.api.progress + + + + 1 + 1.24.1 + + + + org.netbeans.modules.settings + + + + 1 + 1.31.1 + + + + org.openide.awt + + + + 7.31.1 + + + + org.openide.modules + + + + 7.23.1 + + + + org.openide.nodes + + + + 7.21.1 + + + + org.openide.util + + + + 8.15.1 + + + + org.openide.util.lookup + + + + 8.8.1 + + + + org.openide.windows + + + + 6.40.1 + + + + org.sleuthkit.autopsy.casemodule + + + + 1 + 1.0 + + + + org.sleuthkit.autopsy.corecomponentinterfaces + + + + 1 + 1.0 + + + + org.sleuthkit.autopsy.corecomponents + + + + 1 + 1.0 + + + + org.sleuthkit.autopsy.coreutils + + + + 0-1 + 0.0 + + + + org.sleuthkit.autopsy.datamodel + + + + 1 + 1.0 + + + + + org.sleuthkit.autopsy.keywordsearch + + + ext/slf4j-api-1.6.1.jar + release/modules/ext/slf4j-api-1.6.1.jar + + + ext/commons-io-1.4.jar + release/modules/ext/commons-io-1.4.jar + + + ext/commons-httpclient-3.1.jar + release/modules/ext/commons-httpclient-3.1.jar + + + ext/commons-codec-1.5.jar + release/modules/ext/commons-codec-1.5.jar + + + ext/commons-lang-2.4.jar + release/modules/ext/commons-lang-2.4.jar + + + ext/jcl-over-slf4j-1.6.1.jar + release/modules/ext/jcl-over-slf4j-1.6.1.jar + + + ext/solr-solrj-3.5.0.jar + release/modules/ext/solr-solrj-3.5.0.jar + + + + diff --git a/KeywordSearch/nbproject/suite.properties b/KeywordSearch/nbproject/suite.properties new file mode 100644 index 0000000000..29d7cc9bd6 --- /dev/null +++ b/KeywordSearch/nbproject/suite.properties @@ -0,0 +1 @@ +suite.dir=${basedir}/.. diff --git a/KeywordSearch/release/solr/etc/jetty.xml b/KeywordSearch/release/solr/etc/jetty.xml new file mode 100644 index 0000000000..403d835dbd --- /dev/null +++ b/KeywordSearch/release/solr/etc/jetty.xml @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + org.mortbay.jetty.Request.maxFormContentSize + 1000000 + + + + + + + + + 10 + 10000 + 20 + + + + + + + + + + + + + + + + + + + + + + + 50000 + 1500 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /contexts + 5 + + + + + + + + + + + + + + + + + + + + + + /webapps + false + true + false + /etc/webdefault.xml + + + + + + + + + + + + + + + + + + + + + + + + + true + false + false + 1000 + + diff --git a/KeywordSearch/release/solr/etc/webdefault.xml b/KeywordSearch/release/solr/etc/webdefault.xml new file mode 100644 index 0000000000..b8d0114169 --- /dev/null +++ b/KeywordSearch/release/solr/etc/webdefault.xml @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + Default web.xml file. + This file is applied to a Web application before it's own WEB_INF/web.xml file + + + + + + + + + + org.mortbay.jetty.webapp.NoTLDJarPattern + start.jar|ant-.*\.jar|dojo-.*\.jar|jetty-.*\.jar|jsp-api-.*\.jar|junit-.*\.jar|servlet-api-.*\.jar|dnsns\.jar|rt\.jar|jsse\.jar|tools\.jar|sunpkcs11\.jar|sunjce_provider\.jar|xerces.*\.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.mortbay.jetty.servlet.DefaultServlet + + acceptRanges + true + + + dirAllowed + true + + + welcomeServlets + false + + + redirectWelcome + false + + + maxCacheSize + 256000000 + + + maxCachedFileSize + 10000000 + + + maxCachedFiles + 1000 + + + cacheType + both + + + gzip + true + + + useFileMappedBuffer + true + + + 0 + + + default / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + logVerbosityLevel + DEBUG + + + fork + false + + + xpoweredBy + false + + + 0 + + + + jsp + *.jsp + *.jspf + *.jspx + *.xsp + *.JSP + *.JSPF + *.JSPX + *.XSP + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + + + arISO-8859-6 + beISO-8859-5 + bgISO-8859-5 + caISO-8859-1 + csISO-8859-2 + daISO-8859-1 + deISO-8859-1 + elISO-8859-7 + enISO-8859-1 + esISO-8859-1 + etISO-8859-1 + fiISO-8859-1 + frISO-8859-1 + hrISO-8859-2 + huISO-8859-2 + isISO-8859-1 + itISO-8859-1 + iwISO-8859-8 + jaShift_JIS + koEUC-KR + ltISO-8859-2 + lvISO-8859-2 + mkISO-8859-5 + nlISO-8859-1 + noISO-8859-1 + plISO-8859-2 + ptISO-8859-1 + roISO-8859-2 + ruISO-8859-5 + shISO-8859-5 + skISO-8859-2 + slISO-8859-2 + sqISO-8859-2 + srISO-8859-5 + svISO-8859-1 + trISO-8859-9 + ukISO-8859-5 + zhGB2312 + zh_TWBig5 + + + + + Disable TRACE + / + TRACE + + + + + + diff --git a/KeywordSearch/release/solr/solr/conf/admin-extra.html b/KeywordSearch/release/solr/solr/conf/admin-extra.html new file mode 100644 index 0000000000..aa739da862 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/admin-extra.html @@ -0,0 +1,31 @@ + + + diff --git a/KeywordSearch/release/solr/solr/conf/elevate.xml b/KeywordSearch/release/solr/solr/conf/elevate.xml new file mode 100644 index 0000000000..7630ebe20f --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/release/solr/solr/conf/mapping-FoldToASCII.txt b/KeywordSearch/release/solr/solr/conf/mapping-FoldToASCII.txt new file mode 100644 index 0000000000..9a84b6eac3 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/mapping-FoldToASCII.txt @@ -0,0 +1,3813 @@ +# The ASF licenses this file to You 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. + + +# This map converts alphabetic, numeric, and symbolic Unicode characters +# which are not in the first 127 ASCII characters (the "Basic Latin" Unicode +# block) into their ASCII equivalents, if one exists. +# +# Characters from the following Unicode blocks are converted; however, only +# those characters with reasonable ASCII alternatives are converted: +# +# - C1 Controls and Latin-1 Supplement: http://www.unicode.org/charts/PDF/U0080.pdf +# - Latin Extended-A: http://www.unicode.org/charts/PDF/U0100.pdf +# - Latin Extended-B: http://www.unicode.org/charts/PDF/U0180.pdf +# - Latin Extended Additional: http://www.unicode.org/charts/PDF/U1E00.pdf +# - Latin Extended-C: http://www.unicode.org/charts/PDF/U2C60.pdf +# - Latin Extended-D: http://www.unicode.org/charts/PDF/UA720.pdf +# - IPA Extensions: http://www.unicode.org/charts/PDF/U0250.pdf +# - Phonetic Extensions: http://www.unicode.org/charts/PDF/U1D00.pdf +# - Phonetic Extensions Supplement: http://www.unicode.org/charts/PDF/U1D80.pdf +# - General Punctuation: http://www.unicode.org/charts/PDF/U2000.pdf +# - Superscripts and Subscripts: http://www.unicode.org/charts/PDF/U2070.pdf +# - Enclosed Alphanumerics: http://www.unicode.org/charts/PDF/U2460.pdf +# - Dingbats: http://www.unicode.org/charts/PDF/U2700.pdf +# - Supplemental Punctuation: http://www.unicode.org/charts/PDF/U2E00.pdf +# - Alphabetic Presentation Forms: http://www.unicode.org/charts/PDF/UFB00.pdf +# - Halfwidth and Fullwidth Forms: http://www.unicode.org/charts/PDF/UFF00.pdf +# +# See: http://en.wikipedia.org/wiki/Latin_characters_in_Unicode +# +# The set of character conversions supported by this map is a superset of +# those supported by the map represented by mapping-ISOLatin1Accent.txt. +# +# See the bottom of this file for the Perl script used to generate the contents +# of this file (without this header) from ASCIIFoldingFilter.java. + + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + + +# À [LATIN CAPITAL LETTER A WITH GRAVE] +"\u00C0" => "A" + +# Á [LATIN CAPITAL LETTER A WITH ACUTE] +"\u00C1" => "A" + +#  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] +"\u00C2" => "A" + +# à [LATIN CAPITAL LETTER A WITH TILDE] +"\u00C3" => "A" + +# Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] +"\u00C4" => "A" + +# Å [LATIN CAPITAL LETTER A WITH RING ABOVE] +"\u00C5" => "A" + +# Ā [LATIN CAPITAL LETTER A WITH MACRON] +"\u0100" => "A" + +# Ă [LATIN CAPITAL LETTER A WITH BREVE] +"\u0102" => "A" + +# Ą [LATIN CAPITAL LETTER A WITH OGONEK] +"\u0104" => "A" + +# Ə http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] +"\u018F" => "A" + +# Ǎ [LATIN CAPITAL LETTER A WITH CARON] +"\u01CD" => "A" + +# Ǟ [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] +"\u01DE" => "A" + +# Ǡ [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] +"\u01E0" => "A" + +# Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] +"\u01FA" => "A" + +# Ȁ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] +"\u0200" => "A" + +# Ȃ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] +"\u0202" => "A" + +# Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] +"\u0226" => "A" + +# Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] +"\u023A" => "A" + +# ᴀ [LATIN LETTER SMALL CAPITAL A] +"\u1D00" => "A" + +# Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] +"\u1E00" => "A" + +# Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] +"\u1EA0" => "A" + +# Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] +"\u1EA2" => "A" + +# Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] +"\u1EA4" => "A" + +# Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] +"\u1EA6" => "A" + +# Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EA8" => "A" + +# Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] +"\u1EAA" => "A" + +# Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] +"\u1EAC" => "A" + +# Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] +"\u1EAE" => "A" + +# Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] +"\u1EB0" => "A" + +# Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] +"\u1EB2" => "A" + +# Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] +"\u1EB4" => "A" + +# Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] +"\u1EB6" => "A" + +# Ⓐ [CIRCLED LATIN CAPITAL LETTER A] +"\u24B6" => "A" + +# A [FULLWIDTH LATIN CAPITAL LETTER A] +"\uFF21" => "A" + +# à [LATIN SMALL LETTER A WITH GRAVE] +"\u00E0" => "a" + +# á [LATIN SMALL LETTER A WITH ACUTE] +"\u00E1" => "a" + +# â [LATIN SMALL LETTER A WITH CIRCUMFLEX] +"\u00E2" => "a" + +# ã [LATIN SMALL LETTER A WITH TILDE] +"\u00E3" => "a" + +# ä [LATIN SMALL LETTER A WITH DIAERESIS] +"\u00E4" => "a" + +# å [LATIN SMALL LETTER A WITH RING ABOVE] +"\u00E5" => "a" + +# ā [LATIN SMALL LETTER A WITH MACRON] +"\u0101" => "a" + +# ă [LATIN SMALL LETTER A WITH BREVE] +"\u0103" => "a" + +# ą [LATIN SMALL LETTER A WITH OGONEK] +"\u0105" => "a" + +# ǎ [LATIN SMALL LETTER A WITH CARON] +"\u01CE" => "a" + +# ǟ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] +"\u01DF" => "a" + +# ǡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] +"\u01E1" => "a" + +# ǻ [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] +"\u01FB" => "a" + +# ȁ [LATIN SMALL LETTER A WITH DOUBLE GRAVE] +"\u0201" => "a" + +# ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] +"\u0203" => "a" + +# ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] +"\u0227" => "a" + +# ɐ [LATIN SMALL LETTER TURNED A] +"\u0250" => "a" + +# ə [LATIN SMALL LETTER SCHWA] +"\u0259" => "a" + +# ɚ [LATIN SMALL LETTER SCHWA WITH HOOK] +"\u025A" => "a" + +# ᶏ [LATIN SMALL LETTER A WITH RETROFLEX HOOK] +"\u1D8F" => "a" + +# ᶕ [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] +"\u1D95" => "a" + +# ạ [LATIN SMALL LETTER A WITH RING BELOW] +"\u1E01" => "a" + +# ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] +"\u1E9A" => "a" + +# ạ [LATIN SMALL LETTER A WITH DOT BELOW] +"\u1EA1" => "a" + +# ả [LATIN SMALL LETTER A WITH HOOK ABOVE] +"\u1EA3" => "a" + +# ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] +"\u1EA5" => "a" + +# ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] +"\u1EA7" => "a" + +# ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EA9" => "a" + +# ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] +"\u1EAB" => "a" + +# ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] +"\u1EAD" => "a" + +# ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] +"\u1EAF" => "a" + +# ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] +"\u1EB1" => "a" + +# ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] +"\u1EB3" => "a" + +# ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] +"\u1EB5" => "a" + +# ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] +"\u1EB7" => "a" + +# ₐ [LATIN SUBSCRIPT SMALL LETTER A] +"\u2090" => "a" + +# ₔ [LATIN SUBSCRIPT SMALL LETTER SCHWA] +"\u2094" => "a" + +# ⓐ [CIRCLED LATIN SMALL LETTER A] +"\u24D0" => "a" + +# ⱥ [LATIN SMALL LETTER A WITH STROKE] +"\u2C65" => "a" + +# Ɐ [LATIN CAPITAL LETTER TURNED A] +"\u2C6F" => "a" + +# a [FULLWIDTH LATIN SMALL LETTER A] +"\uFF41" => "a" + +# Ꜳ [LATIN CAPITAL LETTER AA] +"\uA732" => "AA" + +# Æ [LATIN CAPITAL LETTER AE] +"\u00C6" => "AE" + +# Ǣ [LATIN CAPITAL LETTER AE WITH MACRON] +"\u01E2" => "AE" + +# Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] +"\u01FC" => "AE" + +# ᴁ [LATIN LETTER SMALL CAPITAL AE] +"\u1D01" => "AE" + +# Ꜵ [LATIN CAPITAL LETTER AO] +"\uA734" => "AO" + +# Ꜷ [LATIN CAPITAL LETTER AU] +"\uA736" => "AU" + +# Ꜹ [LATIN CAPITAL LETTER AV] +"\uA738" => "AV" + +# Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] +"\uA73A" => "AV" + +# Ꜽ [LATIN CAPITAL LETTER AY] +"\uA73C" => "AY" + +# ⒜ [PARENTHESIZED LATIN SMALL LETTER A] +"\u249C" => "(a)" + +# ꜳ [LATIN SMALL LETTER AA] +"\uA733" => "aa" + +# æ [LATIN SMALL LETTER AE] +"\u00E6" => "ae" + +# ǣ [LATIN SMALL LETTER AE WITH MACRON] +"\u01E3" => "ae" + +# ǽ [LATIN SMALL LETTER AE WITH ACUTE] +"\u01FD" => "ae" + +# ᴂ [LATIN SMALL LETTER TURNED AE] +"\u1D02" => "ae" + +# ꜵ [LATIN SMALL LETTER AO] +"\uA735" => "ao" + +# ꜷ [LATIN SMALL LETTER AU] +"\uA737" => "au" + +# ꜹ [LATIN SMALL LETTER AV] +"\uA739" => "av" + +# ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] +"\uA73B" => "av" + +# ꜽ [LATIN SMALL LETTER AY] +"\uA73D" => "ay" + +# Ɓ [LATIN CAPITAL LETTER B WITH HOOK] +"\u0181" => "B" + +# Ƃ [LATIN CAPITAL LETTER B WITH TOPBAR] +"\u0182" => "B" + +# Ƀ [LATIN CAPITAL LETTER B WITH STROKE] +"\u0243" => "B" + +# ʙ [LATIN LETTER SMALL CAPITAL B] +"\u0299" => "B" + +# ᴃ [LATIN LETTER SMALL CAPITAL BARRED B] +"\u1D03" => "B" + +# Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] +"\u1E02" => "B" + +# Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] +"\u1E04" => "B" + +# Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] +"\u1E06" => "B" + +# Ⓑ [CIRCLED LATIN CAPITAL LETTER B] +"\u24B7" => "B" + +# B [FULLWIDTH LATIN CAPITAL LETTER B] +"\uFF22" => "B" + +# ƀ [LATIN SMALL LETTER B WITH STROKE] +"\u0180" => "b" + +# ƃ [LATIN SMALL LETTER B WITH TOPBAR] +"\u0183" => "b" + +# ɓ [LATIN SMALL LETTER B WITH HOOK] +"\u0253" => "b" + +# ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] +"\u1D6C" => "b" + +# ᶀ [LATIN SMALL LETTER B WITH PALATAL HOOK] +"\u1D80" => "b" + +# ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] +"\u1E03" => "b" + +# ḅ [LATIN SMALL LETTER B WITH DOT BELOW] +"\u1E05" => "b" + +# ḇ [LATIN SMALL LETTER B WITH LINE BELOW] +"\u1E07" => "b" + +# ⓑ [CIRCLED LATIN SMALL LETTER B] +"\u24D1" => "b" + +# b [FULLWIDTH LATIN SMALL LETTER B] +"\uFF42" => "b" + +# ⒝ [PARENTHESIZED LATIN SMALL LETTER B] +"\u249D" => "(b)" + +# Ç [LATIN CAPITAL LETTER C WITH CEDILLA] +"\u00C7" => "C" + +# Ć [LATIN CAPITAL LETTER C WITH ACUTE] +"\u0106" => "C" + +# Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] +"\u0108" => "C" + +# Ċ [LATIN CAPITAL LETTER C WITH DOT ABOVE] +"\u010A" => "C" + +# Č [LATIN CAPITAL LETTER C WITH CARON] +"\u010C" => "C" + +# Ƈ [LATIN CAPITAL LETTER C WITH HOOK] +"\u0187" => "C" + +# Ȼ [LATIN CAPITAL LETTER C WITH STROKE] +"\u023B" => "C" + +# ʗ [LATIN LETTER STRETCHED C] +"\u0297" => "C" + +# ᴄ [LATIN LETTER SMALL CAPITAL C] +"\u1D04" => "C" + +# Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] +"\u1E08" => "C" + +# Ⓒ [CIRCLED LATIN CAPITAL LETTER C] +"\u24B8" => "C" + +# C [FULLWIDTH LATIN CAPITAL LETTER C] +"\uFF23" => "C" + +# ç [LATIN SMALL LETTER C WITH CEDILLA] +"\u00E7" => "c" + +# ć [LATIN SMALL LETTER C WITH ACUTE] +"\u0107" => "c" + +# ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] +"\u0109" => "c" + +# ċ [LATIN SMALL LETTER C WITH DOT ABOVE] +"\u010B" => "c" + +# č [LATIN SMALL LETTER C WITH CARON] +"\u010D" => "c" + +# ƈ [LATIN SMALL LETTER C WITH HOOK] +"\u0188" => "c" + +# ȼ [LATIN SMALL LETTER C WITH STROKE] +"\u023C" => "c" + +# ɕ [LATIN SMALL LETTER C WITH CURL] +"\u0255" => "c" + +# ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] +"\u1E09" => "c" + +# ↄ [LATIN SMALL LETTER REVERSED C] +"\u2184" => "c" + +# ⓒ [CIRCLED LATIN SMALL LETTER C] +"\u24D2" => "c" + +# Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] +"\uA73E" => "c" + +# ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] +"\uA73F" => "c" + +# c [FULLWIDTH LATIN SMALL LETTER C] +"\uFF43" => "c" + +# ⒞ [PARENTHESIZED LATIN SMALL LETTER C] +"\u249E" => "(c)" + +# Ð [LATIN CAPITAL LETTER ETH] +"\u00D0" => "D" + +# Ď [LATIN CAPITAL LETTER D WITH CARON] +"\u010E" => "D" + +# Đ [LATIN CAPITAL LETTER D WITH STROKE] +"\u0110" => "D" + +# Ɖ [LATIN CAPITAL LETTER AFRICAN D] +"\u0189" => "D" + +# Ɗ [LATIN CAPITAL LETTER D WITH HOOK] +"\u018A" => "D" + +# Ƌ [LATIN CAPITAL LETTER D WITH TOPBAR] +"\u018B" => "D" + +# ᴅ [LATIN LETTER SMALL CAPITAL D] +"\u1D05" => "D" + +# ᴆ [LATIN LETTER SMALL CAPITAL ETH] +"\u1D06" => "D" + +# Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] +"\u1E0A" => "D" + +# Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] +"\u1E0C" => "D" + +# Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] +"\u1E0E" => "D" + +# Ḑ [LATIN CAPITAL LETTER D WITH CEDILLA] +"\u1E10" => "D" + +# Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] +"\u1E12" => "D" + +# Ⓓ [CIRCLED LATIN CAPITAL LETTER D] +"\u24B9" => "D" + +# Ꝺ [LATIN CAPITAL LETTER INSULAR D] +"\uA779" => "D" + +# D [FULLWIDTH LATIN CAPITAL LETTER D] +"\uFF24" => "D" + +# ð [LATIN SMALL LETTER ETH] +"\u00F0" => "d" + +# ď [LATIN SMALL LETTER D WITH CARON] +"\u010F" => "d" + +# đ [LATIN SMALL LETTER D WITH STROKE] +"\u0111" => "d" + +# ƌ [LATIN SMALL LETTER D WITH TOPBAR] +"\u018C" => "d" + +# ȡ [LATIN SMALL LETTER D WITH CURL] +"\u0221" => "d" + +# ɖ [LATIN SMALL LETTER D WITH TAIL] +"\u0256" => "d" + +# ɗ [LATIN SMALL LETTER D WITH HOOK] +"\u0257" => "d" + +# ᵭ [LATIN SMALL LETTER D WITH MIDDLE TILDE] +"\u1D6D" => "d" + +# ᶁ [LATIN SMALL LETTER D WITH PALATAL HOOK] +"\u1D81" => "d" + +# ᶑ [LATIN SMALL LETTER D WITH HOOK AND TAIL] +"\u1D91" => "d" + +# ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] +"\u1E0B" => "d" + +# ḍ [LATIN SMALL LETTER D WITH DOT BELOW] +"\u1E0D" => "d" + +# ḏ [LATIN SMALL LETTER D WITH LINE BELOW] +"\u1E0F" => "d" + +# ḑ [LATIN SMALL LETTER D WITH CEDILLA] +"\u1E11" => "d" + +# ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] +"\u1E13" => "d" + +# ⓓ [CIRCLED LATIN SMALL LETTER D] +"\u24D3" => "d" + +# ꝺ [LATIN SMALL LETTER INSULAR D] +"\uA77A" => "d" + +# d [FULLWIDTH LATIN SMALL LETTER D] +"\uFF44" => "d" + +# DŽ [LATIN CAPITAL LETTER DZ WITH CARON] +"\u01C4" => "DZ" + +# DZ [LATIN CAPITAL LETTER DZ] +"\u01F1" => "DZ" + +# Dž [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] +"\u01C5" => "Dz" + +# Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] +"\u01F2" => "Dz" + +# ⒟ [PARENTHESIZED LATIN SMALL LETTER D] +"\u249F" => "(d)" + +# ȸ [LATIN SMALL LETTER DB DIGRAPH] +"\u0238" => "db" + +# dž [LATIN SMALL LETTER DZ WITH CARON] +"\u01C6" => "dz" + +# dz [LATIN SMALL LETTER DZ] +"\u01F3" => "dz" + +# ʣ [LATIN SMALL LETTER DZ DIGRAPH] +"\u02A3" => "dz" + +# ʥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] +"\u02A5" => "dz" + +# È [LATIN CAPITAL LETTER E WITH GRAVE] +"\u00C8" => "E" + +# É [LATIN CAPITAL LETTER E WITH ACUTE] +"\u00C9" => "E" + +# Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] +"\u00CA" => "E" + +# Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] +"\u00CB" => "E" + +# Ē [LATIN CAPITAL LETTER E WITH MACRON] +"\u0112" => "E" + +# Ĕ [LATIN CAPITAL LETTER E WITH BREVE] +"\u0114" => "E" + +# Ė [LATIN CAPITAL LETTER E WITH DOT ABOVE] +"\u0116" => "E" + +# Ę [LATIN CAPITAL LETTER E WITH OGONEK] +"\u0118" => "E" + +# Ě [LATIN CAPITAL LETTER E WITH CARON] +"\u011A" => "E" + +# Ǝ [LATIN CAPITAL LETTER REVERSED E] +"\u018E" => "E" + +# Ɛ [LATIN CAPITAL LETTER OPEN E] +"\u0190" => "E" + +# Ȅ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] +"\u0204" => "E" + +# Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] +"\u0206" => "E" + +# Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] +"\u0228" => "E" + +# Ɇ [LATIN CAPITAL LETTER E WITH STROKE] +"\u0246" => "E" + +# ᴇ [LATIN LETTER SMALL CAPITAL E] +"\u1D07" => "E" + +# Ḕ [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] +"\u1E14" => "E" + +# Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] +"\u1E16" => "E" + +# Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] +"\u1E18" => "E" + +# Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] +"\u1E1A" => "E" + +# Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] +"\u1E1C" => "E" + +# Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] +"\u1EB8" => "E" + +# Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] +"\u1EBA" => "E" + +# Ẽ [LATIN CAPITAL LETTER E WITH TILDE] +"\u1EBC" => "E" + +# Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] +"\u1EBE" => "E" + +# Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] +"\u1EC0" => "E" + +# Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EC2" => "E" + +# Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] +"\u1EC4" => "E" + +# Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] +"\u1EC6" => "E" + +# Ⓔ [CIRCLED LATIN CAPITAL LETTER E] +"\u24BA" => "E" + +# ⱻ [LATIN LETTER SMALL CAPITAL TURNED E] +"\u2C7B" => "E" + +# E [FULLWIDTH LATIN CAPITAL LETTER E] +"\uFF25" => "E" + +# è [LATIN SMALL LETTER E WITH GRAVE] +"\u00E8" => "e" + +# é [LATIN SMALL LETTER E WITH ACUTE] +"\u00E9" => "e" + +# ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] +"\u00EA" => "e" + +# ë [LATIN SMALL LETTER E WITH DIAERESIS] +"\u00EB" => "e" + +# ē [LATIN SMALL LETTER E WITH MACRON] +"\u0113" => "e" + +# ĕ [LATIN SMALL LETTER E WITH BREVE] +"\u0115" => "e" + +# ė [LATIN SMALL LETTER E WITH DOT ABOVE] +"\u0117" => "e" + +# ę [LATIN SMALL LETTER E WITH OGONEK] +"\u0119" => "e" + +# ě [LATIN SMALL LETTER E WITH CARON] +"\u011B" => "e" + +# ǝ [LATIN SMALL LETTER TURNED E] +"\u01DD" => "e" + +# ȅ [LATIN SMALL LETTER E WITH DOUBLE GRAVE] +"\u0205" => "e" + +# ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] +"\u0207" => "e" + +# ȩ [LATIN SMALL LETTER E WITH CEDILLA] +"\u0229" => "e" + +# ɇ [LATIN SMALL LETTER E WITH STROKE] +"\u0247" => "e" + +# ɘ [LATIN SMALL LETTER REVERSED E] +"\u0258" => "e" + +# ɛ [LATIN SMALL LETTER OPEN E] +"\u025B" => "e" + +# ɜ [LATIN SMALL LETTER REVERSED OPEN E] +"\u025C" => "e" + +# ɝ [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] +"\u025D" => "e" + +# ɞ [LATIN SMALL LETTER CLOSED REVERSED OPEN E] +"\u025E" => "e" + +# ʚ [LATIN SMALL LETTER CLOSED OPEN E] +"\u029A" => "e" + +# ᴈ [LATIN SMALL LETTER TURNED OPEN E] +"\u1D08" => "e" + +# ᶒ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] +"\u1D92" => "e" + +# ᶓ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] +"\u1D93" => "e" + +# ᶔ [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] +"\u1D94" => "e" + +# ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] +"\u1E15" => "e" + +# ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] +"\u1E17" => "e" + +# ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] +"\u1E19" => "e" + +# ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] +"\u1E1B" => "e" + +# ḝ [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] +"\u1E1D" => "e" + +# ẹ [LATIN SMALL LETTER E WITH DOT BELOW] +"\u1EB9" => "e" + +# ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] +"\u1EBB" => "e" + +# ẽ [LATIN SMALL LETTER E WITH TILDE] +"\u1EBD" => "e" + +# ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] +"\u1EBF" => "e" + +# ề [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] +"\u1EC1" => "e" + +# ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EC3" => "e" + +# ễ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] +"\u1EC5" => "e" + +# ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] +"\u1EC7" => "e" + +# ₑ [LATIN SUBSCRIPT SMALL LETTER E] +"\u2091" => "e" + +# ⓔ [CIRCLED LATIN SMALL LETTER E] +"\u24D4" => "e" + +# ⱸ [LATIN SMALL LETTER E WITH NOTCH] +"\u2C78" => "e" + +# e [FULLWIDTH LATIN SMALL LETTER E] +"\uFF45" => "e" + +# ⒠ [PARENTHESIZED LATIN SMALL LETTER E] +"\u24A0" => "(e)" + +# Ƒ [LATIN CAPITAL LETTER F WITH HOOK] +"\u0191" => "F" + +# Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] +"\u1E1E" => "F" + +# Ⓕ [CIRCLED LATIN CAPITAL LETTER F] +"\u24BB" => "F" + +# ꜰ [LATIN LETTER SMALL CAPITAL F] +"\uA730" => "F" + +# Ꝼ [LATIN CAPITAL LETTER INSULAR F] +"\uA77B" => "F" + +# ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] +"\uA7FB" => "F" + +# F [FULLWIDTH LATIN CAPITAL LETTER F] +"\uFF26" => "F" + +# ƒ [LATIN SMALL LETTER F WITH HOOK] +"\u0192" => "f" + +# ᵮ [LATIN SMALL LETTER F WITH MIDDLE TILDE] +"\u1D6E" => "f" + +# ᶂ [LATIN SMALL LETTER F WITH PALATAL HOOK] +"\u1D82" => "f" + +# ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] +"\u1E1F" => "f" + +# ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] +"\u1E9B" => "f" + +# ⓕ [CIRCLED LATIN SMALL LETTER F] +"\u24D5" => "f" + +# ꝼ [LATIN SMALL LETTER INSULAR F] +"\uA77C" => "f" + +# f [FULLWIDTH LATIN SMALL LETTER F] +"\uFF46" => "f" + +# ⒡ [PARENTHESIZED LATIN SMALL LETTER F] +"\u24A1" => "(f)" + +# ff [LATIN SMALL LIGATURE FF] +"\uFB00" => "ff" + +# ffi [LATIN SMALL LIGATURE FFI] +"\uFB03" => "ffi" + +# ffl [LATIN SMALL LIGATURE FFL] +"\uFB04" => "ffl" + +# fi [LATIN SMALL LIGATURE FI] +"\uFB01" => "fi" + +# fl [LATIN SMALL LIGATURE FL] +"\uFB02" => "fl" + +# Ĝ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] +"\u011C" => "G" + +# Ğ [LATIN CAPITAL LETTER G WITH BREVE] +"\u011E" => "G" + +# Ġ [LATIN CAPITAL LETTER G WITH DOT ABOVE] +"\u0120" => "G" + +# Ģ [LATIN CAPITAL LETTER G WITH CEDILLA] +"\u0122" => "G" + +# Ɠ [LATIN CAPITAL LETTER G WITH HOOK] +"\u0193" => "G" + +# Ǥ [LATIN CAPITAL LETTER G WITH STROKE] +"\u01E4" => "G" + +# ǥ [LATIN SMALL LETTER G WITH STROKE] +"\u01E5" => "G" + +# Ǧ [LATIN CAPITAL LETTER G WITH CARON] +"\u01E6" => "G" + +# ǧ [LATIN SMALL LETTER G WITH CARON] +"\u01E7" => "G" + +# Ǵ [LATIN CAPITAL LETTER G WITH ACUTE] +"\u01F4" => "G" + +# ɢ [LATIN LETTER SMALL CAPITAL G] +"\u0262" => "G" + +# ʛ [LATIN LETTER SMALL CAPITAL G WITH HOOK] +"\u029B" => "G" + +# Ḡ [LATIN CAPITAL LETTER G WITH MACRON] +"\u1E20" => "G" + +# Ⓖ [CIRCLED LATIN CAPITAL LETTER G] +"\u24BC" => "G" + +# Ᵹ [LATIN CAPITAL LETTER INSULAR G] +"\uA77D" => "G" + +# Ꝿ [LATIN CAPITAL LETTER TURNED INSULAR G] +"\uA77E" => "G" + +# G [FULLWIDTH LATIN CAPITAL LETTER G] +"\uFF27" => "G" + +# ĝ [LATIN SMALL LETTER G WITH CIRCUMFLEX] +"\u011D" => "g" + +# ğ [LATIN SMALL LETTER G WITH BREVE] +"\u011F" => "g" + +# ġ [LATIN SMALL LETTER G WITH DOT ABOVE] +"\u0121" => "g" + +# ģ [LATIN SMALL LETTER G WITH CEDILLA] +"\u0123" => "g" + +# ǵ [LATIN SMALL LETTER G WITH ACUTE] +"\u01F5" => "g" + +# ɠ [LATIN SMALL LETTER G WITH HOOK] +"\u0260" => "g" + +# ɡ [LATIN SMALL LETTER SCRIPT G] +"\u0261" => "g" + +# ᵷ [LATIN SMALL LETTER TURNED G] +"\u1D77" => "g" + +# ᵹ [LATIN SMALL LETTER INSULAR G] +"\u1D79" => "g" + +# ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] +"\u1D83" => "g" + +# ḡ [LATIN SMALL LETTER G WITH MACRON] +"\u1E21" => "g" + +# ⓖ [CIRCLED LATIN SMALL LETTER G] +"\u24D6" => "g" + +# ꝿ [LATIN SMALL LETTER TURNED INSULAR G] +"\uA77F" => "g" + +# g [FULLWIDTH LATIN SMALL LETTER G] +"\uFF47" => "g" + +# ⒢ [PARENTHESIZED LATIN SMALL LETTER G] +"\u24A2" => "(g)" + +# Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] +"\u0124" => "H" + +# Ħ [LATIN CAPITAL LETTER H WITH STROKE] +"\u0126" => "H" + +# Ȟ [LATIN CAPITAL LETTER H WITH CARON] +"\u021E" => "H" + +# ʜ [LATIN LETTER SMALL CAPITAL H] +"\u029C" => "H" + +# Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] +"\u1E22" => "H" + +# Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] +"\u1E24" => "H" + +# Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] +"\u1E26" => "H" + +# Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] +"\u1E28" => "H" + +# Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] +"\u1E2A" => "H" + +# Ⓗ [CIRCLED LATIN CAPITAL LETTER H] +"\u24BD" => "H" + +# Ⱨ [LATIN CAPITAL LETTER H WITH DESCENDER] +"\u2C67" => "H" + +# Ⱶ [LATIN CAPITAL LETTER HALF H] +"\u2C75" => "H" + +# H [FULLWIDTH LATIN CAPITAL LETTER H] +"\uFF28" => "H" + +# ĥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] +"\u0125" => "h" + +# ħ [LATIN SMALL LETTER H WITH STROKE] +"\u0127" => "h" + +# ȟ [LATIN SMALL LETTER H WITH CARON] +"\u021F" => "h" + +# ɥ [LATIN SMALL LETTER TURNED H] +"\u0265" => "h" + +# ɦ [LATIN SMALL LETTER H WITH HOOK] +"\u0266" => "h" + +# ʮ [LATIN SMALL LETTER TURNED H WITH FISHHOOK] +"\u02AE" => "h" + +# ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] +"\u02AF" => "h" + +# ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] +"\u1E23" => "h" + +# ḥ [LATIN SMALL LETTER H WITH DOT BELOW] +"\u1E25" => "h" + +# ḧ [LATIN SMALL LETTER H WITH DIAERESIS] +"\u1E27" => "h" + +# ḩ [LATIN SMALL LETTER H WITH CEDILLA] +"\u1E29" => "h" + +# ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] +"\u1E2B" => "h" + +# ẖ [LATIN SMALL LETTER H WITH LINE BELOW] +"\u1E96" => "h" + +# ⓗ [CIRCLED LATIN SMALL LETTER H] +"\u24D7" => "h" + +# ⱨ [LATIN SMALL LETTER H WITH DESCENDER] +"\u2C68" => "h" + +# ⱶ [LATIN SMALL LETTER HALF H] +"\u2C76" => "h" + +# h [FULLWIDTH LATIN SMALL LETTER H] +"\uFF48" => "h" + +# Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] +"\u01F6" => "HV" + +# ⒣ [PARENTHESIZED LATIN SMALL LETTER H] +"\u24A3" => "(h)" + +# ƕ [LATIN SMALL LETTER HV] +"\u0195" => "hv" + +# Ì [LATIN CAPITAL LETTER I WITH GRAVE] +"\u00CC" => "I" + +# Í [LATIN CAPITAL LETTER I WITH ACUTE] +"\u00CD" => "I" + +# Î [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] +"\u00CE" => "I" + +# Ï [LATIN CAPITAL LETTER I WITH DIAERESIS] +"\u00CF" => "I" + +# Ĩ [LATIN CAPITAL LETTER I WITH TILDE] +"\u0128" => "I" + +# Ī [LATIN CAPITAL LETTER I WITH MACRON] +"\u012A" => "I" + +# Ĭ [LATIN CAPITAL LETTER I WITH BREVE] +"\u012C" => "I" + +# Į [LATIN CAPITAL LETTER I WITH OGONEK] +"\u012E" => "I" + +# İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] +"\u0130" => "I" + +# Ɩ [LATIN CAPITAL LETTER IOTA] +"\u0196" => "I" + +# Ɨ [LATIN CAPITAL LETTER I WITH STROKE] +"\u0197" => "I" + +# Ǐ [LATIN CAPITAL LETTER I WITH CARON] +"\u01CF" => "I" + +# Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] +"\u0208" => "I" + +# Ȋ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] +"\u020A" => "I" + +# ɪ [LATIN LETTER SMALL CAPITAL I] +"\u026A" => "I" + +# ᵻ [LATIN SMALL CAPITAL LETTER I WITH STROKE] +"\u1D7B" => "I" + +# Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] +"\u1E2C" => "I" + +# Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] +"\u1E2E" => "I" + +# Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] +"\u1EC8" => "I" + +# Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] +"\u1ECA" => "I" + +# Ⓘ [CIRCLED LATIN CAPITAL LETTER I] +"\u24BE" => "I" + +# ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] +"\uA7FE" => "I" + +# I [FULLWIDTH LATIN CAPITAL LETTER I] +"\uFF29" => "I" + +# ì [LATIN SMALL LETTER I WITH GRAVE] +"\u00EC" => "i" + +# í [LATIN SMALL LETTER I WITH ACUTE] +"\u00ED" => "i" + +# î [LATIN SMALL LETTER I WITH CIRCUMFLEX] +"\u00EE" => "i" + +# ï [LATIN SMALL LETTER I WITH DIAERESIS] +"\u00EF" => "i" + +# ĩ [LATIN SMALL LETTER I WITH TILDE] +"\u0129" => "i" + +# ī [LATIN SMALL LETTER I WITH MACRON] +"\u012B" => "i" + +# ĭ [LATIN SMALL LETTER I WITH BREVE] +"\u012D" => "i" + +# į [LATIN SMALL LETTER I WITH OGONEK] +"\u012F" => "i" + +# ı [LATIN SMALL LETTER DOTLESS I] +"\u0131" => "i" + +# ǐ [LATIN SMALL LETTER I WITH CARON] +"\u01D0" => "i" + +# ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] +"\u0209" => "i" + +# ȋ [LATIN SMALL LETTER I WITH INVERTED BREVE] +"\u020B" => "i" + +# ɨ [LATIN SMALL LETTER I WITH STROKE] +"\u0268" => "i" + +# ᴉ [LATIN SMALL LETTER TURNED I] +"\u1D09" => "i" + +# ᵢ [LATIN SUBSCRIPT SMALL LETTER I] +"\u1D62" => "i" + +# ᵼ [LATIN SMALL LETTER IOTA WITH STROKE] +"\u1D7C" => "i" + +# ᶖ [LATIN SMALL LETTER I WITH RETROFLEX HOOK] +"\u1D96" => "i" + +# ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] +"\u1E2D" => "i" + +# ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] +"\u1E2F" => "i" + +# ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] +"\u1EC9" => "i" + +# ị [LATIN SMALL LETTER I WITH DOT BELOW] +"\u1ECB" => "i" + +# ⁱ [SUPERSCRIPT LATIN SMALL LETTER I] +"\u2071" => "i" + +# ⓘ [CIRCLED LATIN SMALL LETTER I] +"\u24D8" => "i" + +# i [FULLWIDTH LATIN SMALL LETTER I] +"\uFF49" => "i" + +# IJ [LATIN CAPITAL LIGATURE IJ] +"\u0132" => "IJ" + +# ⒤ [PARENTHESIZED LATIN SMALL LETTER I] +"\u24A4" => "(i)" + +# ij [LATIN SMALL LIGATURE IJ] +"\u0133" => "ij" + +# Ĵ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] +"\u0134" => "J" + +# Ɉ [LATIN CAPITAL LETTER J WITH STROKE] +"\u0248" => "J" + +# ᴊ [LATIN LETTER SMALL CAPITAL J] +"\u1D0A" => "J" + +# Ⓙ [CIRCLED LATIN CAPITAL LETTER J] +"\u24BF" => "J" + +# J [FULLWIDTH LATIN CAPITAL LETTER J] +"\uFF2A" => "J" + +# ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] +"\u0135" => "j" + +# ǰ [LATIN SMALL LETTER J WITH CARON] +"\u01F0" => "j" + +# ȷ [LATIN SMALL LETTER DOTLESS J] +"\u0237" => "j" + +# ɉ [LATIN SMALL LETTER J WITH STROKE] +"\u0249" => "j" + +# ɟ [LATIN SMALL LETTER DOTLESS J WITH STROKE] +"\u025F" => "j" + +# ʄ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] +"\u0284" => "j" + +# ʝ [LATIN SMALL LETTER J WITH CROSSED-TAIL] +"\u029D" => "j" + +# ⓙ [CIRCLED LATIN SMALL LETTER J] +"\u24D9" => "j" + +# ⱼ [LATIN SUBSCRIPT SMALL LETTER J] +"\u2C7C" => "j" + +# j [FULLWIDTH LATIN SMALL LETTER J] +"\uFF4A" => "j" + +# ⒥ [PARENTHESIZED LATIN SMALL LETTER J] +"\u24A5" => "(j)" + +# Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] +"\u0136" => "K" + +# Ƙ [LATIN CAPITAL LETTER K WITH HOOK] +"\u0198" => "K" + +# Ǩ [LATIN CAPITAL LETTER K WITH CARON] +"\u01E8" => "K" + +# ᴋ [LATIN LETTER SMALL CAPITAL K] +"\u1D0B" => "K" + +# Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] +"\u1E30" => "K" + +# Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] +"\u1E32" => "K" + +# Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] +"\u1E34" => "K" + +# Ⓚ [CIRCLED LATIN CAPITAL LETTER K] +"\u24C0" => "K" + +# Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] +"\u2C69" => "K" + +# Ꝁ [LATIN CAPITAL LETTER K WITH STROKE] +"\uA740" => "K" + +# Ꝃ [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] +"\uA742" => "K" + +# Ꝅ [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] +"\uA744" => "K" + +# K [FULLWIDTH LATIN CAPITAL LETTER K] +"\uFF2B" => "K" + +# ķ [LATIN SMALL LETTER K WITH CEDILLA] +"\u0137" => "k" + +# ƙ [LATIN SMALL LETTER K WITH HOOK] +"\u0199" => "k" + +# ǩ [LATIN SMALL LETTER K WITH CARON] +"\u01E9" => "k" + +# ʞ [LATIN SMALL LETTER TURNED K] +"\u029E" => "k" + +# ᶄ [LATIN SMALL LETTER K WITH PALATAL HOOK] +"\u1D84" => "k" + +# ḱ [LATIN SMALL LETTER K WITH ACUTE] +"\u1E31" => "k" + +# ḳ [LATIN SMALL LETTER K WITH DOT BELOW] +"\u1E33" => "k" + +# ḵ [LATIN SMALL LETTER K WITH LINE BELOW] +"\u1E35" => "k" + +# ⓚ [CIRCLED LATIN SMALL LETTER K] +"\u24DA" => "k" + +# ⱪ [LATIN SMALL LETTER K WITH DESCENDER] +"\u2C6A" => "k" + +# ꝁ [LATIN SMALL LETTER K WITH STROKE] +"\uA741" => "k" + +# ꝃ [LATIN SMALL LETTER K WITH DIAGONAL STROKE] +"\uA743" => "k" + +# ꝅ [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] +"\uA745" => "k" + +# k [FULLWIDTH LATIN SMALL LETTER K] +"\uFF4B" => "k" + +# ⒦ [PARENTHESIZED LATIN SMALL LETTER K] +"\u24A6" => "(k)" + +# Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] +"\u0139" => "L" + +# Ļ [LATIN CAPITAL LETTER L WITH CEDILLA] +"\u013B" => "L" + +# Ľ [LATIN CAPITAL LETTER L WITH CARON] +"\u013D" => "L" + +# Ŀ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] +"\u013F" => "L" + +# Ł [LATIN CAPITAL LETTER L WITH STROKE] +"\u0141" => "L" + +# Ƚ [LATIN CAPITAL LETTER L WITH BAR] +"\u023D" => "L" + +# ʟ [LATIN LETTER SMALL CAPITAL L] +"\u029F" => "L" + +# ᴌ [LATIN LETTER SMALL CAPITAL L WITH STROKE] +"\u1D0C" => "L" + +# Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] +"\u1E36" => "L" + +# Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] +"\u1E38" => "L" + +# Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] +"\u1E3A" => "L" + +# Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] +"\u1E3C" => "L" + +# Ⓛ [CIRCLED LATIN CAPITAL LETTER L] +"\u24C1" => "L" + +# Ⱡ [LATIN CAPITAL LETTER L WITH DOUBLE BAR] +"\u2C60" => "L" + +# Ɫ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] +"\u2C62" => "L" + +# Ꝇ [LATIN CAPITAL LETTER BROKEN L] +"\uA746" => "L" + +# Ꝉ [LATIN CAPITAL LETTER L WITH HIGH STROKE] +"\uA748" => "L" + +# Ꞁ [LATIN CAPITAL LETTER TURNED L] +"\uA780" => "L" + +# L [FULLWIDTH LATIN CAPITAL LETTER L] +"\uFF2C" => "L" + +# ĺ [LATIN SMALL LETTER L WITH ACUTE] +"\u013A" => "l" + +# ļ [LATIN SMALL LETTER L WITH CEDILLA] +"\u013C" => "l" + +# ľ [LATIN SMALL LETTER L WITH CARON] +"\u013E" => "l" + +# ŀ [LATIN SMALL LETTER L WITH MIDDLE DOT] +"\u0140" => "l" + +# ł [LATIN SMALL LETTER L WITH STROKE] +"\u0142" => "l" + +# ƚ [LATIN SMALL LETTER L WITH BAR] +"\u019A" => "l" + +# ȴ [LATIN SMALL LETTER L WITH CURL] +"\u0234" => "l" + +# ɫ [LATIN SMALL LETTER L WITH MIDDLE TILDE] +"\u026B" => "l" + +# ɬ [LATIN SMALL LETTER L WITH BELT] +"\u026C" => "l" + +# ɭ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] +"\u026D" => "l" + +# ᶅ [LATIN SMALL LETTER L WITH PALATAL HOOK] +"\u1D85" => "l" + +# ḷ [LATIN SMALL LETTER L WITH DOT BELOW] +"\u1E37" => "l" + +# ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] +"\u1E39" => "l" + +# ḻ [LATIN SMALL LETTER L WITH LINE BELOW] +"\u1E3B" => "l" + +# ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] +"\u1E3D" => "l" + +# ⓛ [CIRCLED LATIN SMALL LETTER L] +"\u24DB" => "l" + +# ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] +"\u2C61" => "l" + +# ꝇ [LATIN SMALL LETTER BROKEN L] +"\uA747" => "l" + +# ꝉ [LATIN SMALL LETTER L WITH HIGH STROKE] +"\uA749" => "l" + +# ꞁ [LATIN SMALL LETTER TURNED L] +"\uA781" => "l" + +# l [FULLWIDTH LATIN SMALL LETTER L] +"\uFF4C" => "l" + +# LJ [LATIN CAPITAL LETTER LJ] +"\u01C7" => "LJ" + +# Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] +"\u1EFA" => "LL" + +# Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] +"\u01C8" => "Lj" + +# ⒧ [PARENTHESIZED LATIN SMALL LETTER L] +"\u24A7" => "(l)" + +# lj [LATIN SMALL LETTER LJ] +"\u01C9" => "lj" + +# ỻ [LATIN SMALL LETTER MIDDLE-WELSH LL] +"\u1EFB" => "ll" + +# ʪ [LATIN SMALL LETTER LS DIGRAPH] +"\u02AA" => "ls" + +# ʫ [LATIN SMALL LETTER LZ DIGRAPH] +"\u02AB" => "lz" + +# Ɯ [LATIN CAPITAL LETTER TURNED M] +"\u019C" => "M" + +# ᴍ [LATIN LETTER SMALL CAPITAL M] +"\u1D0D" => "M" + +# Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] +"\u1E3E" => "M" + +# Ṁ [LATIN CAPITAL LETTER M WITH DOT ABOVE] +"\u1E40" => "M" + +# Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] +"\u1E42" => "M" + +# Ⓜ [CIRCLED LATIN CAPITAL LETTER M] +"\u24C2" => "M" + +# Ɱ [LATIN CAPITAL LETTER M WITH HOOK] +"\u2C6E" => "M" + +# ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] +"\uA7FD" => "M" + +# ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] +"\uA7FF" => "M" + +# M [FULLWIDTH LATIN CAPITAL LETTER M] +"\uFF2D" => "M" + +# ɯ [LATIN SMALL LETTER TURNED M] +"\u026F" => "m" + +# ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] +"\u0270" => "m" + +# ɱ [LATIN SMALL LETTER M WITH HOOK] +"\u0271" => "m" + +# ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] +"\u1D6F" => "m" + +# ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] +"\u1D86" => "m" + +# ḿ [LATIN SMALL LETTER M WITH ACUTE] +"\u1E3F" => "m" + +# ṁ [LATIN SMALL LETTER M WITH DOT ABOVE] +"\u1E41" => "m" + +# ṃ [LATIN SMALL LETTER M WITH DOT BELOW] +"\u1E43" => "m" + +# ⓜ [CIRCLED LATIN SMALL LETTER M] +"\u24DC" => "m" + +# m [FULLWIDTH LATIN SMALL LETTER M] +"\uFF4D" => "m" + +# ⒨ [PARENTHESIZED LATIN SMALL LETTER M] +"\u24A8" => "(m)" + +# Ñ [LATIN CAPITAL LETTER N WITH TILDE] +"\u00D1" => "N" + +# Ń [LATIN CAPITAL LETTER N WITH ACUTE] +"\u0143" => "N" + +# Ņ [LATIN CAPITAL LETTER N WITH CEDILLA] +"\u0145" => "N" + +# Ň [LATIN CAPITAL LETTER N WITH CARON] +"\u0147" => "N" + +# Ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] +"\u014A" => "N" + +# Ɲ [LATIN CAPITAL LETTER N WITH LEFT HOOK] +"\u019D" => "N" + +# Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] +"\u01F8" => "N" + +# Ƞ [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] +"\u0220" => "N" + +# ɴ [LATIN LETTER SMALL CAPITAL N] +"\u0274" => "N" + +# ᴎ [LATIN LETTER SMALL CAPITAL REVERSED N] +"\u1D0E" => "N" + +# Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] +"\u1E44" => "N" + +# Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] +"\u1E46" => "N" + +# Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] +"\u1E48" => "N" + +# Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] +"\u1E4A" => "N" + +# Ⓝ [CIRCLED LATIN CAPITAL LETTER N] +"\u24C3" => "N" + +# N [FULLWIDTH LATIN CAPITAL LETTER N] +"\uFF2E" => "N" + +# ñ [LATIN SMALL LETTER N WITH TILDE] +"\u00F1" => "n" + +# ń [LATIN SMALL LETTER N WITH ACUTE] +"\u0144" => "n" + +# ņ [LATIN SMALL LETTER N WITH CEDILLA] +"\u0146" => "n" + +# ň [LATIN SMALL LETTER N WITH CARON] +"\u0148" => "n" + +# ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] +"\u0149" => "n" + +# ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] +"\u014B" => "n" + +# ƞ [LATIN SMALL LETTER N WITH LONG RIGHT LEG] +"\u019E" => "n" + +# ǹ [LATIN SMALL LETTER N WITH GRAVE] +"\u01F9" => "n" + +# ȵ [LATIN SMALL LETTER N WITH CURL] +"\u0235" => "n" + +# ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] +"\u0272" => "n" + +# ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] +"\u0273" => "n" + +# ᵰ [LATIN SMALL LETTER N WITH MIDDLE TILDE] +"\u1D70" => "n" + +# ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] +"\u1D87" => "n" + +# ṅ [LATIN SMALL LETTER N WITH DOT ABOVE] +"\u1E45" => "n" + +# ṇ [LATIN SMALL LETTER N WITH DOT BELOW] +"\u1E47" => "n" + +# ṉ [LATIN SMALL LETTER N WITH LINE BELOW] +"\u1E49" => "n" + +# ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] +"\u1E4B" => "n" + +# ⁿ [SUPERSCRIPT LATIN SMALL LETTER N] +"\u207F" => "n" + +# ⓝ [CIRCLED LATIN SMALL LETTER N] +"\u24DD" => "n" + +# n [FULLWIDTH LATIN SMALL LETTER N] +"\uFF4E" => "n" + +# NJ [LATIN CAPITAL LETTER NJ] +"\u01CA" => "NJ" + +# Nj [LATIN CAPITAL LETTER N WITH SMALL LETTER J] +"\u01CB" => "Nj" + +# ⒩ [PARENTHESIZED LATIN SMALL LETTER N] +"\u24A9" => "(n)" + +# nj [LATIN SMALL LETTER NJ] +"\u01CC" => "nj" + +# Ò [LATIN CAPITAL LETTER O WITH GRAVE] +"\u00D2" => "O" + +# Ó [LATIN CAPITAL LETTER O WITH ACUTE] +"\u00D3" => "O" + +# Ô [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] +"\u00D4" => "O" + +# Õ [LATIN CAPITAL LETTER O WITH TILDE] +"\u00D5" => "O" + +# Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] +"\u00D6" => "O" + +# Ø [LATIN CAPITAL LETTER O WITH STROKE] +"\u00D8" => "O" + +# Ō [LATIN CAPITAL LETTER O WITH MACRON] +"\u014C" => "O" + +# Ŏ [LATIN CAPITAL LETTER O WITH BREVE] +"\u014E" => "O" + +# Ő [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] +"\u0150" => "O" + +# Ɔ [LATIN CAPITAL LETTER OPEN O] +"\u0186" => "O" + +# Ɵ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] +"\u019F" => "O" + +# Ơ [LATIN CAPITAL LETTER O WITH HORN] +"\u01A0" => "O" + +# Ǒ [LATIN CAPITAL LETTER O WITH CARON] +"\u01D1" => "O" + +# Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] +"\u01EA" => "O" + +# Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] +"\u01EC" => "O" + +# Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] +"\u01FE" => "O" + +# Ȍ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] +"\u020C" => "O" + +# Ȏ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] +"\u020E" => "O" + +# Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] +"\u022A" => "O" + +# Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] +"\u022C" => "O" + +# Ȯ [LATIN CAPITAL LETTER O WITH DOT ABOVE] +"\u022E" => "O" + +# Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] +"\u0230" => "O" + +# ᴏ [LATIN LETTER SMALL CAPITAL O] +"\u1D0F" => "O" + +# ᴐ [LATIN LETTER SMALL CAPITAL OPEN O] +"\u1D10" => "O" + +# Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] +"\u1E4C" => "O" + +# Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] +"\u1E4E" => "O" + +# Ṑ [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] +"\u1E50" => "O" + +# Ṓ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] +"\u1E52" => "O" + +# Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] +"\u1ECC" => "O" + +# Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] +"\u1ECE" => "O" + +# Ố [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] +"\u1ED0" => "O" + +# Ồ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] +"\u1ED2" => "O" + +# Ổ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1ED4" => "O" + +# Ỗ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] +"\u1ED6" => "O" + +# Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] +"\u1ED8" => "O" + +# Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] +"\u1EDA" => "O" + +# Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] +"\u1EDC" => "O" + +# Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] +"\u1EDE" => "O" + +# Ỡ [LATIN CAPITAL LETTER O WITH HORN AND TILDE] +"\u1EE0" => "O" + +# Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] +"\u1EE2" => "O" + +# Ⓞ [CIRCLED LATIN CAPITAL LETTER O] +"\u24C4" => "O" + +# Ꝋ [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] +"\uA74A" => "O" + +# Ꝍ [LATIN CAPITAL LETTER O WITH LOOP] +"\uA74C" => "O" + +# O [FULLWIDTH LATIN CAPITAL LETTER O] +"\uFF2F" => "O" + +# ò [LATIN SMALL LETTER O WITH GRAVE] +"\u00F2" => "o" + +# ó [LATIN SMALL LETTER O WITH ACUTE] +"\u00F3" => "o" + +# ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] +"\u00F4" => "o" + +# õ [LATIN SMALL LETTER O WITH TILDE] +"\u00F5" => "o" + +# ö [LATIN SMALL LETTER O WITH DIAERESIS] +"\u00F6" => "o" + +# ø [LATIN SMALL LETTER O WITH STROKE] +"\u00F8" => "o" + +# ō [LATIN SMALL LETTER O WITH MACRON] +"\u014D" => "o" + +# ŏ [LATIN SMALL LETTER O WITH BREVE] +"\u014F" => "o" + +# ő [LATIN SMALL LETTER O WITH DOUBLE ACUTE] +"\u0151" => "o" + +# ơ [LATIN SMALL LETTER O WITH HORN] +"\u01A1" => "o" + +# ǒ [LATIN SMALL LETTER O WITH CARON] +"\u01D2" => "o" + +# ǫ [LATIN SMALL LETTER O WITH OGONEK] +"\u01EB" => "o" + +# ǭ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] +"\u01ED" => "o" + +# ǿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] +"\u01FF" => "o" + +# ȍ [LATIN SMALL LETTER O WITH DOUBLE GRAVE] +"\u020D" => "o" + +# ȏ [LATIN SMALL LETTER O WITH INVERTED BREVE] +"\u020F" => "o" + +# ȫ [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] +"\u022B" => "o" + +# ȭ [LATIN SMALL LETTER O WITH TILDE AND MACRON] +"\u022D" => "o" + +# ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] +"\u022F" => "o" + +# ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] +"\u0231" => "o" + +# ɔ [LATIN SMALL LETTER OPEN O] +"\u0254" => "o" + +# ɵ [LATIN SMALL LETTER BARRED O] +"\u0275" => "o" + +# ᴖ [LATIN SMALL LETTER TOP HALF O] +"\u1D16" => "o" + +# ᴗ [LATIN SMALL LETTER BOTTOM HALF O] +"\u1D17" => "o" + +# ᶗ [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] +"\u1D97" => "o" + +# ṍ [LATIN SMALL LETTER O WITH TILDE AND ACUTE] +"\u1E4D" => "o" + +# ṏ [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] +"\u1E4F" => "o" + +# ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] +"\u1E51" => "o" + +# ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] +"\u1E53" => "o" + +# ọ [LATIN SMALL LETTER O WITH DOT BELOW] +"\u1ECD" => "o" + +# ỏ [LATIN SMALL LETTER O WITH HOOK ABOVE] +"\u1ECF" => "o" + +# ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] +"\u1ED1" => "o" + +# ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] +"\u1ED3" => "o" + +# ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1ED5" => "o" + +# ỗ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] +"\u1ED7" => "o" + +# ộ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] +"\u1ED9" => "o" + +# ớ [LATIN SMALL LETTER O WITH HORN AND ACUTE] +"\u1EDB" => "o" + +# ờ [LATIN SMALL LETTER O WITH HORN AND GRAVE] +"\u1EDD" => "o" + +# ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] +"\u1EDF" => "o" + +# ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] +"\u1EE1" => "o" + +# ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] +"\u1EE3" => "o" + +# ₒ [LATIN SUBSCRIPT SMALL LETTER O] +"\u2092" => "o" + +# ⓞ [CIRCLED LATIN SMALL LETTER O] +"\u24DE" => "o" + +# ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] +"\u2C7A" => "o" + +# ꝋ [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] +"\uA74B" => "o" + +# ꝍ [LATIN SMALL LETTER O WITH LOOP] +"\uA74D" => "o" + +# o [FULLWIDTH LATIN SMALL LETTER O] +"\uFF4F" => "o" + +# Œ [LATIN CAPITAL LIGATURE OE] +"\u0152" => "OE" + +# ɶ [LATIN LETTER SMALL CAPITAL OE] +"\u0276" => "OE" + +# Ꝏ [LATIN CAPITAL LETTER OO] +"\uA74E" => "OO" + +# Ȣ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] +"\u0222" => "OU" + +# ᴕ [LATIN LETTER SMALL CAPITAL OU] +"\u1D15" => "OU" + +# ⒪ [PARENTHESIZED LATIN SMALL LETTER O] +"\u24AA" => "(o)" + +# œ [LATIN SMALL LIGATURE OE] +"\u0153" => "oe" + +# ᴔ [LATIN SMALL LETTER TURNED OE] +"\u1D14" => "oe" + +# ꝏ [LATIN SMALL LETTER OO] +"\uA74F" => "oo" + +# ȣ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] +"\u0223" => "ou" + +# Ƥ [LATIN CAPITAL LETTER P WITH HOOK] +"\u01A4" => "P" + +# ᴘ [LATIN LETTER SMALL CAPITAL P] +"\u1D18" => "P" + +# Ṕ [LATIN CAPITAL LETTER P WITH ACUTE] +"\u1E54" => "P" + +# Ṗ [LATIN CAPITAL LETTER P WITH DOT ABOVE] +"\u1E56" => "P" + +# Ⓟ [CIRCLED LATIN CAPITAL LETTER P] +"\u24C5" => "P" + +# Ᵽ [LATIN CAPITAL LETTER P WITH STROKE] +"\u2C63" => "P" + +# Ꝑ [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] +"\uA750" => "P" + +# Ꝓ [LATIN CAPITAL LETTER P WITH FLOURISH] +"\uA752" => "P" + +# Ꝕ [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] +"\uA754" => "P" + +# P [FULLWIDTH LATIN CAPITAL LETTER P] +"\uFF30" => "P" + +# ƥ [LATIN SMALL LETTER P WITH HOOK] +"\u01A5" => "p" + +# ᵱ [LATIN SMALL LETTER P WITH MIDDLE TILDE] +"\u1D71" => "p" + +# ᵽ [LATIN SMALL LETTER P WITH STROKE] +"\u1D7D" => "p" + +# ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] +"\u1D88" => "p" + +# ṕ [LATIN SMALL LETTER P WITH ACUTE] +"\u1E55" => "p" + +# ṗ [LATIN SMALL LETTER P WITH DOT ABOVE] +"\u1E57" => "p" + +# ⓟ [CIRCLED LATIN SMALL LETTER P] +"\u24DF" => "p" + +# ꝑ [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] +"\uA751" => "p" + +# ꝓ [LATIN SMALL LETTER P WITH FLOURISH] +"\uA753" => "p" + +# ꝕ [LATIN SMALL LETTER P WITH SQUIRREL TAIL] +"\uA755" => "p" + +# ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] +"\uA7FC" => "p" + +# p [FULLWIDTH LATIN SMALL LETTER P] +"\uFF50" => "p" + +# ⒫ [PARENTHESIZED LATIN SMALL LETTER P] +"\u24AB" => "(p)" + +# Ɋ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] +"\u024A" => "Q" + +# Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] +"\u24C6" => "Q" + +# Ꝗ [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] +"\uA756" => "Q" + +# Ꝙ [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] +"\uA758" => "Q" + +# Q [FULLWIDTH LATIN CAPITAL LETTER Q] +"\uFF31" => "Q" + +# ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] +"\u0138" => "q" + +# ɋ [LATIN SMALL LETTER Q WITH HOOK TAIL] +"\u024B" => "q" + +# ʠ [LATIN SMALL LETTER Q WITH HOOK] +"\u02A0" => "q" + +# ⓠ [CIRCLED LATIN SMALL LETTER Q] +"\u24E0" => "q" + +# ꝗ [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] +"\uA757" => "q" + +# ꝙ [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] +"\uA759" => "q" + +# q [FULLWIDTH LATIN SMALL LETTER Q] +"\uFF51" => "q" + +# ⒬ [PARENTHESIZED LATIN SMALL LETTER Q] +"\u24AC" => "(q)" + +# ȹ [LATIN SMALL LETTER QP DIGRAPH] +"\u0239" => "qp" + +# Ŕ [LATIN CAPITAL LETTER R WITH ACUTE] +"\u0154" => "R" + +# Ŗ [LATIN CAPITAL LETTER R WITH CEDILLA] +"\u0156" => "R" + +# Ř [LATIN CAPITAL LETTER R WITH CARON] +"\u0158" => "R" + +# Ȓ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] +"\u0210" => "R" + +# Ȓ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] +"\u0212" => "R" + +# Ɍ [LATIN CAPITAL LETTER R WITH STROKE] +"\u024C" => "R" + +# ʀ [LATIN LETTER SMALL CAPITAL R] +"\u0280" => "R" + +# ʁ [LATIN LETTER SMALL CAPITAL INVERTED R] +"\u0281" => "R" + +# ᴙ [LATIN LETTER SMALL CAPITAL REVERSED R] +"\u1D19" => "R" + +# ᴚ [LATIN LETTER SMALL CAPITAL TURNED R] +"\u1D1A" => "R" + +# Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] +"\u1E58" => "R" + +# Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] +"\u1E5A" => "R" + +# Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] +"\u1E5C" => "R" + +# Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] +"\u1E5E" => "R" + +# Ⓡ [CIRCLED LATIN CAPITAL LETTER R] +"\u24C7" => "R" + +# Ɽ [LATIN CAPITAL LETTER R WITH TAIL] +"\u2C64" => "R" + +# Ꝛ [LATIN CAPITAL LETTER R ROTUNDA] +"\uA75A" => "R" + +# Ꞃ [LATIN CAPITAL LETTER INSULAR R] +"\uA782" => "R" + +# R [FULLWIDTH LATIN CAPITAL LETTER R] +"\uFF32" => "R" + +# ŕ [LATIN SMALL LETTER R WITH ACUTE] +"\u0155" => "r" + +# ŗ [LATIN SMALL LETTER R WITH CEDILLA] +"\u0157" => "r" + +# ř [LATIN SMALL LETTER R WITH CARON] +"\u0159" => "r" + +# ȑ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] +"\u0211" => "r" + +# ȓ [LATIN SMALL LETTER R WITH INVERTED BREVE] +"\u0213" => "r" + +# ɍ [LATIN SMALL LETTER R WITH STROKE] +"\u024D" => "r" + +# ɼ [LATIN SMALL LETTER R WITH LONG LEG] +"\u027C" => "r" + +# ɽ [LATIN SMALL LETTER R WITH TAIL] +"\u027D" => "r" + +# ɾ [LATIN SMALL LETTER R WITH FISHHOOK] +"\u027E" => "r" + +# ɿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] +"\u027F" => "r" + +# ᵣ [LATIN SUBSCRIPT SMALL LETTER R] +"\u1D63" => "r" + +# ᵲ [LATIN SMALL LETTER R WITH MIDDLE TILDE] +"\u1D72" => "r" + +# ᵳ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] +"\u1D73" => "r" + +# ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] +"\u1D89" => "r" + +# ṙ [LATIN SMALL LETTER R WITH DOT ABOVE] +"\u1E59" => "r" + +# ṛ [LATIN SMALL LETTER R WITH DOT BELOW] +"\u1E5B" => "r" + +# ṝ [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] +"\u1E5D" => "r" + +# ṟ [LATIN SMALL LETTER R WITH LINE BELOW] +"\u1E5F" => "r" + +# ⓡ [CIRCLED LATIN SMALL LETTER R] +"\u24E1" => "r" + +# ꝛ [LATIN SMALL LETTER R ROTUNDA] +"\uA75B" => "r" + +# ꞃ [LATIN SMALL LETTER INSULAR R] +"\uA783" => "r" + +# r [FULLWIDTH LATIN SMALL LETTER R] +"\uFF52" => "r" + +# ⒭ [PARENTHESIZED LATIN SMALL LETTER R] +"\u24AD" => "(r)" + +# Ś [LATIN CAPITAL LETTER S WITH ACUTE] +"\u015A" => "S" + +# Ŝ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] +"\u015C" => "S" + +# Ş [LATIN CAPITAL LETTER S WITH CEDILLA] +"\u015E" => "S" + +# Š [LATIN CAPITAL LETTER S WITH CARON] +"\u0160" => "S" + +# Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] +"\u0218" => "S" + +# Ṡ [LATIN CAPITAL LETTER S WITH DOT ABOVE] +"\u1E60" => "S" + +# Ṣ [LATIN CAPITAL LETTER S WITH DOT BELOW] +"\u1E62" => "S" + +# Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] +"\u1E64" => "S" + +# Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] +"\u1E66" => "S" + +# Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] +"\u1E68" => "S" + +# Ⓢ [CIRCLED LATIN CAPITAL LETTER S] +"\u24C8" => "S" + +# ꜱ [LATIN LETTER SMALL CAPITAL S] +"\uA731" => "S" + +# ꞅ [LATIN SMALL LETTER INSULAR S] +"\uA785" => "S" + +# S [FULLWIDTH LATIN CAPITAL LETTER S] +"\uFF33" => "S" + +# ś [LATIN SMALL LETTER S WITH ACUTE] +"\u015B" => "s" + +# ŝ [LATIN SMALL LETTER S WITH CIRCUMFLEX] +"\u015D" => "s" + +# ş [LATIN SMALL LETTER S WITH CEDILLA] +"\u015F" => "s" + +# š [LATIN SMALL LETTER S WITH CARON] +"\u0161" => "s" + +# ſ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] +"\u017F" => "s" + +# ș [LATIN SMALL LETTER S WITH COMMA BELOW] +"\u0219" => "s" + +# ȿ [LATIN SMALL LETTER S WITH SWASH TAIL] +"\u023F" => "s" + +# ʂ [LATIN SMALL LETTER S WITH HOOK] +"\u0282" => "s" + +# ᵴ [LATIN SMALL LETTER S WITH MIDDLE TILDE] +"\u1D74" => "s" + +# ᶊ [LATIN SMALL LETTER S WITH PALATAL HOOK] +"\u1D8A" => "s" + +# ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] +"\u1E61" => "s" + +# ṣ [LATIN SMALL LETTER S WITH DOT BELOW] +"\u1E63" => "s" + +# ṥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] +"\u1E65" => "s" + +# ṧ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] +"\u1E67" => "s" + +# ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] +"\u1E69" => "s" + +# ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] +"\u1E9C" => "s" + +# ẝ [LATIN SMALL LETTER LONG S WITH HIGH STROKE] +"\u1E9D" => "s" + +# ⓢ [CIRCLED LATIN SMALL LETTER S] +"\u24E2" => "s" + +# Ꞅ [LATIN CAPITAL LETTER INSULAR S] +"\uA784" => "s" + +# s [FULLWIDTH LATIN SMALL LETTER S] +"\uFF53" => "s" + +# ẞ [LATIN CAPITAL LETTER SHARP S] +"\u1E9E" => "SS" + +# ⒮ [PARENTHESIZED LATIN SMALL LETTER S] +"\u24AE" => "(s)" + +# ß [LATIN SMALL LETTER SHARP S] +"\u00DF" => "ss" + +# st [LATIN SMALL LIGATURE ST] +"\uFB06" => "st" + +# Ţ [LATIN CAPITAL LETTER T WITH CEDILLA] +"\u0162" => "T" + +# Ť [LATIN CAPITAL LETTER T WITH CARON] +"\u0164" => "T" + +# Ŧ [LATIN CAPITAL LETTER T WITH STROKE] +"\u0166" => "T" + +# Ƭ [LATIN CAPITAL LETTER T WITH HOOK] +"\u01AC" => "T" + +# Ʈ [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] +"\u01AE" => "T" + +# Ț [LATIN CAPITAL LETTER T WITH COMMA BELOW] +"\u021A" => "T" + +# Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] +"\u023E" => "T" + +# ᴛ [LATIN LETTER SMALL CAPITAL T] +"\u1D1B" => "T" + +# Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] +"\u1E6A" => "T" + +# Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] +"\u1E6C" => "T" + +# Ṯ [LATIN CAPITAL LETTER T WITH LINE BELOW] +"\u1E6E" => "T" + +# Ṱ [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] +"\u1E70" => "T" + +# Ⓣ [CIRCLED LATIN CAPITAL LETTER T] +"\u24C9" => "T" + +# Ꞇ [LATIN CAPITAL LETTER INSULAR T] +"\uA786" => "T" + +# T [FULLWIDTH LATIN CAPITAL LETTER T] +"\uFF34" => "T" + +# ţ [LATIN SMALL LETTER T WITH CEDILLA] +"\u0163" => "t" + +# ť [LATIN SMALL LETTER T WITH CARON] +"\u0165" => "t" + +# ŧ [LATIN SMALL LETTER T WITH STROKE] +"\u0167" => "t" + +# ƫ [LATIN SMALL LETTER T WITH PALATAL HOOK] +"\u01AB" => "t" + +# ƭ [LATIN SMALL LETTER T WITH HOOK] +"\u01AD" => "t" + +# ț [LATIN SMALL LETTER T WITH COMMA BELOW] +"\u021B" => "t" + +# ȶ [LATIN SMALL LETTER T WITH CURL] +"\u0236" => "t" + +# ʇ [LATIN SMALL LETTER TURNED T] +"\u0287" => "t" + +# ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] +"\u0288" => "t" + +# ᵵ [LATIN SMALL LETTER T WITH MIDDLE TILDE] +"\u1D75" => "t" + +# ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] +"\u1E6B" => "t" + +# ṭ [LATIN SMALL LETTER T WITH DOT BELOW] +"\u1E6D" => "t" + +# ṯ [LATIN SMALL LETTER T WITH LINE BELOW] +"\u1E6F" => "t" + +# ṱ [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] +"\u1E71" => "t" + +# ẗ [LATIN SMALL LETTER T WITH DIAERESIS] +"\u1E97" => "t" + +# ⓣ [CIRCLED LATIN SMALL LETTER T] +"\u24E3" => "t" + +# ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] +"\u2C66" => "t" + +# t [FULLWIDTH LATIN SMALL LETTER T] +"\uFF54" => "t" + +# Þ [LATIN CAPITAL LETTER THORN] +"\u00DE" => "TH" + +# Ꝧ [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] +"\uA766" => "TH" + +# Ꜩ [LATIN CAPITAL LETTER TZ] +"\uA728" => "TZ" + +# ⒯ [PARENTHESIZED LATIN SMALL LETTER T] +"\u24AF" => "(t)" + +# ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] +"\u02A8" => "tc" + +# þ [LATIN SMALL LETTER THORN] +"\u00FE" => "th" + +# ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] +"\u1D7A" => "th" + +# ꝧ [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] +"\uA767" => "th" + +# ʦ [LATIN SMALL LETTER TS DIGRAPH] +"\u02A6" => "ts" + +# ꜩ [LATIN SMALL LETTER TZ] +"\uA729" => "tz" + +# Ù [LATIN CAPITAL LETTER U WITH GRAVE] +"\u00D9" => "U" + +# Ú [LATIN CAPITAL LETTER U WITH ACUTE] +"\u00DA" => "U" + +# Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] +"\u00DB" => "U" + +# Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] +"\u00DC" => "U" + +# Ũ [LATIN CAPITAL LETTER U WITH TILDE] +"\u0168" => "U" + +# Ū [LATIN CAPITAL LETTER U WITH MACRON] +"\u016A" => "U" + +# Ŭ [LATIN CAPITAL LETTER U WITH BREVE] +"\u016C" => "U" + +# Ů [LATIN CAPITAL LETTER U WITH RING ABOVE] +"\u016E" => "U" + +# Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] +"\u0170" => "U" + +# Ų [LATIN CAPITAL LETTER U WITH OGONEK] +"\u0172" => "U" + +# Ư [LATIN CAPITAL LETTER U WITH HORN] +"\u01AF" => "U" + +# Ǔ [LATIN CAPITAL LETTER U WITH CARON] +"\u01D3" => "U" + +# Ǖ [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] +"\u01D5" => "U" + +# Ǘ [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] +"\u01D7" => "U" + +# Ǚ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] +"\u01D9" => "U" + +# Ǜ [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] +"\u01DB" => "U" + +# Ȕ [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] +"\u0214" => "U" + +# Ȗ [LATIN CAPITAL LETTER U WITH INVERTED BREVE] +"\u0216" => "U" + +# Ʉ [LATIN CAPITAL LETTER U BAR] +"\u0244" => "U" + +# ᴜ [LATIN LETTER SMALL CAPITAL U] +"\u1D1C" => "U" + +# ᵾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] +"\u1D7E" => "U" + +# Ṳ [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] +"\u1E72" => "U" + +# Ṵ [LATIN CAPITAL LETTER U WITH TILDE BELOW] +"\u1E74" => "U" + +# Ṷ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] +"\u1E76" => "U" + +# Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] +"\u1E78" => "U" + +# Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] +"\u1E7A" => "U" + +# Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] +"\u1EE4" => "U" + +# Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] +"\u1EE6" => "U" + +# Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] +"\u1EE8" => "U" + +# Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] +"\u1EEA" => "U" + +# Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] +"\u1EEC" => "U" + +# Ữ [LATIN CAPITAL LETTER U WITH HORN AND TILDE] +"\u1EEE" => "U" + +# Ự [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] +"\u1EF0" => "U" + +# Ⓤ [CIRCLED LATIN CAPITAL LETTER U] +"\u24CA" => "U" + +# U [FULLWIDTH LATIN CAPITAL LETTER U] +"\uFF35" => "U" + +# ù [LATIN SMALL LETTER U WITH GRAVE] +"\u00F9" => "u" + +# ú [LATIN SMALL LETTER U WITH ACUTE] +"\u00FA" => "u" + +# û [LATIN SMALL LETTER U WITH CIRCUMFLEX] +"\u00FB" => "u" + +# ü [LATIN SMALL LETTER U WITH DIAERESIS] +"\u00FC" => "u" + +# ũ [LATIN SMALL LETTER U WITH TILDE] +"\u0169" => "u" + +# ū [LATIN SMALL LETTER U WITH MACRON] +"\u016B" => "u" + +# ŭ [LATIN SMALL LETTER U WITH BREVE] +"\u016D" => "u" + +# ů [LATIN SMALL LETTER U WITH RING ABOVE] +"\u016F" => "u" + +# ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] +"\u0171" => "u" + +# ų [LATIN SMALL LETTER U WITH OGONEK] +"\u0173" => "u" + +# ư [LATIN SMALL LETTER U WITH HORN] +"\u01B0" => "u" + +# ǔ [LATIN SMALL LETTER U WITH CARON] +"\u01D4" => "u" + +# ǖ [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] +"\u01D6" => "u" + +# ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] +"\u01D8" => "u" + +# ǚ [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] +"\u01DA" => "u" + +# ǜ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] +"\u01DC" => "u" + +# ȕ [LATIN SMALL LETTER U WITH DOUBLE GRAVE] +"\u0215" => "u" + +# ȗ [LATIN SMALL LETTER U WITH INVERTED BREVE] +"\u0217" => "u" + +# ʉ [LATIN SMALL LETTER U BAR] +"\u0289" => "u" + +# ᵤ [LATIN SUBSCRIPT SMALL LETTER U] +"\u1D64" => "u" + +# ᶙ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] +"\u1D99" => "u" + +# ṳ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] +"\u1E73" => "u" + +# ṵ [LATIN SMALL LETTER U WITH TILDE BELOW] +"\u1E75" => "u" + +# ṷ [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] +"\u1E77" => "u" + +# ṹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] +"\u1E79" => "u" + +# ṻ [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] +"\u1E7B" => "u" + +# ụ [LATIN SMALL LETTER U WITH DOT BELOW] +"\u1EE5" => "u" + +# ủ [LATIN SMALL LETTER U WITH HOOK ABOVE] +"\u1EE7" => "u" + +# ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] +"\u1EE9" => "u" + +# ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] +"\u1EEB" => "u" + +# ử [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] +"\u1EED" => "u" + +# ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] +"\u1EEF" => "u" + +# ự [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] +"\u1EF1" => "u" + +# ⓤ [CIRCLED LATIN SMALL LETTER U] +"\u24E4" => "u" + +# u [FULLWIDTH LATIN SMALL LETTER U] +"\uFF55" => "u" + +# ⒰ [PARENTHESIZED LATIN SMALL LETTER U] +"\u24B0" => "(u)" + +# ᵫ [LATIN SMALL LETTER UE] +"\u1D6B" => "ue" + +# Ʋ [LATIN CAPITAL LETTER V WITH HOOK] +"\u01B2" => "V" + +# Ʌ [LATIN CAPITAL LETTER TURNED V] +"\u0245" => "V" + +# ᴠ [LATIN LETTER SMALL CAPITAL V] +"\u1D20" => "V" + +# Ṽ [LATIN CAPITAL LETTER V WITH TILDE] +"\u1E7C" => "V" + +# Ṿ [LATIN CAPITAL LETTER V WITH DOT BELOW] +"\u1E7E" => "V" + +# Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] +"\u1EFC" => "V" + +# Ⓥ [CIRCLED LATIN CAPITAL LETTER V] +"\u24CB" => "V" + +# Ꝟ [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] +"\uA75E" => "V" + +# Ꝩ [LATIN CAPITAL LETTER VEND] +"\uA768" => "V" + +# V [FULLWIDTH LATIN CAPITAL LETTER V] +"\uFF36" => "V" + +# ʋ [LATIN SMALL LETTER V WITH HOOK] +"\u028B" => "v" + +# ʌ [LATIN SMALL LETTER TURNED V] +"\u028C" => "v" + +# ᵥ [LATIN SUBSCRIPT SMALL LETTER V] +"\u1D65" => "v" + +# ᶌ [LATIN SMALL LETTER V WITH PALATAL HOOK] +"\u1D8C" => "v" + +# ṽ [LATIN SMALL LETTER V WITH TILDE] +"\u1E7D" => "v" + +# ṿ [LATIN SMALL LETTER V WITH DOT BELOW] +"\u1E7F" => "v" + +# ⓥ [CIRCLED LATIN SMALL LETTER V] +"\u24E5" => "v" + +# ⱱ [LATIN SMALL LETTER V WITH RIGHT HOOK] +"\u2C71" => "v" + +# ⱴ [LATIN SMALL LETTER V WITH CURL] +"\u2C74" => "v" + +# ꝟ [LATIN SMALL LETTER V WITH DIAGONAL STROKE] +"\uA75F" => "v" + +# v [FULLWIDTH LATIN SMALL LETTER V] +"\uFF56" => "v" + +# Ꝡ [LATIN CAPITAL LETTER VY] +"\uA760" => "VY" + +# ⒱ [PARENTHESIZED LATIN SMALL LETTER V] +"\u24B1" => "(v)" + +# ꝡ [LATIN SMALL LETTER VY] +"\uA761" => "vy" + +# Ŵ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] +"\u0174" => "W" + +# Ƿ http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] +"\u01F7" => "W" + +# ᴡ [LATIN LETTER SMALL CAPITAL W] +"\u1D21" => "W" + +# Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] +"\u1E80" => "W" + +# Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] +"\u1E82" => "W" + +# Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] +"\u1E84" => "W" + +# Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] +"\u1E86" => "W" + +# Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] +"\u1E88" => "W" + +# Ⓦ [CIRCLED LATIN CAPITAL LETTER W] +"\u24CC" => "W" + +# Ⱳ [LATIN CAPITAL LETTER W WITH HOOK] +"\u2C72" => "W" + +# W [FULLWIDTH LATIN CAPITAL LETTER W] +"\uFF37" => "W" + +# ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] +"\u0175" => "w" + +# ƿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] +"\u01BF" => "w" + +# ʍ [LATIN SMALL LETTER TURNED W] +"\u028D" => "w" + +# ẁ [LATIN SMALL LETTER W WITH GRAVE] +"\u1E81" => "w" + +# ẃ [LATIN SMALL LETTER W WITH ACUTE] +"\u1E83" => "w" + +# ẅ [LATIN SMALL LETTER W WITH DIAERESIS] +"\u1E85" => "w" + +# ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] +"\u1E87" => "w" + +# ẉ [LATIN SMALL LETTER W WITH DOT BELOW] +"\u1E89" => "w" + +# ẘ [LATIN SMALL LETTER W WITH RING ABOVE] +"\u1E98" => "w" + +# ⓦ [CIRCLED LATIN SMALL LETTER W] +"\u24E6" => "w" + +# ⱳ [LATIN SMALL LETTER W WITH HOOK] +"\u2C73" => "w" + +# w [FULLWIDTH LATIN SMALL LETTER W] +"\uFF57" => "w" + +# ⒲ [PARENTHESIZED LATIN SMALL LETTER W] +"\u24B2" => "(w)" + +# Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] +"\u1E8A" => "X" + +# Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] +"\u1E8C" => "X" + +# Ⓧ [CIRCLED LATIN CAPITAL LETTER X] +"\u24CD" => "X" + +# X [FULLWIDTH LATIN CAPITAL LETTER X] +"\uFF38" => "X" + +# ᶍ [LATIN SMALL LETTER X WITH PALATAL HOOK] +"\u1D8D" => "x" + +# ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] +"\u1E8B" => "x" + +# ẍ [LATIN SMALL LETTER X WITH DIAERESIS] +"\u1E8D" => "x" + +# ₓ [LATIN SUBSCRIPT SMALL LETTER X] +"\u2093" => "x" + +# ⓧ [CIRCLED LATIN SMALL LETTER X] +"\u24E7" => "x" + +# x [FULLWIDTH LATIN SMALL LETTER X] +"\uFF58" => "x" + +# ⒳ [PARENTHESIZED LATIN SMALL LETTER X] +"\u24B3" => "(x)" + +# Ý [LATIN CAPITAL LETTER Y WITH ACUTE] +"\u00DD" => "Y" + +# Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] +"\u0176" => "Y" + +# Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] +"\u0178" => "Y" + +# Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] +"\u01B3" => "Y" + +# Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] +"\u0232" => "Y" + +# Ɏ [LATIN CAPITAL LETTER Y WITH STROKE] +"\u024E" => "Y" + +# ʏ [LATIN LETTER SMALL CAPITAL Y] +"\u028F" => "Y" + +# Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] +"\u1E8E" => "Y" + +# Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] +"\u1EF2" => "Y" + +# Ỵ [LATIN CAPITAL LETTER Y WITH DOT BELOW] +"\u1EF4" => "Y" + +# Ỷ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] +"\u1EF6" => "Y" + +# Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] +"\u1EF8" => "Y" + +# Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] +"\u1EFE" => "Y" + +# Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] +"\u24CE" => "Y" + +# Y [FULLWIDTH LATIN CAPITAL LETTER Y] +"\uFF39" => "Y" + +# ý [LATIN SMALL LETTER Y WITH ACUTE] +"\u00FD" => "y" + +# ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] +"\u00FF" => "y" + +# ŷ [LATIN SMALL LETTER Y WITH CIRCUMFLEX] +"\u0177" => "y" + +# ƴ [LATIN SMALL LETTER Y WITH HOOK] +"\u01B4" => "y" + +# ȳ [LATIN SMALL LETTER Y WITH MACRON] +"\u0233" => "y" + +# ɏ [LATIN SMALL LETTER Y WITH STROKE] +"\u024F" => "y" + +# ʎ [LATIN SMALL LETTER TURNED Y] +"\u028E" => "y" + +# ẏ [LATIN SMALL LETTER Y WITH DOT ABOVE] +"\u1E8F" => "y" + +# ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] +"\u1E99" => "y" + +# ỳ [LATIN SMALL LETTER Y WITH GRAVE] +"\u1EF3" => "y" + +# ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] +"\u1EF5" => "y" + +# ỷ [LATIN SMALL LETTER Y WITH HOOK ABOVE] +"\u1EF7" => "y" + +# ỹ [LATIN SMALL LETTER Y WITH TILDE] +"\u1EF9" => "y" + +# ỿ [LATIN SMALL LETTER Y WITH LOOP] +"\u1EFF" => "y" + +# ⓨ [CIRCLED LATIN SMALL LETTER Y] +"\u24E8" => "y" + +# y [FULLWIDTH LATIN SMALL LETTER Y] +"\uFF59" => "y" + +# ⒴ [PARENTHESIZED LATIN SMALL LETTER Y] +"\u24B4" => "(y)" + +# Ź [LATIN CAPITAL LETTER Z WITH ACUTE] +"\u0179" => "Z" + +# Ż [LATIN CAPITAL LETTER Z WITH DOT ABOVE] +"\u017B" => "Z" + +# Ž [LATIN CAPITAL LETTER Z WITH CARON] +"\u017D" => "Z" + +# Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] +"\u01B5" => "Z" + +# Ȝ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] +"\u021C" => "Z" + +# Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] +"\u0224" => "Z" + +# ᴢ [LATIN LETTER SMALL CAPITAL Z] +"\u1D22" => "Z" + +# Ẑ [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] +"\u1E90" => "Z" + +# Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] +"\u1E92" => "Z" + +# Ẕ [LATIN CAPITAL LETTER Z WITH LINE BELOW] +"\u1E94" => "Z" + +# Ⓩ [CIRCLED LATIN CAPITAL LETTER Z] +"\u24CF" => "Z" + +# Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] +"\u2C6B" => "Z" + +# Ꝣ [LATIN CAPITAL LETTER VISIGOTHIC Z] +"\uA762" => "Z" + +# Z [FULLWIDTH LATIN CAPITAL LETTER Z] +"\uFF3A" => "Z" + +# ź [LATIN SMALL LETTER Z WITH ACUTE] +"\u017A" => "z" + +# ż [LATIN SMALL LETTER Z WITH DOT ABOVE] +"\u017C" => "z" + +# ž [LATIN SMALL LETTER Z WITH CARON] +"\u017E" => "z" + +# ƶ [LATIN SMALL LETTER Z WITH STROKE] +"\u01B6" => "z" + +# ȝ http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] +"\u021D" => "z" + +# ȥ [LATIN SMALL LETTER Z WITH HOOK] +"\u0225" => "z" + +# ɀ [LATIN SMALL LETTER Z WITH SWASH TAIL] +"\u0240" => "z" + +# ʐ [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] +"\u0290" => "z" + +# ʑ [LATIN SMALL LETTER Z WITH CURL] +"\u0291" => "z" + +# ᵶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] +"\u1D76" => "z" + +# ᶎ [LATIN SMALL LETTER Z WITH PALATAL HOOK] +"\u1D8E" => "z" + +# ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] +"\u1E91" => "z" + +# ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] +"\u1E93" => "z" + +# ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] +"\u1E95" => "z" + +# ⓩ [CIRCLED LATIN SMALL LETTER Z] +"\u24E9" => "z" + +# ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] +"\u2C6C" => "z" + +# ꝣ [LATIN SMALL LETTER VISIGOTHIC Z] +"\uA763" => "z" + +# z [FULLWIDTH LATIN SMALL LETTER Z] +"\uFF5A" => "z" + +# ⒵ [PARENTHESIZED LATIN SMALL LETTER Z] +"\u24B5" => "(z)" + +# ⁰ [SUPERSCRIPT ZERO] +"\u2070" => "0" + +# ₀ [SUBSCRIPT ZERO] +"\u2080" => "0" + +# ⓪ [CIRCLED DIGIT ZERO] +"\u24EA" => "0" + +# ⓿ [NEGATIVE CIRCLED DIGIT ZERO] +"\u24FF" => "0" + +# 0 [FULLWIDTH DIGIT ZERO] +"\uFF10" => "0" + +# ¹ [SUPERSCRIPT ONE] +"\u00B9" => "1" + +# ₁ [SUBSCRIPT ONE] +"\u2081" => "1" + +# ① [CIRCLED DIGIT ONE] +"\u2460" => "1" + +# ⓵ [DOUBLE CIRCLED DIGIT ONE] +"\u24F5" => "1" + +# ❶ [DINGBAT NEGATIVE CIRCLED DIGIT ONE] +"\u2776" => "1" + +# ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] +"\u2780" => "1" + +# ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] +"\u278A" => "1" + +# 1 [FULLWIDTH DIGIT ONE] +"\uFF11" => "1" + +# ⒈ [DIGIT ONE FULL STOP] +"\u2488" => "1." + +# ⑴ [PARENTHESIZED DIGIT ONE] +"\u2474" => "(1)" + +# ² [SUPERSCRIPT TWO] +"\u00B2" => "2" + +# ₂ [SUBSCRIPT TWO] +"\u2082" => "2" + +# ② [CIRCLED DIGIT TWO] +"\u2461" => "2" + +# ⓶ [DOUBLE CIRCLED DIGIT TWO] +"\u24F6" => "2" + +# ❷ [DINGBAT NEGATIVE CIRCLED DIGIT TWO] +"\u2777" => "2" + +# ➁ [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] +"\u2781" => "2" + +# ➋ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] +"\u278B" => "2" + +# 2 [FULLWIDTH DIGIT TWO] +"\uFF12" => "2" + +# ⒉ [DIGIT TWO FULL STOP] +"\u2489" => "2." + +# ⑵ [PARENTHESIZED DIGIT TWO] +"\u2475" => "(2)" + +# ³ [SUPERSCRIPT THREE] +"\u00B3" => "3" + +# ₃ [SUBSCRIPT THREE] +"\u2083" => "3" + +# ③ [CIRCLED DIGIT THREE] +"\u2462" => "3" + +# ⓷ [DOUBLE CIRCLED DIGIT THREE] +"\u24F7" => "3" + +# ❸ [DINGBAT NEGATIVE CIRCLED DIGIT THREE] +"\u2778" => "3" + +# ➂ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] +"\u2782" => "3" + +# ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] +"\u278C" => "3" + +# 3 [FULLWIDTH DIGIT THREE] +"\uFF13" => "3" + +# ⒊ [DIGIT THREE FULL STOP] +"\u248A" => "3." + +# ⑶ [PARENTHESIZED DIGIT THREE] +"\u2476" => "(3)" + +# ⁴ [SUPERSCRIPT FOUR] +"\u2074" => "4" + +# ₄ [SUBSCRIPT FOUR] +"\u2084" => "4" + +# ④ [CIRCLED DIGIT FOUR] +"\u2463" => "4" + +# ⓸ [DOUBLE CIRCLED DIGIT FOUR] +"\u24F8" => "4" + +# ❹ [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] +"\u2779" => "4" + +# ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] +"\u2783" => "4" + +# ➍ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] +"\u278D" => "4" + +# 4 [FULLWIDTH DIGIT FOUR] +"\uFF14" => "4" + +# ⒋ [DIGIT FOUR FULL STOP] +"\u248B" => "4." + +# ⑷ [PARENTHESIZED DIGIT FOUR] +"\u2477" => "(4)" + +# ⁵ [SUPERSCRIPT FIVE] +"\u2075" => "5" + +# ₅ [SUBSCRIPT FIVE] +"\u2085" => "5" + +# ⑤ [CIRCLED DIGIT FIVE] +"\u2464" => "5" + +# ⓹ [DOUBLE CIRCLED DIGIT FIVE] +"\u24F9" => "5" + +# ❺ [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] +"\u277A" => "5" + +# ➄ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] +"\u2784" => "5" + +# ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] +"\u278E" => "5" + +# 5 [FULLWIDTH DIGIT FIVE] +"\uFF15" => "5" + +# ⒌ [DIGIT FIVE FULL STOP] +"\u248C" => "5." + +# ⑸ [PARENTHESIZED DIGIT FIVE] +"\u2478" => "(5)" + +# ⁶ [SUPERSCRIPT SIX] +"\u2076" => "6" + +# ₆ [SUBSCRIPT SIX] +"\u2086" => "6" + +# ⑥ [CIRCLED DIGIT SIX] +"\u2465" => "6" + +# ⓺ [DOUBLE CIRCLED DIGIT SIX] +"\u24FA" => "6" + +# ❻ [DINGBAT NEGATIVE CIRCLED DIGIT SIX] +"\u277B" => "6" + +# ➅ [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] +"\u2785" => "6" + +# ➏ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] +"\u278F" => "6" + +# 6 [FULLWIDTH DIGIT SIX] +"\uFF16" => "6" + +# ⒍ [DIGIT SIX FULL STOP] +"\u248D" => "6." + +# ⑹ [PARENTHESIZED DIGIT SIX] +"\u2479" => "(6)" + +# ⁷ [SUPERSCRIPT SEVEN] +"\u2077" => "7" + +# ₇ [SUBSCRIPT SEVEN] +"\u2087" => "7" + +# ⑦ [CIRCLED DIGIT SEVEN] +"\u2466" => "7" + +# ⓻ [DOUBLE CIRCLED DIGIT SEVEN] +"\u24FB" => "7" + +# ❼ [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] +"\u277C" => "7" + +# ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] +"\u2786" => "7" + +# ➐ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] +"\u2790" => "7" + +# 7 [FULLWIDTH DIGIT SEVEN] +"\uFF17" => "7" + +# ⒎ [DIGIT SEVEN FULL STOP] +"\u248E" => "7." + +# ⑺ [PARENTHESIZED DIGIT SEVEN] +"\u247A" => "(7)" + +# ⁸ [SUPERSCRIPT EIGHT] +"\u2078" => "8" + +# ₈ [SUBSCRIPT EIGHT] +"\u2088" => "8" + +# ⑧ [CIRCLED DIGIT EIGHT] +"\u2467" => "8" + +# ⓼ [DOUBLE CIRCLED DIGIT EIGHT] +"\u24FC" => "8" + +# ❽ [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] +"\u277D" => "8" + +# ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] +"\u2787" => "8" + +# ➑ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] +"\u2791" => "8" + +# 8 [FULLWIDTH DIGIT EIGHT] +"\uFF18" => "8" + +# ⒏ [DIGIT EIGHT FULL STOP] +"\u248F" => "8." + +# ⑻ [PARENTHESIZED DIGIT EIGHT] +"\u247B" => "(8)" + +# ⁹ [SUPERSCRIPT NINE] +"\u2079" => "9" + +# ₉ [SUBSCRIPT NINE] +"\u2089" => "9" + +# ⑨ [CIRCLED DIGIT NINE] +"\u2468" => "9" + +# ⓽ [DOUBLE CIRCLED DIGIT NINE] +"\u24FD" => "9" + +# ❾ [DINGBAT NEGATIVE CIRCLED DIGIT NINE] +"\u277E" => "9" + +# ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] +"\u2788" => "9" + +# ➒ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] +"\u2792" => "9" + +# 9 [FULLWIDTH DIGIT NINE] +"\uFF19" => "9" + +# ⒐ [DIGIT NINE FULL STOP] +"\u2490" => "9." + +# ⑼ [PARENTHESIZED DIGIT NINE] +"\u247C" => "(9)" + +# ⑩ [CIRCLED NUMBER TEN] +"\u2469" => "10" + +# ⓾ [DOUBLE CIRCLED NUMBER TEN] +"\u24FE" => "10" + +# ❿ [DINGBAT NEGATIVE CIRCLED NUMBER TEN] +"\u277F" => "10" + +# ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] +"\u2789" => "10" + +# ➓ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] +"\u2793" => "10" + +# ⒑ [NUMBER TEN FULL STOP] +"\u2491" => "10." + +# ⑽ [PARENTHESIZED NUMBER TEN] +"\u247D" => "(10)" + +# ⑪ [CIRCLED NUMBER ELEVEN] +"\u246A" => "11" + +# ⓫ [NEGATIVE CIRCLED NUMBER ELEVEN] +"\u24EB" => "11" + +# ⒒ [NUMBER ELEVEN FULL STOP] +"\u2492" => "11." + +# ⑾ [PARENTHESIZED NUMBER ELEVEN] +"\u247E" => "(11)" + +# ⑫ [CIRCLED NUMBER TWELVE] +"\u246B" => "12" + +# ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] +"\u24EC" => "12" + +# ⒓ [NUMBER TWELVE FULL STOP] +"\u2493" => "12." + +# ⑿ [PARENTHESIZED NUMBER TWELVE] +"\u247F" => "(12)" + +# ⑬ [CIRCLED NUMBER THIRTEEN] +"\u246C" => "13" + +# ⓭ [NEGATIVE CIRCLED NUMBER THIRTEEN] +"\u24ED" => "13" + +# ⒔ [NUMBER THIRTEEN FULL STOP] +"\u2494" => "13." + +# ⒀ [PARENTHESIZED NUMBER THIRTEEN] +"\u2480" => "(13)" + +# ⑭ [CIRCLED NUMBER FOURTEEN] +"\u246D" => "14" + +# ⓮ [NEGATIVE CIRCLED NUMBER FOURTEEN] +"\u24EE" => "14" + +# ⒕ [NUMBER FOURTEEN FULL STOP] +"\u2495" => "14." + +# ⒁ [PARENTHESIZED NUMBER FOURTEEN] +"\u2481" => "(14)" + +# ⑮ [CIRCLED NUMBER FIFTEEN] +"\u246E" => "15" + +# ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] +"\u24EF" => "15" + +# ⒖ [NUMBER FIFTEEN FULL STOP] +"\u2496" => "15." + +# ⒂ [PARENTHESIZED NUMBER FIFTEEN] +"\u2482" => "(15)" + +# ⑯ [CIRCLED NUMBER SIXTEEN] +"\u246F" => "16" + +# ⓰ [NEGATIVE CIRCLED NUMBER SIXTEEN] +"\u24F0" => "16" + +# ⒗ [NUMBER SIXTEEN FULL STOP] +"\u2497" => "16." + +# ⒃ [PARENTHESIZED NUMBER SIXTEEN] +"\u2483" => "(16)" + +# ⑰ [CIRCLED NUMBER SEVENTEEN] +"\u2470" => "17" + +# ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] +"\u24F1" => "17" + +# ⒘ [NUMBER SEVENTEEN FULL STOP] +"\u2498" => "17." + +# ⒄ [PARENTHESIZED NUMBER SEVENTEEN] +"\u2484" => "(17)" + +# ⑱ [CIRCLED NUMBER EIGHTEEN] +"\u2471" => "18" + +# ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] +"\u24F2" => "18" + +# ⒙ [NUMBER EIGHTEEN FULL STOP] +"\u2499" => "18." + +# ⒅ [PARENTHESIZED NUMBER EIGHTEEN] +"\u2485" => "(18)" + +# ⑲ [CIRCLED NUMBER NINETEEN] +"\u2472" => "19" + +# ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] +"\u24F3" => "19" + +# ⒚ [NUMBER NINETEEN FULL STOP] +"\u249A" => "19." + +# ⒆ [PARENTHESIZED NUMBER NINETEEN] +"\u2486" => "(19)" + +# ⑳ [CIRCLED NUMBER TWENTY] +"\u2473" => "20" + +# ⓴ [NEGATIVE CIRCLED NUMBER TWENTY] +"\u24F4" => "20" + +# ⒛ [NUMBER TWENTY FULL STOP] +"\u249B" => "20." + +# ⒇ [PARENTHESIZED NUMBER TWENTY] +"\u2487" => "(20)" + +# « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] +"\u00AB" => "\"" + +# » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] +"\u00BB" => "\"" + +# “ [LEFT DOUBLE QUOTATION MARK] +"\u201C" => "\"" + +# ” [RIGHT DOUBLE QUOTATION MARK] +"\u201D" => "\"" + +# „ [DOUBLE LOW-9 QUOTATION MARK] +"\u201E" => "\"" + +# ″ [DOUBLE PRIME] +"\u2033" => "\"" + +# ‶ [REVERSED DOUBLE PRIME] +"\u2036" => "\"" + +# ❝ [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] +"\u275D" => "\"" + +# ❞ [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] +"\u275E" => "\"" + +# ❮ [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] +"\u276E" => "\"" + +# ❯ [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] +"\u276F" => "\"" + +# " [FULLWIDTH QUOTATION MARK] +"\uFF02" => "\"" + +# ‘ [LEFT SINGLE QUOTATION MARK] +"\u2018" => "\'" + +# ’ [RIGHT SINGLE QUOTATION MARK] +"\u2019" => "\'" + +# ‚ [SINGLE LOW-9 QUOTATION MARK] +"\u201A" => "\'" + +# ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] +"\u201B" => "\'" + +# ′ [PRIME] +"\u2032" => "\'" + +# ‵ [REVERSED PRIME] +"\u2035" => "\'" + +# ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] +"\u2039" => "\'" + +# › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] +"\u203A" => "\'" + +# ❛ [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] +"\u275B" => "\'" + +# ❜ [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] +"\u275C" => "\'" + +# ' [FULLWIDTH APOSTROPHE] +"\uFF07" => "\'" + +# ‐ [HYPHEN] +"\u2010" => "-" + +# ‑ [NON-BREAKING HYPHEN] +"\u2011" => "-" + +# ‒ [FIGURE DASH] +"\u2012" => "-" + +# – [EN DASH] +"\u2013" => "-" + +# — [EM DASH] +"\u2014" => "-" + +# ⁻ [SUPERSCRIPT MINUS] +"\u207B" => "-" + +# ₋ [SUBSCRIPT MINUS] +"\u208B" => "-" + +# - [FULLWIDTH HYPHEN-MINUS] +"\uFF0D" => "-" + +# ⁅ [LEFT SQUARE BRACKET WITH QUILL] +"\u2045" => "[" + +# ❲ [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] +"\u2772" => "[" + +# [ [FULLWIDTH LEFT SQUARE BRACKET] +"\uFF3B" => "[" + +# ⁆ [RIGHT SQUARE BRACKET WITH QUILL] +"\u2046" => "]" + +# ❳ [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] +"\u2773" => "]" + +# ] [FULLWIDTH RIGHT SQUARE BRACKET] +"\uFF3D" => "]" + +# ⁽ [SUPERSCRIPT LEFT PARENTHESIS] +"\u207D" => "(" + +# ₍ [SUBSCRIPT LEFT PARENTHESIS] +"\u208D" => "(" + +# ❨ [MEDIUM LEFT PARENTHESIS ORNAMENT] +"\u2768" => "(" + +# ❪ [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] +"\u276A" => "(" + +# ( [FULLWIDTH LEFT PARENTHESIS] +"\uFF08" => "(" + +# ⸨ [LEFT DOUBLE PARENTHESIS] +"\u2E28" => "((" + +# ⁾ [SUPERSCRIPT RIGHT PARENTHESIS] +"\u207E" => ")" + +# ₎ [SUBSCRIPT RIGHT PARENTHESIS] +"\u208E" => ")" + +# ❩ [MEDIUM RIGHT PARENTHESIS ORNAMENT] +"\u2769" => ")" + +# ❫ [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] +"\u276B" => ")" + +# ) [FULLWIDTH RIGHT PARENTHESIS] +"\uFF09" => ")" + +# ⸩ [RIGHT DOUBLE PARENTHESIS] +"\u2E29" => "))" + +# ❬ [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] +"\u276C" => "<" + +# ❰ [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] +"\u2770" => "<" + +# < [FULLWIDTH LESS-THAN SIGN] +"\uFF1C" => "<" + +# ❭ [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] +"\u276D" => ">" + +# ❱ [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] +"\u2771" => ">" + +# > [FULLWIDTH GREATER-THAN SIGN] +"\uFF1E" => ">" + +# ❴ [MEDIUM LEFT CURLY BRACKET ORNAMENT] +"\u2774" => "{" + +# { [FULLWIDTH LEFT CURLY BRACKET] +"\uFF5B" => "{" + +# ❵ [MEDIUM RIGHT CURLY BRACKET ORNAMENT] +"\u2775" => "}" + +# } [FULLWIDTH RIGHT CURLY BRACKET] +"\uFF5D" => "}" + +# ⁺ [SUPERSCRIPT PLUS SIGN] +"\u207A" => "+" + +# ₊ [SUBSCRIPT PLUS SIGN] +"\u208A" => "+" + +# + [FULLWIDTH PLUS SIGN] +"\uFF0B" => "+" + +# ⁼ [SUPERSCRIPT EQUALS SIGN] +"\u207C" => "=" + +# ₌ [SUBSCRIPT EQUALS SIGN] +"\u208C" => "=" + +# = [FULLWIDTH EQUALS SIGN] +"\uFF1D" => "=" + +# ! [FULLWIDTH EXCLAMATION MARK] +"\uFF01" => "!" + +# ‼ [DOUBLE EXCLAMATION MARK] +"\u203C" => "!!" + +# ⁉ [EXCLAMATION QUESTION MARK] +"\u2049" => "!?" + +# # [FULLWIDTH NUMBER SIGN] +"\uFF03" => "#" + +# $ [FULLWIDTH DOLLAR SIGN] +"\uFF04" => "$" + +# ⁒ [COMMERCIAL MINUS SIGN] +"\u2052" => "%" + +# % [FULLWIDTH PERCENT SIGN] +"\uFF05" => "%" + +# & [FULLWIDTH AMPERSAND] +"\uFF06" => "&" + +# ⁎ [LOW ASTERISK] +"\u204E" => "*" + +# * [FULLWIDTH ASTERISK] +"\uFF0A" => "*" + +# , [FULLWIDTH COMMA] +"\uFF0C" => "," + +# . [FULLWIDTH FULL STOP] +"\uFF0E" => "." + +# ⁄ [FRACTION SLASH] +"\u2044" => "/" + +# / [FULLWIDTH SOLIDUS] +"\uFF0F" => "/" + +# : [FULLWIDTH COLON] +"\uFF1A" => ":" + +# ⁏ [REVERSED SEMICOLON] +"\u204F" => ";" + +# ; [FULLWIDTH SEMICOLON] +"\uFF1B" => ";" + +# ? [FULLWIDTH QUESTION MARK] +"\uFF1F" => "?" + +# ⁇ [DOUBLE QUESTION MARK] +"\u2047" => "??" + +# ⁈ [QUESTION EXCLAMATION MARK] +"\u2048" => "?!" + +# @ [FULLWIDTH COMMERCIAL AT] +"\uFF20" => "@" + +# \ [FULLWIDTH REVERSE SOLIDUS] +"\uFF3C" => "\\" + +# ‸ [CARET] +"\u2038" => "^" + +# ^ [FULLWIDTH CIRCUMFLEX ACCENT] +"\uFF3E" => "^" + +# _ [FULLWIDTH LOW LINE] +"\uFF3F" => "_" + +# ⁓ [SWUNG DASH] +"\u2053" => "~" + +# ~ [FULLWIDTH TILDE] +"\uFF5E" => "~" + +################################################################ +# Below is the Perl script used to generate the above mappings # +# from ASCIIFoldingFilter.java: # +################################################################ +# +# #!/usr/bin/perl +# +# use warnings; +# use strict; +# +# my @source_chars = (); +# my @source_char_descriptions = (); +# my $target = ''; +# +# while (<>) { +# if (/case\s+'(\\u[A-F0-9]+)':\s*\/\/\s*(.*)/i) { +# push @source_chars, $1; +# push @source_char_descriptions, $2; +# next; +# } +# if (/output\[[^\]]+\]\s*=\s*'(\\'|\\\\|.)'/) { +# $target .= $1; +# next; +# } +# if (/break;/) { +# $target = "\\\"" if ($target eq '"'); +# for my $source_char_num (0..$#source_chars) { +# print "# $source_char_descriptions[$source_char_num]\n"; +# print "\"$source_chars[$source_char_num]\" => \"$target\"\n\n"; +# } +# @source_chars = (); +# @source_char_descriptions = (); +# $target = ''; +# } +# } diff --git a/KeywordSearch/release/solr/solr/conf/mapping-ISOLatin1Accent.txt b/KeywordSearch/release/solr/solr/conf/mapping-ISOLatin1Accent.txt new file mode 100644 index 0000000000..ede7742581 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/mapping-ISOLatin1Accent.txt @@ -0,0 +1,246 @@ +# The ASF licenses this file to You 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. + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + +# example: +# "À" => "A" +# "\u00C0" => "A" +# "\u00C0" => "\u0041" +# "ß" => "ss" +# "\t" => " " +# "\n" => "" + +# À => A +"\u00C0" => "A" + +# Á => A +"\u00C1" => "A" + +#  => A +"\u00C2" => "A" + +# à => A +"\u00C3" => "A" + +# Ä => A +"\u00C4" => "A" + +# Å => A +"\u00C5" => "A" + +# Æ => AE +"\u00C6" => "AE" + +# Ç => C +"\u00C7" => "C" + +# È => E +"\u00C8" => "E" + +# É => E +"\u00C9" => "E" + +# Ê => E +"\u00CA" => "E" + +# Ë => E +"\u00CB" => "E" + +# Ì => I +"\u00CC" => "I" + +# Í => I +"\u00CD" => "I" + +# Î => I +"\u00CE" => "I" + +# Ï => I +"\u00CF" => "I" + +# IJ => IJ +"\u0132" => "IJ" + +# Ð => D +"\u00D0" => "D" + +# Ñ => N +"\u00D1" => "N" + +# Ò => O +"\u00D2" => "O" + +# Ó => O +"\u00D3" => "O" + +# Ô => O +"\u00D4" => "O" + +# Õ => O +"\u00D5" => "O" + +# Ö => O +"\u00D6" => "O" + +# Ø => O +"\u00D8" => "O" + +# Œ => OE +"\u0152" => "OE" + +# Þ +"\u00DE" => "TH" + +# Ù => U +"\u00D9" => "U" + +# Ú => U +"\u00DA" => "U" + +# Û => U +"\u00DB" => "U" + +# Ü => U +"\u00DC" => "U" + +# Ý => Y +"\u00DD" => "Y" + +# Ÿ => Y +"\u0178" => "Y" + +# à => a +"\u00E0" => "a" + +# á => a +"\u00E1" => "a" + +# â => a +"\u00E2" => "a" + +# ã => a +"\u00E3" => "a" + +# ä => a +"\u00E4" => "a" + +# å => a +"\u00E5" => "a" + +# æ => ae +"\u00E6" => "ae" + +# ç => c +"\u00E7" => "c" + +# è => e +"\u00E8" => "e" + +# é => e +"\u00E9" => "e" + +# ê => e +"\u00EA" => "e" + +# ë => e +"\u00EB" => "e" + +# ì => i +"\u00EC" => "i" + +# í => i +"\u00ED" => "i" + +# î => i +"\u00EE" => "i" + +# ï => i +"\u00EF" => "i" + +# ij => ij +"\u0133" => "ij" + +# ð => d +"\u00F0" => "d" + +# ñ => n +"\u00F1" => "n" + +# ò => o +"\u00F2" => "o" + +# ó => o +"\u00F3" => "o" + +# ô => o +"\u00F4" => "o" + +# õ => o +"\u00F5" => "o" + +# ö => o +"\u00F6" => "o" + +# ø => o +"\u00F8" => "o" + +# œ => oe +"\u0153" => "oe" + +# ß => ss +"\u00DF" => "ss" + +# þ => th +"\u00FE" => "th" + +# ù => u +"\u00F9" => "u" + +# ú => u +"\u00FA" => "u" + +# û => u +"\u00FB" => "u" + +# ü => u +"\u00FC" => "u" + +# ý => y +"\u00FD" => "y" + +# ÿ => y +"\u00FF" => "y" + +# ff => ff +"\uFB00" => "ff" + +# fi => fi +"\uFB01" => "fi" + +# fl => fl +"\uFB02" => "fl" + +# ffi => ffi +"\uFB03" => "ffi" + +# ffl => ffl +"\uFB04" => "ffl" + +# ſt => ft +"\uFB05" => "ft" + +# st => st +"\uFB06" => "st" diff --git a/KeywordSearch/release/solr/solr/conf/protwords.txt b/KeywordSearch/release/solr/solr/conf/protwords.txt new file mode 100644 index 0000000000..1dfc0abecb --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/KeywordSearch/release/solr/solr/conf/schema.xml b/KeywordSearch/release/solr/solr/conf/schema.xml new file mode 100644 index 0000000000..0436a567a9 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/schema.xml @@ -0,0 +1,638 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + text + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/release/solr/solr/conf/scripts.conf b/KeywordSearch/release/solr/solr/conf/scripts.conf new file mode 100644 index 0000000000..f58b262ae0 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/scripts.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +user= +solr_hostname=localhost +solr_port=8983 +rsyncd_port=18983 +data_dir= +webapp_name=solr +master_host= +master_data_dir= +master_status_dir= diff --git a/KeywordSearch/release/solr/solr/conf/solrconfig.xml b/KeywordSearch/release/solr/solr/conf/solrconfig.xml new file mode 100644 index 0000000000..f24b5f5550 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/solrconfig.xml @@ -0,0 +1,1550 @@ + + + + + + + + + ${solr.abortOnConfigurationError:true} + + + LUCENE_34 + + + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + false + + 10 + + 32 + + + + 10000 + 1000 + 10000 + + + + + + + + + native + + + + + + + + + false + 32 + 10 + + + false + + + true + + + + + 1 + + 0 + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + static firstSearcher warming in solrconfig.xml + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + 10 + + + + + + + + + + + + + + explicit + + + velocity + + browse + layout + Solritas + + edismax + *:* + 10 + *,score + + text^0.5 content^1.0 file_name^1.2 id^10.0 + + text,content,file_name,id + 3 + + + text^0.5 content^1.0 file_name^1.2 id^10.0 + + + + + + on + text content file_name + 0 + file_name + + + spellcheck + + + + + + + + + + + + + + + + + + + + + + + + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + + + search + solrpingquery + + + all + + + + + + + explicit + true + + + + + + + + + + + + textSpell + + + + + + default + name + spellchecker + + + + + + + + + + + + + + + + + false + false + 1 + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + default + + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + + 20 + + + clustering/carrot2 + + + ENGLISH + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + + + true + default + true + + name + id + + features + + true + + + + false + + edismax + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + + *:* + 10 + *,score + + + clustering + + + + + + + + + + true + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + *:* + + + + + + diff --git a/KeywordSearch/release/solr/solr/conf/spellings.txt b/KeywordSearch/release/solr/solr/conf/spellings.txt new file mode 100644 index 0000000000..162a044d56 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history diff --git a/KeywordSearch/release/solr/solr/conf/stopwords.txt b/KeywordSearch/release/solr/solr/conf/stopwords.txt new file mode 100644 index 0000000000..ae1e83eeb3 --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/stopwords.txt @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. diff --git a/KeywordSearch/release/solr/solr/conf/stopwords_en.txt b/KeywordSearch/release/solr/solr/conf/stopwords_en.txt new file mode 100644 index 0000000000..2c164c0b2a --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/stopwords_en.txt @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +# Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +such +that +the +their +then +there +these +they +this +to +was +will +with diff --git a/KeywordSearch/release/solr/solr/conf/synonyms.txt b/KeywordSearch/release/solr/solr/conf/synonyms.txt new file mode 100644 index 0000000000..5c3b95fb6a --- /dev/null +++ b/KeywordSearch/release/solr/solr/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaafoo => aaabar +bbbfoo => bbbfoo bbbbar +cccfoo => cccbar cccbaz +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + +a\,a => b\,b + diff --git a/KeywordSearch/release/solr/solr/solr.xml b/KeywordSearch/release/solr/solr/solr.xml new file mode 100644 index 0000000000..f2d49d693e --- /dev/null +++ b/KeywordSearch/release/solr/solr/solr.xml @@ -0,0 +1,33 @@ + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties new file mode 100644 index 0000000000..1138936b08 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -0,0 +1,36 @@ +CTL_KeywordSearchListAction=List +CTL_KeywordSearchListTopComponent=List +CTL_KeywordSearchTabsTopComponentAction=Keyword Search +CTL_KeywordSearchTabsTopComponentTopComponent=Keyword Search +HINT_KeywordSearchListTopComponent=Keyword Search List +HINT_KeywordSearchTabsTopComponentTopComponent=Keyword Search window +OpenIDE-Module-Name=KeywordSearch +IndexProgressPanel.statusText.text=Status text +IndexProgressPanel.cancelButton.text=Cancel +KeywordSearchTopComponent.filesIndexedNameLabel.AccessibleContext.accessibleName=Files indexed: +KeywordSearchTopComponent.filesIndexedValLabel.AccessibleContext.accessibleName=- +ExtractedContentPanel.hitLabel.text=Match: +ExtractedContentPanel.hitCountLabel.text=- +ExtractedContentPanel.hitOfLabel.text=of +ExtractedContentPanel.hitTotalLabel.text=- +ExtractedContentPanel.hitButtonsLabel.text=Match +ExtractedContentPanel.hitPreviousButton.text= +ExtractedContentPanel.hitNextButton.text= +KeywordSearchSimpleTopComponent.filesIndexedValLabel.text=- +KeywordSearchSimpleTopComponent.filesIndexedNameLabel.text=Files indexed: +KeywordSearchSimpleTopComponent.queryLabel.text=Query: +KeywordSearchSimpleTopComponent.searchButton.text=Search +KeywordSearchListTopComponent.searchButton.text=Search +KeywordSearchListTopComponent.filesIndexedNameLabel.text=Files indexed: +KeywordSearchListTopComponent.filesIndexedValLabel.text=- +KeywordSearchListTopComponent.titleLabel.text=Search for or load a saved list of keywords +KeywordSearchListTopComponent.listLabel.text=Current list of keywords: +KeywordSearchListTopComponent.addWordButton.text=Add +KeywordSearchListTopComponent.loadListButton.text=Load +KeywordSearchListTopComponent.addWordField.text= +KeywordSearchListTopComponent.saveListButton.text=Save +KeywordSearchListTopComponent.chLiteralWord.text=Literal +KeywordSearchListTopComponent.addWordLabel.text=Add a new keyword: +KeywordSearchListTopComponent.deleteWordButton.text=Delete +KeywordSearchListTopComponent.deleteAllWordsButton.text=Delete All +KeywordSearchSimpleTopComponent.chRegex.text=RegEx diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java new file mode 100644 index 0000000000..148f3a5c40 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentFind.java @@ -0,0 +1,144 @@ +/* + * 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.HashMap; +import java.util.logging.Logger; + +/** + * Match tracker / find functionality for a given MarkupSource + * Supports keeping track of matches for multiple sources. + * What is a match and how to index, is a responsibility of a given MarkupSource + */ +public class ExtractedContentFind { + + private static final Logger logger = Logger.getLogger(ExtractedContentFind.class.getName()); + + public ExtractedContentFind() { + curIndex = new HashMap(); + } + private HashMap curIndex; + + public static final int INDEX_NOT_FOUND = -2; + public static final int INDEX_INITIALIZED = -1; + + /** + * get total number of matches in the source + * @param source + * @return number of matches in the source + */ + public int getCurrentIndexTotal(MarkupSource source) { + return source.getNumberHits(); + } + + /** + * get current match + * @param source + * @return current match + */ + public int getCurrentIndexI(MarkupSource source) { + Integer curI = curIndex.get(source); + if (curI != null) { + return curI; + } else { + return -1; + } + } + + /** + * Check if there is a next match + * @param source + * @return true if the source has next match + */ + public boolean hasNext(MarkupSource source) { + int total = source.getNumberHits(); + int cur = curIndex.get(source); + if (total == 0) { + return false; + } else if (cur == INDEX_INITIALIZED) { + return true; + } else if (cur == total - 1) { + return false; + } + return true; + } + + /** + * Check if there is a previous match + * @param source + * @return true if the source has previous match + */ + public boolean hasPrevious(MarkupSource source) { + int total = source.getNumberHits(); + int cur = curIndex.get(source); + if (total == 0) { + return false; + } else if (cur == INDEX_INITIALIZED) { + return false; + } else if (cur == 0) { + return false; + } + return true; + } + + /** + * make step toward next match and return the next index + * or INDEX_NOT_FOUND if no next match + * @param source + * @return index corresponding to next match + */ + public long getNext(MarkupSource source) { + int total = source.getNumberHits(); + int cur = curIndex.get(source); + if (total == 0 || cur == total - 1) { + return INDEX_NOT_FOUND; + } + ++cur; + //update curIndex location + curIndex.put(source, cur); + return cur; + } + + /** + * make step toward previous match and return the prev. index + * or INDEX_NOT_FOUND if no next match + * @param source + * @return index corresponding to prev. match + */ + public long getPrevious(MarkupSource source) { + int total = source.getNumberHits(); + int cur = curIndex.get(source); + if (total == 0 || cur == 0) { + return INDEX_NOT_FOUND; + } + --cur; + //update curIndex location + curIndex.put(source, cur); + return cur; + } + + /** + * initialize find functionality for the source + * @param source MarkupSource to initialize find with + */ + public void init(MarkupSource source) { + if (curIndex.get(source) == null) + curIndex.put(source, INDEX_INITIALIZED); + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form new file mode 100644 index 0000000000..1f8c206d52 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.form @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java new file mode 100644 index 0000000000..079fd35179 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java @@ -0,0 +1,251 @@ +/* + * 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.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * Panel displays HTML content sent to ExtractedContentViewer, and provides + * a combo-box to select between multiple sources. + */ +class ExtractedContentPanel extends javax.swing.JPanel { + + private static Logger logger = Logger.getLogger(ExtractedContentPanel.class.getName()); + + ExtractedContentPanel() { + initComponents(); + + initControls(); + extractedTextPane.setContentType("text/html"); + + sourceComboBox.addItemListener(new ItemListener() { + + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + setPanelText(((MarkupSource) e.getItem()).getMarkup()); + } + } + }); + + + setSources(Collections.EMPTY_LIST); + } + + /** 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() { + + jScrollPane1 = new javax.swing.JScrollPane(); + extractedTextPane = new javax.swing.JTextPane(); + sourceComboBox = new javax.swing.JComboBox(); + hitLabel = new javax.swing.JLabel(); + hitCountLabel = new javax.swing.JLabel(); + hitOfLabel = new javax.swing.JLabel(); + hitTotalLabel = new javax.swing.JLabel(); + hitButtonsLabel = new javax.swing.JLabel(); + hitPreviousButton = new javax.swing.JButton(); + hitNextButton = new javax.swing.JButton(); + + extractedTextPane.setEditable(false); + extractedTextPane.setAutoscrolls(false); + jScrollPane1.setViewportView(extractedTextPane); + + sourceComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); + + hitLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitLabel.text")); // NOI18N + + hitCountLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + hitCountLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitCountLabel.text")); // NOI18N + + hitOfLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitOfLabel.text")); // NOI18N + + hitTotalLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + hitTotalLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitTotalLabel.text")); // NOI18N + + hitButtonsLabel.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitButtonsLabel.text")); // NOI18N + + hitPreviousButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif"))); // NOI18N + hitPreviousButton.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitPreviousButton.text")); // NOI18N + + hitNextButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif"))); // NOI18N + hitNextButton.setText(org.openide.util.NbBundle.getMessage(ExtractedContentPanel.class, "ExtractedContentPanel.hitNextButton.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addComponent(hitLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hitOfLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitTotalLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(27, 27, 27) + .addComponent(hitButtonsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(hitPreviousButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(hitNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 87, Short.MAX_VALUE) + .addComponent(sourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(sourceComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(hitLabel) + .addComponent(hitButtonsLabel) + .addComponent(hitOfLabel) + .addComponent(hitTotalLabel) + .addComponent(hitCountLabel)) + .addComponent(hitPreviousButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(hitNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 287, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextPane extractedTextPane; + private javax.swing.JLabel hitButtonsLabel; + private javax.swing.JLabel hitCountLabel; + private javax.swing.JLabel hitLabel; + private javax.swing.JButton hitNextButton; + private javax.swing.JLabel hitOfLabel; + private javax.swing.JButton hitPreviousButton; + private javax.swing.JLabel hitTotalLabel; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JComboBox sourceComboBox; + // End of variables declaration//GEN-END:variables + + /** + * Set the available sources (selects the first source in the list by + * default) + * @param sources + */ + void setSources(List sources) { + sourceComboBox.removeAllItems(); + setPanelText(null); + + for (MarkupSource ms : sources) { + sourceComboBox.addItem(ms); + } + + if (!sources.isEmpty()) { + sourceComboBox.setSelectedIndex(0); + } + } + + /** + * + * @return currently available sources on the panel + */ + public List getSources() { + ArrayList sources = new ArrayList(); + for (int i = 0; i < sourceComboBox.getItemCount(); ++i) { + sources.add((MarkupSource) sourceComboBox.getItemAt(i)); + } + return sources; + } + + /** + * + * @return currently selected Source + */ + public MarkupSource getSelectedSource() { + return (MarkupSource) sourceComboBox.getSelectedItem(); + } + + private void setPanelText(String text) { + extractedTextPane.setText(text); + extractedTextPane.setCaretPosition(0); + } + + private void initControls() { + hitPreviousButton.setEnabled(false); + hitNextButton.setEnabled(false); + } + + + public void scrollToAnchor(String anchor) { + extractedTextPane.scrollToReference(anchor); + } + + /** + * + * @param current, current hit to update the display with + */ + public void updateCurrentDisplay(int current) { + hitCountLabel.setText(Integer.toString(current)); + } + + /** + * + * @param total total number of hits to update the display with + */ + public void updateTotalDisplay(int total) { + hitTotalLabel.setText(Integer.toString(total)); + } + + /** + * enable previous match control + * @param enable whether to enable or disable + */ + public void enablePrevControl(boolean enable) { + hitPreviousButton.setEnabled(enable); + } + + /** + * enable next match control + * @param enable whether to enable or disable + */ + public void enableNextControl(boolean enable) { + hitNextButton.setEnabled(enable); + } + + public void addPrevControlListener(ActionListener l) { + hitPreviousButton.addActionListener(l); + } + + public void addNextControlListener(ActionListener l) { + hitNextButton.addActionListener(l); + } + + public void addSourceComboControlListener(ActionListener l) { + sourceComboBox.addActionListener(l); + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java new file mode 100644 index 0000000000..6ec0ab0902 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java @@ -0,0 +1,295 @@ +/* + * 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.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.SolrServerException; +import org.openide.nodes.Node; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; +import org.apache.commons.lang.StringEscapeUtils; +import org.sleuthkit.datamodel.Content; + +/** + * Displays marked-up (HTML) content for a Node. The sources are all the + * MarkupSource items in the selected Node's lookup, plus the content that + * Solr extracted (if there is any). + */ +@ServiceProvider(service = DataContentViewer.class) +public class ExtractedContentViewer implements DataContentViewer { + + private static final Logger logger = Logger.getLogger(ExtractedContentViewer.class.getName()); + private ExtractedContentPanel panel; + private ExtractedContentFind find; + + public ExtractedContentViewer() { + find = new ExtractedContentFind(); + } + + @Override + public void setNode(final Node selectedNode) { + + // to clear the viewer + if (selectedNode == null) { + resetComponent(); + return; + } + + // sources are custom markup from the node (if available) and default + // markup is fetched from solr + List sources = new ArrayList(); + + sources.addAll(selectedNode.getLookup().lookupAll(MarkupSource.class)); + + + if (solrHasContent(selectedNode)) { + + sources.add(new MarkupSource() { + + @Override + public String getMarkup() { + try { + String content = StringEscapeUtils.escapeHtml(getSolrContent(selectedNode)); + return "
" + content.trim() + "
"; + } catch (SolrServerException ex) { + logger.log(Level.WARNING, "Couldn't get extracted content.", ex); + return ""; + } + } + + @Override + public String toString() { + return "Extracted Content"; + } + + @Override + public boolean isSearchable() { + return false; + } + + @Override + public String getAnchorPrefix() { + return ""; + } + + @Override + public int getNumberHits() { + return 0; + } + }); + + } + + // first source will be the default displayed + setPanel(sources); + } + + @Override + public String getTitle() { + return "Text View"; + } + + @Override + public String getToolTip() { + return "Displays extracted text and keyword-search results."; + } + + @Override + public DataContentViewer getInstance() { + return new ExtractedContentViewer(); + } + + @Override + public Component getComponent() { + if (panel == null) { + panel = new ExtractedContentPanel(); + panel.addPrevControlListener(new PrevFindActionListener()); + panel.addNextControlListener(new NextFindActionListener()); + panel.addSourceComboControlListener(new SourceChangeActionListener()); + } + return panel; + } + + @Override + public void resetComponent() { + setPanel(Collections.EMPTY_LIST); + } + + @Override + public boolean isSupported(Node node) { + if (node == null) { + return false; + } + + Collection sources = node.getLookup().lookupAll(MarkupSource.class); + + return !sources.isEmpty() || solrHasContent(node); + } + + @Override + public boolean isPreferred(Node node, boolean isSupported) { + return isSupported; + } + + /** + * Set the MarkupSources for the panel to display (safe to call even if the + * panel hasn't been created yet) + * @param sources + */ + private void setPanel(List sources) { + if (panel != null) { + panel.setSources(sources); + } + } + + /** + * Check if Solr has extracted content for a given node + * @param node + * @return true if Solr has content, else false + */ + private boolean solrHasContent(Node node) { + Content content = node.getLookup().lookup(Content.class); + if (content == null) + return false; + + Server.Core solrCore = KeywordSearch.getServer().getCore(); + SolrQuery q = new SolrQuery(); + q.setQuery("*:*"); + q.addFilterQuery("id:" + content.getId()); + q.setFields("id"); + + try { + return !solrCore.query(q).getResults().isEmpty(); + } catch (SolrServerException ex) { + logger.log(Level.WARNING, "Couldn't determine whether content is supported.", ex); + return false; + } + } + + /** + * Get extracted content for a node from Solr + * @param cNode a node that has extracted content in Solr (check with + * solrHasContent(ContentNode)) + * @return the extracted content + * @throws SolrServerException if something goes wrong + */ + private String getSolrContent(Node node) throws SolrServerException { + Server.Core solrCore = KeywordSearch.getServer().getCore(); + SolrQuery q = new SolrQuery(); + q.setQuery("*:*"); + q.addFilterQuery("id:" + node.getLookup().lookup(Content.class).getId()); + q.setFields("content"); + + String content = (String) solrCore.query(q).getResults().get(0).getFieldValue("content"); + return content; + } + + class NextFindActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + if (find.hasNext(source)) { + long indexVal = find.getNext(source); + + //scroll + panel.scrollToAnchor(source.getAnchorPrefix() + Long.toString(indexVal)); + + //update display + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + //update controls if needed + if (!find.hasNext(source)) { + panel.enableNextControl(false); + } + if (find.hasPrevious(source)) { + panel.enablePrevControl(true); + } + } + } + } + + class PrevFindActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + if (find.hasPrevious(source)) { + long indexVal = find.getPrevious(source); + + //scroll + panel.scrollToAnchor(source.getAnchorPrefix() + Long.toString(indexVal)); + + //update display + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + //update controls if needed + if (!find.hasPrevious(source)) { + panel.enablePrevControl(false); + } + if (find.hasNext(source)) { + panel.enableNextControl(true); + } + } + } + } + + class SourceChangeActionListener implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + MarkupSource source = panel.getSelectedSource(); + + //setup find controls + if (source != null && source.isSearchable()) { + find.init(source); + panel.updateCurrentDisplay(find.getCurrentIndexI(source) + 1); + panel.updateTotalDisplay(find.getCurrentIndexTotal(source)); + + if (find.hasNext(source)) { + panel.enableNextControl(true); + } else { + panel.enableNextControl(false); + } + + if (find.hasPrevious(source)) { + panel.enablePrevControl(true); + } else { + panel.enablePrevControl(false); + } + } else { + panel.enableNextControl(false); + panel.enablePrevControl(false); + } + + + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/FsContentStringStream.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/FsContentStringStream.java new file mode 100755 index 0000000000..db367335f8 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/FsContentStringStream.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.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.solr.common.util.ContentStream; +import org.sleuthkit.autopsy.datamodel.DataConversion; +import org.sleuthkit.datamodel.FsContent; +import org.sleuthkit.datamodel.TskException; + +/** + * Converter from FsContent into String with specific encoding + * Then, an adapter back to Solr' ContentStream (which is a specific InputStream), + * using the same encoding + */ +public class FsContentStringStream implements ContentStream { + //supported encoding, encoding string names match java canonical names + public static enum Encoding {ASCII,}; + + private static final int MIN_ASCII_CHARS = 4; //minimum consecutive number of ASCII chars to qualify as string + + //input + private FsContent content; + private Encoding encoding; + + //converted + private String convertedString; + private InputStream convertedStream; + private long convertedLength; + + private static Logger logger = Logger.getLogger(FsContentStringStream.class.getName()); + + public FsContentStringStream(FsContent content, Encoding encoding) { + this.content = content; + this.encoding = encoding; + convertedLength = 0; + } + + public FsContent getFsContent() { + return content; + } + + + /** + * Does all the work and delegation of extracting string and converting + * to appropriate stream with the right encoding + * @throws TskException if conversion failed for any reason + */ + public void convert() throws TskException { + //read entire content and extract strings + long contentLen = content.getSize(); + byte [] data = content.read(0, contentLen); + convertedString = DataConversion.getString(data, MIN_ASCII_CHARS); + + //convert the extracted string back to byte stream with the same encoding + try { + byte [] bytes = convertedString.getBytes(encoding.toString()); + convertedLength = bytes.length; + convertedStream = new ByteArrayInputStream(bytes); + } + catch (UnsupportedEncodingException e) { + logger.log(Level.SEVERE, "Unsupported encoding " + encoding); + throw new TskException("Unsupported encoding " + encoding); + } + + } + + @Override + public String getContentType() { + return encoding.toString(); + } + + @Override + public String getName() { + return content.getName(); + } + + @Override + public Reader getReader() throws IOException { + if (convertedStream == null) + throw new UnsupportedOperationException("Not supported yet."); + return new InputStreamReader(convertedStream); + + } + + @Override + public Long getSize() { + return convertedLength; + } + + @Override + public String getSourceInfo() { + return "File:" + content.getId(); + } + + @Override + public InputStream getStream() throws IOException { + return convertedStream; + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetAllFilesContentVisitor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetAllFilesContentVisitor.java new file mode 100755 index 0000000000..470e3b2d68 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetAllFilesContentVisitor.java @@ -0,0 +1,66 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.FsContent; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.datamodel.TskData.FileKnown; + +/** + * Visitor for getting all the files to try to index from any Content object. + * Currently gets all non-zero files. + */ +class GetAllFilesContentVisitor extends GetFilesContentVisitor { + + private static final Logger logger = Logger.getLogger(GetAllFilesContentVisitor.class.getName()); + + @Override + public Collection visit(File file) { + return Collections.singleton((FsContent) file); + } + + @Override + public Collection visit(FileSystem fs) { + // Files in the database have a filesystem field, so it's quick to + // get all the matching files for an entire filesystem with a query + + SleuthkitCase sc = Case.getCurrentCase().getSleuthkitCase(); + + String query = "SELECT * FROM tsk_files WHERE fs_obj_id = " + fs.getId() + + " AND (meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getMetaType() + + ") AND (known != " + FileKnown.KNOWN.toLong() + ") AND (size > 0)"; + try { + ResultSet rs = sc.runQuery(query); + return sc.resultSetToFsContents(rs); + } catch (SQLException ex) { + logger.log(Level.WARNING, "Couldn't get all files in FileSystem", ex); + return Collections.EMPTY_SET; + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetFilesContentVisitor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetFilesContentVisitor.java new file mode 100644 index 0000000000..eebb36a16a --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetFilesContentVisitor.java @@ -0,0 +1,104 @@ +/* + * 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.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentVisitor; +import org.sleuthkit.datamodel.Directory; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.FsContent; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.TskException; +import org.sleuthkit.datamodel.Volume; +import org.sleuthkit.datamodel.VolumeSystem; + +/** + * Abstract visitor for getting all the files from content + */ +public abstract class GetFilesContentVisitor implements ContentVisitor> { + + private static final Logger logger = Logger.getLogger(GetFilesContentVisitor.class.getName()); + + @Override + public abstract Collection visit(File file); + + @Override + public abstract Collection visit(FileSystem fs); + + @Override + public Collection visit(Directory drctr) { + return getAllFromChildren(drctr); + } + + @Override + public Collection visit(Image image) { + return getAllFromChildren(image); + } + + @Override + public Collection visit(Volume volume) { + return getAllFromChildren(volume); + } + + @Override + public Collection visit(VolumeSystem vs) { + return getAllFromChildren(vs); + } + + /** + * Aggregate all the matches from visiting the children Content objects of the + * one passed + * @param parent + * @return + */ + protected Collection getAllFromChildren(Content parent) { + Collection all = new ArrayList(); + + try { + for (Content child : parent.getChildren()) { + all.addAll(child.accept(this)); + } + } catch (TskException ex) { + logger.log(Level.SEVERE, "Error getting Content children", ex); + } + + return all; + } + + /** + * Get the part of a file name after (not including) the last '.' and + * coerced to lowercase. + * @param fileName + * @return the file extension, or an empty string if there is none + */ + protected static String getExtension(String fileName) { + int lastDot = fileName.lastIndexOf("."); + + if (lastDot >= 0) { + return fileName.substring(lastDot + 1, fileName.length()).toLowerCase(); + } else { + return ""; + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetIngestableFilesContentVisitor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetIngestableFilesContentVisitor.java new file mode 100755 index 0000000000..1fae4d3778 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GetIngestableFilesContentVisitor.java @@ -0,0 +1,103 @@ +/* + * 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.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.FsContent; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskData.FileKnown; +import org.sleuthkit.datamodel.TskData; + + +/** + * Visitor for getting all the files to try to index from any Content object. + * Currently gets all the non-zero sized files with a file extensions that match a list of + * document types that Tika/Solr-Cell supports. + */ +class GetIngestableFilesContentVisitor extends GetFilesContentVisitor { + + private static final Logger logger = Logger.getLogger(GetIngestableFilesContentVisitor.class.getName()); + + // TODO: use a more robust method than checking file extension to determine + // whether to try a file + + // supported extensions list from http://www.lucidimagination.com/devzone/technical-articles/content-extraction-tika + private static final String[] supportedExtensions = {"tar", "jar", "zip", "bzip2", + "gz", "tgz", "doc", "xls", "ppt", "rtf", "pdf", "html", "xhtml", "txt", + "bmp", "gif", "png", "jpeg", "tiff", "mp3", "aiff", "au", "midi", "wav", + "pst", "xml", "class"}; + // the full predicate of a SQLite statement to match supported extensions + private static final String extensionsLikePredicate; + + static { + // build the query fragment for matching file extensions + + StringBuilder likes = new StringBuilder("0"); + + for (String ext : supportedExtensions) { + likes.append(" OR (name LIKE '%."); + likes.append(ext); + likes.append("')"); + } + + extensionsLikePredicate = likes.toString(); + } + + + + @Override + public Collection visit(File file) { + String extension = getExtension(file.getName()); + if (Arrays.asList(supportedExtensions).contains(extension)) { + return Collections.singleton((FsContent) file); + } else { + return Collections.EMPTY_LIST; + } + } + + @Override + public Collection visit(FileSystem fs) { + // Files in the database have a filesystem field, so it's quick to + // get all the matching files for an entire filesystem with a query + + SleuthkitCase sc = Case.getCurrentCase().getSleuthkitCase(); + + String query = "SELECT * FROM tsk_files WHERE fs_obj_id = " + fs.getId() + + " AND (" + extensionsLikePredicate + ")" + + " AND (known != " + FileKnown.KNOWN.toLong() + ")" + + " AND (meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getMetaType() + ")" + + " AND (size > 0)"; + try { + ResultSet rs = sc.runQuery(query); + return sc.resultSetToFsContents(rs); + } catch (SQLException ex) { + logger.log(Level.WARNING, "Couldn't get all files in FileSystem", ex); + return Collections.EMPTY_SET; + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java new file mode 100644 index 0000000000..a1eba61173 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedMatchesSource.java @@ -0,0 +1,134 @@ +/* + * 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 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.response.QueryResponse; +import org.sleuthkit.autopsy.keywordsearch.Server.Core; +import org.sleuthkit.datamodel.Content; + +/** + * Gets extracted content from Solr with the parts that match the query + * highlighted + */ +class HighlightedMatchesSource implements MarkupSource { + + private static final Logger logger = Logger.getLogger(HighlightedMatchesSource.class.getName()); + private static final String HIGHLIGHT_PRE = ""; + private static final String HIGHLIGHT_POST = ""; + private static final String ANCHOR_PREFIX = HighlightedMatchesSource.class.getName() + "_"; + private Content content; + private String solrQuery; + private Core solrCore; + private int numberHits; + + HighlightedMatchesSource(Content content, String solrQuery) { + this(content, solrQuery, KeywordSearch.getServer().getCore()); + } + + HighlightedMatchesSource(Content content, String solrQuery, Core solrCore) { + this.content = content; + this.solrQuery = solrQuery; + this.solrCore = solrCore; + } + + @Override + public String getMarkup() { + + SolrQuery q = new SolrQuery(); + final String queryEscaped = KeywordSearchUtil.escapeLuceneQuery(solrQuery, true, false); + + q.setQuery(queryEscaped); + q.addFilterQuery("id:" + content.getId()); + q.addHighlightField("content"); //for exact highlighting, try content_ws field (with stored="true" in Solr schema) + q.setHighlightSimplePre(HIGHLIGHT_PRE); + q.setHighlightSimplePost(HIGHLIGHT_POST); + q.setHighlightFragsize(0); // don't fragment the highlight + + try { + QueryResponse response = solrCore.query(q, METHOD.POST); + Map>> responseHighlight = response.getHighlighting(); + long contentID = content.getId(); + Map> responseHighlightID = responseHighlight.get(Long.toString(contentID)); + final String NO_MATCHES = "No matches in content."; + if (responseHighlightID == null) { + return NO_MATCHES; + } + List 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 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.ActionEvent; +import java.beans.PropertyChangeListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.AbstractAction; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrServer; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer; +import org.apache.solr.client.solrj.request.CoreAdminRequest; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.TermsResponse; +import org.apache.commons.httpclient.NoHttpResponseException; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrRequest.METHOD; +import org.openide.modules.InstalledFileLocator; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.Content; + +/** + * Handles for keeping track of a Solr server and its cores + */ +class Server { + + private static final Logger logger = Logger.getLogger(Server.class.getName()); + private static final String DEFAULT_CORE_NAME = "coreCase"; + // TODO: DEFAULT_CORE_NAME needs to be replaced with unique names to support multiple open cases + public static final String CORE_EVT = "CORE_EVT"; + + public enum CORE_EVT_STATES { + + STOPPED, STARTED + }; + private CommonsHttpSolrServer solrServer; + private String instanceDir; + private File solrFolder; + private ServerAction serverAction; + + /** + * New instance for the server at the given URL + * @param url should be something like "http://localhost:8983/solr/" + */ + Server(String url) { + try { + this.solrServer = new CommonsHttpSolrServer(url); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + + serverAction = new ServerAction(); + solrFolder = InstalledFileLocator.getDefault().locate("solr", Server.class.getPackage().getName(), false); + instanceDir = solrFolder.getAbsolutePath() + File.separator + "solr"; + } + + @Override + public void finalize() throws java.lang.Throwable { + stop(); + super.finalize(); + } + + public void addServerActionListener(PropertyChangeListener l) { + serverAction.addPropertyChangeListener(l); + } + + /** + * Helper threads to handle stderr/stdout from Solr process + */ + private static class InputStreamPrinterThread extends Thread { + + InputStream stream; + + InputStreamPrinterThread(InputStream stream) { + this.stream = stream; + } + + @Override + public void run() { + InputStreamReader isr = new InputStreamReader(stream); + BufferedReader br = new BufferedReader(isr); + String line = null; + try { + while ((line = br.readLine()) != null) { + logger.log(Level.INFO, "SOLR OUTPUT: " + line.trim()); + } + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + /** + * Tries to start a Solr instance in a separate process. Returns immediately + * (probably before the server is ready) and doesn't check whether it was + * successful. + */ + void start() { + logger.log(Level.INFO, "Starting Solr server from: " + solrFolder.getAbsolutePath()); + try { + Process start = Runtime.getRuntime().exec("java -DSTOP.PORT=8079 -DSTOP.KEY=mysecret -jar start.jar", null, solrFolder); + + // Handle output to prevent process from blocking + (new InputStreamPrinterThread(start.getInputStream())).start(); + (new InputStreamPrinterThread(start.getErrorStream())).start(); + + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Tries to stop a Solr instance. + * + * Waits for the stop command to finish + * before returning. + * @return true if the stop command finished successfully, else false + */ + boolean stop() { + try { + logger.log(Level.INFO, "Stopping Solr server from: " + solrFolder.getAbsolutePath()); + Process stop = Runtime.getRuntime().exec("java -DSTOP.PORT=8079 -DSTOP.KEY=mysecret -jar start.jar --stop", null, solrFolder); + return stop.waitFor() == 0; + + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Tests if there's a Solr server running by sending it a core-status request. + * @return false if the request failed with a connection error, otherwise true + */ + boolean isRunning() { + + try { + // making a status request here instead of just doing solrServer.ping(), because + // that doesn't work when there are no cores + + CoreAdminRequest.getStatus(null, solrServer); + } catch (SolrServerException ex) { + + Throwable cause = ex.getRootCause(); + + // TODO: check if SocketExceptions should actually happen (is + // probably caused by starting a connection as the server finishes + // shutting down) + if (cause instanceof ConnectException || cause instanceof SocketException || cause instanceof NoHttpResponseException) { + return false; + } else { + throw new RuntimeException("Error checking if server is running", ex); + } + } catch (IOException ex) { + throw new RuntimeException("Error checking if server is running", ex); + } + + return true; + } + /**** Convenience methods for use while we only open one case at a time ****/ + private Core currentCore = null; + + void openCore() { + if (currentCore != null) { + throw new RuntimeException("Already an open Core!"); + } + currentCore = openCore(Case.getCurrentCase()); + serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STARTED); + } + + void closeCore() { + if (currentCore == null) { + throw new RuntimeException("No currently open Core!"); + } + currentCore.close(); + currentCore = null; + serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STOPPED); + } + + Core getCore() { + if (currentCore == null) { + throw new RuntimeException("No currently open Core!"); + } + return currentCore; + } + + /**** end single-case specific methods ****/ + /** + * Open a core for the given case + * @param c + * @return + */ + Core openCore(Case c) { + String sep = File.separator; + String dataDir = c.getCaseDirectory() + sep + "keywordsearch" + sep + "data"; + return this.openCore(DEFAULT_CORE_NAME, new File(dataDir)); + } + + /** + * Open a new core + * @param coreName name to refer to the core by in Solr + * @param dataDir directory to load/store the core data from/to + * @return new core + */ + Core openCore(String coreName, File dataDir) { + try { + if (!dataDir.exists()) { + dataDir.mkdirs(); + } + + CoreAdminRequest.Create createCore = new CoreAdminRequest.Create(); + createCore.setDataDir(dataDir.getAbsolutePath()); + createCore.setInstanceDir(instanceDir); + createCore.setCoreName(coreName); + + this.solrServer.request(createCore); + + return new Core(coreName); + + } catch (SolrServerException ex) { + throw new RuntimeException(ex); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + class Core { + + // handle to the core in Solr + private String name; + // the server to access a core needs to be built from a URL with the + // core in it, and is only good for core-specific operations + private SolrServer solrCore; + + private Core(String name) { + this.name = name; + try { + this.solrCore = new CommonsHttpSolrServer(solrServer.getBaseURL() + "/" + name); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + } + + public Ingester getIngester() { + return new Ingester(this.solrCore); + } + + public QueryResponse query(SolrQuery sq) throws SolrServerException { + return solrCore.query(sq); + } + + public QueryResponse query(SolrQuery sq, SolrRequest.METHOD method) throws SolrServerException { + return solrCore.query(sq, method); + } + + + public TermsResponse queryTerms(SolrQuery sq) throws SolrServerException { + QueryResponse qres = solrCore.query(sq); + return qres.getTermsResponse(); + } + + public String getSolrContent(final Content content) { + final SolrQuery q = new SolrQuery(); + q.setQuery("*:*"); + q.addFilterQuery("id:" + content.getId()); + q.setFields("content"); + try { + return (String) solrCore.query(q).getResults().get(0).getFieldValue("content"); + } catch (SolrServerException ex) { + logger.log(Level.WARNING, "Error getting content from Solr and validating regex match", ex); + return null; + } + } + + void close() { + try { + CoreAdminRequest.unloadCore(this.name, solrServer); + } catch (SolrServerException ex) { + throw new RuntimeException(ex); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Execute query that gets only number of all Solr documents indexed + * without actually returning the documents + * @return int representing number of indexed files + * @throws SolrServerException + */ + public int queryNumIndexedFiles() throws SolrServerException { + SolrQuery q = new SolrQuery("*:*"); + q.setRows(0); + return (int) query(q).getResults().getNumFound(); + } + } + + class ServerAction extends AbstractAction { + + @Override + public void actionPerformed(ActionEvent e) { + logger.log(Level.INFO, e.paramString().trim()); + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java new file mode 100644 index 0000000000..83492307d0 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java @@ -0,0 +1,305 @@ +/* + * 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.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import javax.swing.SwingWorker; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.response.TermsResponse; +import org.apache.solr.client.solrj.response.TermsResponse.Term; +import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.progress.ProgressHandleFactory; +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.KeywordSearchQueryManager.Presentation; +import org.sleuthkit.datamodel.FsContent; + +public class TermComponentQuery implements KeywordSearchQuery { + + private static final int TERMS_UNLIMITED = -1; + //corresponds to field in Solr schema, analyzed with white-space tokenizer only + private static final String TERMS_SEARCH_FIELD = "content_ws"; + private static final String TERMS_HANDLER = "/terms"; + private static final int TERMS_TIMEOUT = 90 * 1000; //in ms + private static Logger logger = Logger.getLogger(TermComponentQuery.class.getName()); + private String termsQuery; + private String queryEscaped; + private boolean isEscaped; + private List terms; + + public TermComponentQuery(String query) { + this.termsQuery = query; + this.queryEscaped = query; + isEscaped = false; + terms = null; + } + + @Override + public void escape() { + //treat as literal + //TODO for actual literal query to work in Java/Solr + //might need to either: use terms prefix (not regex) query with the literal + //or append .* to the literal regex + queryEscaped = Pattern.quote(termsQuery); + isEscaped = true; + } + + @Override + public boolean validate() { + if (queryEscaped.equals("")) { + return false; + } + + boolean valid = true; + try { + Pattern.compile(queryEscaped); + } catch (PatternSyntaxException ex1) { + valid = false; + } catch (IllegalArgumentException ex2) { + valid = false; + } + return valid; + } + + /* + * helper method to create a Solr terms component query + */ + protected SolrQuery createQuery() { + final SolrQuery q = new SolrQuery(); + q.setQueryType(TERMS_HANDLER); + q.setTerms(true); + q.setTermsLimit(TERMS_UNLIMITED); + q.setTermsRegexFlag("case_insensitive"); + //q.setTermsLimit(200); + //q.setTermsRegexFlag(regexFlag); + //q.setTermsRaw(true); + q.setTermsRegex(queryEscaped); + q.addTermsField(TERMS_SEARCH_FIELD); + q.setTimeAllowed(TERMS_TIMEOUT); + + return q; + + } + + /* + * execute query and return terms, helper method + */ + protected List executeQuery(SolrQuery q) { + Server.Core solrCore = KeywordSearch.getServer().getCore(); + + List termsCol = null; + try { + TermsResponse tr = solrCore.queryTerms(q); + termsCol = tr.getTerms(TERMS_SEARCH_FIELD); + return termsCol; + } catch (SolrServerException ex) { + logger.log(Level.SEVERE, "Error executing the regex terms query: " + termsQuery, ex); + return null; //no need to create result view, just display error dialog + } + } + + @Override + public String getEscapedQueryString() { + return this.queryEscaped; + } + + @Override + public String getQueryString() { + return this.termsQuery; + } + + @Override + public Collection getTerms() { + return terms; + } + + /** + * return collapsed matches with all files for the query + * without per match breakdown + */ + @Override + public List performQuery() { + List results = new ArrayList(); + + final SolrQuery q = createQuery(); + terms = executeQuery(q); + + //get unique match result files + + + //combine the terms into single Solr query to get files + //it's much more efficient and should yield the same file IDs as per match queries + //requires http POST query method due to potentially large query size + StringBuilder filesQueryB = new StringBuilder(); + final int lastTerm = terms.size() - 1; + int curTerm = 0; + for (Term term : terms) { + final String termS = KeywordSearchUtil.escapeLuceneQuery(term.getTerm(), true, false); + //final String termS = term.getTerm(); + if (!termS.contains("*")) { + filesQueryB.append(TERMS_SEARCH_FIELD).append(":").append(termS); + if (curTerm != lastTerm) { + filesQueryB.append(" "); //acts as OR || + } + } + ++curTerm; + } + List uniqueMatches = new ArrayList(); + + if (!terms.isEmpty()) { + LuceneQuery filesQuery = new LuceneQuery(filesQueryB.toString()); + //filesQuery.escape(); + try { + uniqueMatches = filesQuery.performQuery(); + } catch (RuntimeException e) { + logger.log(Level.SEVERE, "Error executing Solr query,", e); + } + } + + + //result postprocessing + //filter out non-matching files using the original query (whether literal or not) + boolean postprocess = false; + if (postprocess) { + for (FsContent f : uniqueMatches) { + Pattern p = Pattern.compile(queryEscaped, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + final String contentStr = KeywordSearch.getServer().getCore().getSolrContent(f); + Matcher m = p.matcher(contentStr); + if (m.find()) { + results.add(f); + } + } + } else { + results.addAll(uniqueMatches); + } + + + + return results; + } + + @Override + public void execute() { + SolrQuery q = createQuery(); + + logger.log(Level.INFO, "Executing TermsComponent query: " + q.toString()); + + final SwingWorker worker = new TermsQueryWorker(q); + worker.execute(); + } + + /** + * map Terms to generic Nodes with key/value pairs properties + * @param terms + */ + private void publishNodes(List terms) { + + Collection things = new ArrayList(); + + Iterator it = terms.iterator(); + int termID = 0; + //long totalMatches = 0; + while (it.hasNext()) { + Term term = it.next(); + Map kvs = new LinkedHashMap(); + //long matches = term.getFrequency(); + final String match = term.getTerm(); + KeywordSearchResultFactory.setCommonProperty(kvs, KeywordSearchResultFactory.CommonPropertyTypes.MATCH, match); + //setCommonProperty(kvs, CommonPropertyTypes.MATCH_RANK, Long.toString(matches)); + things.add(new KeyValueThing(match, kvs, ++termID)); + //totalMatches += matches; + } + + Node rootNode = null; + if (things.size() > 0) { + Children childThingNodes = + Children.create(new KeywordSearchResultFactory(termsQuery, things, Presentation.DETAIL), true); + + rootNode = new AbstractNode(childThingNodes); + } else { + rootNode = Node.EMPTY; + } + + final String pathText = "Term query"; + // String pathText = "RegEx query: " + termsQuery + //+ " Files with exact matches: " + Long.toString(totalMatches) + " (also listing approximate matches)"; + + TopComponent searchResultWin = DataResultTopComponent.createInstance("Keyword search", pathText, rootNode, things.size()); + searchResultWin.requestActive(); // make it the active top component + + } + + class TermsQueryWorker extends SwingWorker, Void> { + + private SolrQuery q; + private ProgressHandle progress; + + TermsQueryWorker(SolrQuery q) { + this.q = q; + } + + @Override + protected List doInBackground() throws Exception { + progress = ProgressHandleFactory.createHandle("Terms query task"); + progress.start(); + progress.progress("Running Terms query."); + + terms = executeQuery(q); + + progress.progress("Terms query completed."); + + return terms; + } + + @Override + protected void done() { + if (!this.isCancelled()) { + try { + List terms = get(); + publishNodes(terms); + } catch (InterruptedException e) { + logger.log(Level.INFO, "Exception while executing regex query,", e); + + } catch (ExecutionException e) { + logger.log(Level.INFO, "Exception while executing regex query,", e); + } finally { + progress.finish(); + } + } + } + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif new file mode 100644 index 0000000000..d0d85dba4b Binary files /dev/null and b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_left.gif differ diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif new file mode 100644 index 0000000000..85272ad99c Binary files /dev/null and b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/arrow_right.gif differ diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/layer.xml b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/layer.xml new file mode 100644 index 0000000000..40f6e5e00b --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/layer.xml @@ -0,0 +1,3 @@ + + + diff --git a/Logging/manifest.mf b/Logging/manifest.mf deleted file mode 100644 index 7ee7025dd7..0000000000 --- a/Logging/manifest.mf +++ /dev/null @@ -1,7 +0,0 @@ -Manifest-Version: 1.0 -OpenIDE-Module: org.sleuthkit.autopsy.logging/0 -OpenIDE-Module-Implementation-Version: 2 -OpenIDE-Module-Install: org/sleuthkit/autopsy/logging/Installer.class -OpenIDE-Module-Layer: org/sleuthkit/autopsy/logging/layer.xml -OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/logging/Bundle.properties - diff --git a/Logging/nbproject/genfiles.properties b/Logging/nbproject/genfiles.properties deleted file mode 100644 index d94cf24310..0000000000 --- a/Logging/nbproject/genfiles.properties +++ /dev/null @@ -1,8 +0,0 @@ -build.xml.data.CRC32=cacaba65 -build.xml.script.CRC32=5960e63f -build.xml.stylesheet.CRC32=a56c6a5b@1.42.2 -# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. -# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=cacaba65 -nbproject/build-impl.xml.script.CRC32=db164ed0 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.42.2 diff --git a/Logging/nbproject/platform.properties b/Logging/nbproject/platform.properties deleted file mode 100644 index 319807f1cd..0000000000 --- a/Logging/nbproject/platform.properties +++ /dev/null @@ -1,7 +0,0 @@ -cluster.path=\ - ${nbplatform.active.dir}/platform -disabled.modules=\ - org.netbeans.libs.jsr223,\ - org.openide.compat,\ - org.openide.util.enumerations -nbplatform.active=default diff --git a/Logging/nbproject/private/platform-private.properties b/Logging/nbproject/private/platform-private.properties deleted file mode 100644 index feb70af372..0000000000 --- a/Logging/nbproject/private/platform-private.properties +++ /dev/null @@ -1 +0,0 @@ -user.properties.file=C:\\Users\\pmartel\\.netbeans\\6.9\\build.properties diff --git a/Logging/nbproject/private/private.xml b/Logging/nbproject/private/private.xml deleted file mode 100644 index 137618d8d2..0000000000 --- a/Logging/nbproject/private/private.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - src/org/sleuthkit/autopsy/logging/Installer.java - 62 - - - diff --git a/Logging/src/org/sleuthkit/autopsy/logging/Bundle.properties b/Logging/src/org/sleuthkit/autopsy/logging/Bundle.properties deleted file mode 100644 index 7ff32e866b..0000000000 --- a/Logging/src/org/sleuthkit/autopsy/logging/Bundle.properties +++ /dev/null @@ -1,2 +0,0 @@ -CTL_TestLogger=Test log -OpenIDE-Module-Name=Logging diff --git a/MenuActions/nbproject/genfiles.properties b/MenuActions/nbproject/genfiles.properties index 5f9fb9a6c0..e7f943c626 100644 --- a/MenuActions/nbproject/genfiles.properties +++ b/MenuActions/nbproject/genfiles.properties @@ -1,8 +1,8 @@ -build.xml.data.CRC32=77b5c2dd +build.xml.data.CRC32=540a1acf build.xml.script.CRC32=46209f11 -build.xml.stylesheet.CRC32=a56c6a5b@1.46.2 +build.xml.stylesheet.CRC32=a56c6a5b@2.47.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=77b5c2dd +nbproject/build-impl.xml.data.CRC32=540a1acf nbproject/build-impl.xml.script.CRC32=cb422238 -nbproject/build-impl.xml.stylesheet.CRC32=238281d1@1.46.2 +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.47.1 diff --git a/MenuActions/nbproject/project.xml b/MenuActions/nbproject/project.xml index ab0bdefcf4..3fb349be49 100644 --- a/MenuActions/nbproject/project.xml +++ b/MenuActions/nbproject/project.xml @@ -99,15 +99,6 @@ 1.0 - - org.sleuthkit.autopsy.logging - - - - 0-1 - 0.0 - - org.sleuthkit.autopsy.menuactions diff --git a/nbproject/build-impl.xml b/nbproject/build-impl.xml index 253a284f00..ab10a335ca 100644 --- a/nbproject/build-impl.xml +++ b/nbproject/build-impl.xml @@ -38,10 +38,11 @@
- + + - + diff --git a/nbproject/genfiles.properties b/nbproject/genfiles.properties index 542aedb4cc..16657aac7f 100644 --- a/nbproject/genfiles.properties +++ b/nbproject/genfiles.properties @@ -4,5 +4,8 @@ build.xml.stylesheet.CRC32=eaf9f76a@1.42.2 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. nbproject/build-impl.xml.data.CRC32=2e7964b0 -nbproject/build-impl.xml.script.CRC32=14b0b78d -nbproject/build-impl.xml.stylesheet.CRC32=183e6ef3@1.42.2 +nbproject/build-impl.xml.script.CRC32=b72261eb +nbproject/build-impl.xml.stylesheet.CRC32=0f381476@2.47.1 +nbproject/platform.xml.data.CRC32=2e7964b0 +nbproject/platform.xml.script.CRC32=db9e1f43 +nbproject/platform.xml.stylesheet.CRC32=df8ac4dd@2.47.1 diff --git a/nbproject/platform.properties b/nbproject/platform.properties index 38ecd5a92e..3c482fc5a2 100644 --- a/nbproject/platform.properties +++ b/nbproject/platform.properties @@ -88,7 +88,6 @@ disabled.modules=\ org.netbeans.modules.refactoring.java,\ org.netbeans.modules.spellchecker.bindings.java,\ org.netbeans.modules.spring.beans,\ - org.netbeans.modules.swingapp,\ org.netbeans.modules.websvc.jaxws21,\ org.netbeans.modules.websvc.jaxws21api,\ org.netbeans.modules.websvc.saas.codegen.java,\ diff --git a/nbproject/platform.xml b/nbproject/platform.xml new file mode 100644 index 0000000000..1c86efdbef --- /dev/null +++ b/nbproject/platform.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nbproject/project.properties b/nbproject/project.properties index d78ab488e8..cce454d130 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -16,13 +16,15 @@ modules=\ ${project.org.sleuthkit.autopsy.corecomponents}:\ ${project.org.sleuthkit.autopsy.filesearch}:\ ${project.org.sleuthkit.autopsy.datamodel}:\ - ${project.org.sleuthkit.autopsy.logging}:\ - ${project.org.sleuthkit.autopsy.casemodule} + ${project.org.sleuthkit.autopsy.casemodule}:\ + ${project.org.sleuthkit.autopsy.keywordsearch}:\ + ${project.org.sleuthkit.autopsy.coreutils} project.org.sleuthkit.autopsy.casemodule=Case project.org.sleuthkit.autopsy.corecomponentinterfaces=CoreComponentInterfaces project.org.sleuthkit.autopsy.corecomponents=CoreComponents +project.org.sleuthkit.autopsy.coreutils=CoreUtils project.org.sleuthkit.autopsy.directorytree=DirectoryTree project.org.sleuthkit.autopsy.filesearch=FileSearch -project.org.sleuthkit.autopsy.logging=Logging +project.org.sleuthkit.autopsy.keywordsearch=KeywordSearch project.org.sleuthkit.autopsy.menuactions=MenuActions project.org.sleuthkit.autopsy.datamodel=DataModel