Autopsy  4.5.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
HashDbManager.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011 - 2016 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.modules.hashdatabase;
20 
21 import java.beans.PropertyChangeEvent;
22 import java.beans.PropertyChangeListener;
23 import java.beans.PropertyChangeSupport;
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Objects;
30 import java.util.Set;
31 import java.util.concurrent.ExecutionException;
32 import java.util.logging.Level;
33 import javax.swing.JFileChooser;
34 import javax.swing.JOptionPane;
35 import javax.swing.SwingWorker;
36 import javax.swing.filechooser.FileNameExtensionFilter;
37 import org.apache.commons.io.FilenameUtils;
38 import org.netbeans.api.progress.ProgressHandle;
39 import org.openide.util.NbBundle;
40 import org.openide.util.NbBundle.Messages;
51 import org.sleuthkit.datamodel.AbstractFile;
52 import org.sleuthkit.datamodel.Content;
53 import org.sleuthkit.datamodel.HashEntry;
54 import org.sleuthkit.datamodel.HashHitInfo;
55 import org.sleuthkit.datamodel.SleuthkitJNI;
56 import org.sleuthkit.datamodel.TskCoreException;
57 import org.sleuthkit.datamodel.TskData;
58 
63 public class HashDbManager implements PropertyChangeListener {
64 
65  private static final String HASH_DATABASE_FILE_EXTENSON = "kdb"; //NON-NLS
66  private static HashDbManager instance = null;
67  private List<HashDb> hashSets = new ArrayList<>();
68  private Set<String> hashSetNames = new HashSet<>();
69  private Set<String> hashSetPaths = new HashSet<>();
70  PropertyChangeSupport changeSupport = new PropertyChangeSupport(HashDbManager.class);
71  private static final Logger logger = Logger.getLogger(HashDbManager.class.getName());
72  private boolean allDatabasesLoadedCorrectly = false;
73 
79  public enum SetEvt {
80 
81  DB_ADDED, DB_DELETED, DB_INDEXED
82  };
83 
89  public static synchronized HashDbManager getInstance() {
90  if (instance == null) {
91  instance = new HashDbManager();
92  }
93  return instance;
94  }
95 
96  public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
97  changeSupport.addPropertyChangeListener(listener);
98  }
99 
100  public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
101  changeSupport.removePropertyChangeListener(listener);
102  }
103 
104  synchronized boolean verifyAllDatabasesLoadedCorrectly(){
106  }
107 
108  private HashDbManager() {
110  }
111 
117  static String getHashDatabaseFileExtension() {
119  }
120 
121  public class HashDbManagerException extends Exception {
122 
123  private static final long serialVersionUID = 1L;
124 
125  private HashDbManagerException(String message) {
126  super(message);
127  }
128 
129  private HashDbManagerException(String message, Throwable exception) {
130  super(message, exception);
131  }
132  }
133 
153  public synchronized HashDb addExistingHashDatabase(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType) throws HashDbManagerException {
154  HashDb hashDb = null;
155  hashDb = this.addExistingHashDatabaseNoSave(hashSetName, path, searchDuringIngest, sendIngestMessages, knownFilesType);
156  this.save();
157  return hashDb;
158  }
159 
160  synchronized HashDb addExistingHashDatabaseNoSave(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType) throws HashDbManagerException {
161  HashDb hashDb = null;
162  try {
163  if (!new File(path).exists()) {
164  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.hashDbDoesNotExistExceptionMsg", path));
165  }
166 
167  if (hashSetPaths.contains(path)) {
168  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.hashDbAlreadyAddedExceptionMsg", path));
169  }
170 
171  if (hashSetNames.contains(hashSetName)) {
172  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.duplicateHashSetNameExceptionMsg", hashSetName));
173  }
174 
175  hashDb = addHashDatabase(SleuthkitJNI.openHashDatabase(path), hashSetName, searchDuringIngest, sendIngestMessages, knownFilesType);
176  } catch (TskCoreException ex) {
177  throw new HashDbManagerException(ex.getMessage());
178  }
179  return hashDb;
180  }
181 
200  public synchronized HashDb addNewHashDatabase(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages,
201  HashDb.KnownFilesType knownFilesType) throws HashDbManagerException {
202 
203  HashDb hashDb = null;
204  hashDb = this.addNewHashDatabaseNoSave(hashSetName, path, searchDuringIngest, sendIngestMessages, knownFilesType);
205 
206  this.save();
207 
208  return hashDb;
209  }
210 
211  public synchronized HashDb addNewHashDatabaseNoSave(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages,
212  HashDb.KnownFilesType knownFilesType) throws HashDbManagerException {
213  HashDb hashDb = null;
214  try {
215  File file = new File(path);
216  if (file.exists()) {
217  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.hashDbFileExistsExceptionMsg", path));
218  }
219  if (!FilenameUtils.getExtension(file.getName()).equalsIgnoreCase(HASH_DATABASE_FILE_EXTENSON)) {
220  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.illegalHashDbFileNameExtensionMsg",
221  getHashDatabaseFileExtension()));
222  }
223 
224  if (hashSetPaths.contains(path)) {
225  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.hashDbAlreadyAddedExceptionMsg", path));
226  }
227 
228  if (hashSetNames.contains(hashSetName)) {
229  throw new HashDbManagerException(NbBundle.getMessage(HashDbManager.class, "HashDbManager.duplicateHashSetNameExceptionMsg", hashSetName));
230  }
231 
232  hashDb = addHashDatabase(SleuthkitJNI.createHashDatabase(path), hashSetName, searchDuringIngest, sendIngestMessages, knownFilesType);
233  } catch (TskCoreException ex) {
234  throw new HashDbManagerException(ex.getMessage());
235  }
236  return hashDb;
237  }
238 
239  private SleuthkitHashSet addHashDatabase(int handle, String hashSetName, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType) throws TskCoreException {
240  // Wrap an object around the handle.
241  SleuthkitHashSet hashDb = new SleuthkitHashSet(handle, hashSetName, searchDuringIngest, sendIngestMessages, knownFilesType);
242 
243  // Get the indentity data before updating the collections since the
244  // accessor methods may throw.
245  String databasePath = hashDb.getDatabasePath();
246  String indexPath = hashDb.getIndexPath();
247 
248  // Update the collections used to ensure that hash set names are unique
249  // and the same database is not added to the configuration more than once.
250  hashSetNames.add(hashDb.getHashSetName());
251  if (!databasePath.equals("None")) { //NON-NLS
252  hashSetPaths.add(databasePath);
253  }
254  if (!indexPath.equals("None")) { //NON-NLS
255  hashSetPaths.add(indexPath);
256  }
257 
258  // Add the hash database to the collection
259  hashSets.add(hashDb);
260 
261  // Let any external listeners know that there's a new set
262  try {
263  changeSupport.firePropertyChange(SetEvt.DB_ADDED.toString(), null, hashSetName);
264  } catch (Exception e) {
265  logger.log(Level.SEVERE, "HashDbManager listener threw exception", e); //NON-NLS
267  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErr"),
268  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErrorListeningToUpdatesMsg"),
270  }
271  return hashDb;
272  }
273 
274  CentralRepoHashSet addExistingCentralRepoHashSet(String hashSetName, String version, int referenceSetID,
275  boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType,
276  boolean readOnly) throws TskCoreException{
277 
278  if(! EamDb.isEnabled()){
279  throw new TskCoreException("Could not load central repository hash set " + hashSetName + " - central repository is not enabled");
280  }
281 
282  CentralRepoHashSet db = new CentralRepoHashSet(hashSetName, version, referenceSetID, searchDuringIngest,
283  sendIngestMessages, knownFilesType, readOnly);
284 
285  if(! db.isValid()){
286  throw new TskCoreException("Error finding hash set " + hashSetName + " in central repository");
287  }
288 
289  // Add the hash database to the collection
290  hashSets.add(db);
291 
292  // Let any external listeners know that there's a new set
293  try {
294  changeSupport.firePropertyChange(SetEvt.DB_ADDED.toString(), null, hashSetName);
295  } catch (Exception e) {
296  logger.log(Level.SEVERE, "HashDbManager listener threw exception", e); //NON-NLS
297  MessageNotifyUtil.Notify.show(
298  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErr"),
299  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErrorListeningToUpdatesMsg"),
300  MessageNotifyUtil.MessageType.ERROR);
301  }
302  return db;
303 
304  }
305 
306  synchronized void indexHashDatabase(SleuthkitHashSet hashDb) {
307  hashDb.addPropertyChangeListener(this);
308  HashDbIndexer creator = new HashDbIndexer(hashDb);
309  creator.execute();
310  }
311 
312  @Override
313  public void propertyChange(PropertyChangeEvent event) {
314  if (event.getPropertyName().equals(SleuthkitHashSet.Event.INDEXING_DONE.name())) {
315  SleuthkitHashSet hashDb = (SleuthkitHashSet) event.getNewValue();
316  if (null != hashDb) {
317  try {
318  String indexPath = hashDb.getIndexPath();
319  if (!indexPath.equals("None")) { //NON-NLS
320  hashSetPaths.add(indexPath);
321  }
322  } catch (TskCoreException ex) {
323  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDb.getHashSetName() + " hash set after indexing", ex); //NON-NLS
324  }
325  }
326  }
327  }
328 
337  public synchronized void removeHashDatabase(HashDb hashDb) throws HashDbManagerException {
338  this.removeHashDatabaseNoSave(hashDb);
339  this.save();
340  }
341 
342  public synchronized void removeHashDatabaseNoSave(HashDb hashDb) throws HashDbManagerException {
343  // Don't remove a database if ingest is running
344  boolean ingestIsRunning = IngestManager.getInstance().isIngestRunning();
345  if (ingestIsRunning) {
346  throw new HashDbManagerException(NbBundle.getMessage(this.getClass(), "HashDbManager.ingestRunningExceptionMsg"));
347  }
348  // Remove the database from whichever hash set list it occupies,
349  // and remove its hash set name from the hash set used to ensure unique
350  // hash set names are used, before undertaking These operations will succeed and constitute
351  // a mostly effective removal, even if the subsequent operations fail.
352  String hashSetName = hashDb.getHashSetName();
353  hashSetNames.remove(hashSetName);
354  hashSets.remove(hashDb);
355 
356  // Now undertake the operations that could throw.
357 
358  // Indexing is only relevanet for sleuthkit hashsets
359  if(hashDb instanceof SleuthkitHashSet){
360  SleuthkitHashSet hashDatabase = (SleuthkitHashSet)hashDb;
361  try {
362  if(hashDatabase.hasIndex()){
363  hashSetPaths.remove(hashDatabase.getIndexPath());
364  }
365  } catch (TskCoreException ex) {
366  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDatabase.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS
367  }
368 
369  try {
370  if (!hashDatabase.hasIndexOnly()) {
371  hashSetPaths.remove(hashDatabase.getDatabasePath());
372  }
373  } catch (TskCoreException ex) {
374  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting hash set path of " + hashDatabase.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS
375  }
376 
377  try {
378  hashDatabase.close();
379  } catch (TskCoreException ex) {
380  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + hashDb.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS
381  }
382  }
383 
384  // Let any external listeners know that a set has been deleted
385  try {
386  changeSupport.firePropertyChange(SetEvt.DB_DELETED.toString(), null, hashSetName);
387  } catch (Exception e) {
388  logger.log(Level.SEVERE, "HashDbManager listener threw exception", e); //NON-NLS
390  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErr"),
391  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErrorListeningToUpdatesMsg"),
393  }
394  }
395 
396  void save() throws HashDbManagerException {
397  try {
398  if (!HashLookupSettings.writeSettings(new HashLookupSettings(HashLookupSettings.convertHashSetList(this.hashSets)))) {
399  throw new HashDbManagerException(NbBundle.getMessage(this.getClass(), "HashDbManager.saveErrorExceptionMsg"));
400  }
401  } catch (HashLookupSettings.HashLookupSettingsException ex) {
402  throw new HashDbManagerException(NbBundle.getMessage(this.getClass(), "HashDbManager.saveErrorExceptionMsg"));
403  }
404  }
405 
413  public synchronized List<HashDb> getAllHashSets() {
414  try{
416  } catch (TskCoreException ex){
417  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error loading central repository hash sets", ex); //NON-NLS
418  }
419 
420  List<HashDb> hashDbs = new ArrayList<>();
421  hashDbs.addAll(this.hashSets);
422  return hashDbs;
423  }
424 
430  public synchronized List<HashDb> getKnownFileHashSets() {
431  List<HashDb> hashDbs = new ArrayList<>();
432  try{
434  } catch (TskCoreException ex){
435  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error loading central repository hash sets", ex); //NON-NLS
436  }
437  this.hashSets.stream().filter((db) -> (db.getKnownFilesType() == HashDb.KnownFilesType.KNOWN)).forEach((db) -> {
438  hashDbs.add(db);
439  });
440  return hashDbs;
441  }
442 
448  public synchronized List<HashDb> getKnownBadFileHashSets() {
449  List<HashDb> hashDbs = new ArrayList<>();
450  try{
452  } catch (TskCoreException ex){
453  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error loading central repository hash sets", ex); //NON-NLS
454  }
455  this.hashSets.stream().filter((db) -> (db.getKnownFilesType() == HashDb.KnownFilesType.KNOWN_BAD)).forEach((db) -> {
456  hashDbs.add(db);
457  });
458  return hashDbs;
459  }
460 
466  public synchronized List<HashDb> getUpdateableHashSets() {
467  return getUpdateableHashSets(this.hashSets);
468  }
469 
470  private List<HashDb> getUpdateableHashSets(List<HashDb> hashDbs) {
471  ArrayList<HashDb> updateableDbs = new ArrayList<>();
472  try{
474  } catch (TskCoreException ex){
475  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error loading central repository hash sets", ex); //NON-NLS
476  }
477  for (HashDb db : hashDbs) {
478  try {
479  if (db.isUpdateable()) {
480  updateableDbs.add(db);
481  }
482  } catch (TskCoreException ex) {
483  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error checking updateable status of " + db.getHashSetName() + " hash set", ex); //NON-NLS
484  }
485  }
486  return updateableDbs;
487  }
488 
489  private List<HashDbInfo> getCentralRepoHashSetsFromDatabase(){
490  List<HashDbInfo> crHashSets = new ArrayList<>();
491  if(EamDb.isEnabled()){
492  try{
494  for(EamGlobalSet globalSet:crSets){
495 
496  // Defaults for fields not stored in the central repository:
497  // searchDuringIngest: false
498  // sendIngestMessages: true if the hash set is notable
499  boolean sendIngestMessages = convertFileKnown(globalSet.getFileKnownStatus()).equals(HashDb.KnownFilesType.KNOWN_BAD);
500  crHashSets.add(new HashDbInfo(globalSet.getSetName(), globalSet.getVersion(),
501  globalSet.getGlobalSetID(), convertFileKnown(globalSet.getFileKnownStatus()), globalSet.isReadOnly(), false, sendIngestMessages));
502  }
503  } catch (EamDbException ex){
504  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error loading central repository hash sets", ex); //NON-NLS
505  }
506  }
507  return crHashSets;
508  }
509 
510  private static HashDb.KnownFilesType convertFileKnown(TskData.FileKnown fileKnown){
511  if(fileKnown.equals(TskData.FileKnown.BAD)){
513  }
514  return HashDb.KnownFilesType.KNOWN;
515  }
516 
521  public synchronized void loadLastSavedConfiguration() {
522  closeHashDatabases(this.hashSets);
523  hashSetNames.clear();
524  hashSetPaths.clear();
525 
527  }
528 
529  private void closeHashDatabases(List<HashDb> hashDatabases) {
530  for (HashDb database : hashDatabases) {
531  if(database instanceof SleuthkitHashSet){
532  try {
533  ((SleuthkitHashSet)database).close();
534  } catch (TskCoreException ex) {
535  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + database.getHashSetName() + " hash set", ex); //NON-NLS
536  }
537  }
538  }
539  hashDatabases.clear();
540  }
541 
542  private void loadHashsetsConfiguration() {
543  try {
544  HashLookupSettings settings = HashLookupSettings.readSettings();
545  this.configureSettings(settings);
546  } catch (HashLookupSettings.HashLookupSettingsException ex) {
547  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Could not read Hash lookup settings from disk.", ex);
548  }
549  }
550 
557  @Messages({"# {0} - hash set name", "HashDbManager.noDbPath.message=Couldn't get valid hash set path for: {0}",
558  "HashDbManager.centralRepoLoadError.message=Error loading central repository hash sets"})
559  private void configureSettings(HashLookupSettings settings) {
560  allDatabasesLoadedCorrectly = true;
561  List<HashDbInfo> hashDbInfoList = settings.getHashDbInfo();
562  for (HashDbInfo hashDbInfo : hashDbInfoList) {
563  try {
564  if(hashDbInfo.isFileDatabaseType()){
565  String dbPath = this.getValidFilePath(hashDbInfo.getHashSetName(), hashDbInfo.getPath());
566  if (dbPath != null) {
567  addHashDatabase(SleuthkitJNI.openHashDatabase(dbPath), hashDbInfo.getHashSetName(), hashDbInfo.getSearchDuringIngest(), hashDbInfo.getSendIngestMessages(), hashDbInfo.getKnownFilesType());
568  } else {
569  logger.log(Level.WARNING, Bundle.HashDbManager_noDbPath_message(hashDbInfo.getHashSetName()));
570  allDatabasesLoadedCorrectly = false;
571  }
572  } else {
573  if(EamDb.isEnabled()){
574  addExistingCentralRepoHashSet(hashDbInfo.getHashSetName(), hashDbInfo.getVersion(),
575  hashDbInfo.getReferenceSetID(),
576  hashDbInfo.getSearchDuringIngest(), hashDbInfo.getSendIngestMessages(),
577  hashDbInfo.getKnownFilesType(), hashDbInfo.isReadOnly());
578  }
579  }
580  } catch (TskCoreException ex) {
581  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash set", ex); //NON-NLS
582  JOptionPane.showMessageDialog(null,
583  NbBundle.getMessage(this.getClass(),
584  "HashDbManager.unableToOpenHashDbMsg", hashDbInfo.getHashSetName()),
585  NbBundle.getMessage(this.getClass(), "HashDbManager.openHashDbErr"),
586  JOptionPane.ERROR_MESSAGE);
587  allDatabasesLoadedCorrectly = false;
588  }
589  }
590 
591  if(EamDb.isEnabled()){
592  try{
594  } catch (TskCoreException ex){
595  Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash set", ex); //NON-NLS
596 
597  JOptionPane.showMessageDialog(null,
598  Bundle.HashDbManager_centralRepoLoadError_message(),
599  NbBundle.getMessage(this.getClass(), "HashDbManager.openHashDbErr"),
600  JOptionPane.ERROR_MESSAGE);
601  allDatabasesLoadedCorrectly = false;
602  }
603  }
604 
605  /* NOTE: When RuntimeProperties.coreComponentsAreActive() is "false",
606  I don't think we should overwrite hash db settings file because we
607  were unable to load a database. The user should have to fix the issue or
608  remove the database from settings. Overwiting the settings effectively removes
609  the database from HashLookupSettings and the user may not know about this
610  because the dialogs are not being displayed. The next time user starts Autopsy, HashDB
611  will load without errors and the user may think that the problem was solved.*/
612  if (!allDatabasesLoadedCorrectly && RuntimeProperties.runningWithGUI()) {
613  try {
614  HashLookupSettings.writeSettings(new HashLookupSettings(HashLookupSettings.convertHashSetList(this.hashSets)));
615  allDatabasesLoadedCorrectly = true;
616  } catch (HashLookupSettings.HashLookupSettingsException ex) {
617  allDatabasesLoadedCorrectly = false;
618  logger.log(Level.SEVERE, "Could not overwrite hash set settings.", ex);
619  }
620  }
621  }
622 
623  private void updateHashSetsFromCentralRepository() throws TskCoreException {
624  if(EamDb.isEnabled()){
625  List<HashDbInfo> crHashDbInfoList = getCentralRepoHashSetsFromDatabase();
626  for(HashDbInfo hashDbInfo : crHashDbInfoList) {
627  if(hashDbInfoIsNew(hashDbInfo)){
628  addExistingCentralRepoHashSet(hashDbInfo.getHashSetName(), hashDbInfo.getVersion(),
629  hashDbInfo.getReferenceSetID(),
630  hashDbInfo.getSearchDuringIngest(), hashDbInfo.getSendIngestMessages(), hashDbInfo.getKnownFilesType(),
631  hashDbInfo.isReadOnly());
632  }
633  }
634  }
635  }
636 
637  private boolean hashDbInfoIsNew(HashDbInfo dbInfo){
638  for(HashDb db:this.hashSets){
639  if(dbInfo.matches(db)){
640  return false;
641  }
642  }
643  return true;
644  }
645 
646  private String getValidFilePath(String hashSetName, String configuredPath) {
647  // Check the configured path.
648  File database = new File(configuredPath);
649  if (database.exists()) {
650  return configuredPath;
651  }
652 
653  // Give the user an opportunity to find the desired file.
654  String newPath = null;
656  JOptionPane.showConfirmDialog(null,
657  NbBundle.getMessage(this.getClass(), "HashDbManager.dlgMsg.dbNotFoundAtLoc",
658  hashSetName, configuredPath),
659  NbBundle.getMessage(this.getClass(), "HashDbManager.dlgTitle.MissingDb"),
660  JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
661  newPath = searchForFile();
662  if (null != newPath && !newPath.isEmpty()) {
663  database = new File(newPath);
664  if (!database.exists()) {
665  newPath = null;
666  }
667  }
668  }
669  return newPath;
670  }
671 
672  private String searchForFile() {
673  String filePath = null;
674  JFileChooser fc = new JFileChooser();
675  fc.setDragEnabled(false);
676  fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
677  String[] EXTENSION = new String[]{"txt", "idx", "hash", "Hash", "kdb"}; //NON-NLS
678  FileNameExtensionFilter filter = new FileNameExtensionFilter(
679  NbBundle.getMessage(this.getClass(), "HashDbManager.fileNameExtensionFilter.title"), EXTENSION);
680  fc.setFileFilter(filter);
681  fc.setMultiSelectionEnabled(false);
682  if (fc.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
683  File f = fc.getSelectedFile();
684  try {
685  filePath = f.getCanonicalPath();
686  } catch (IOException ex) {
687  Logger.getLogger(HashDbManager.class.getName()).log(Level.WARNING, "Couldn't get selected file path", ex); //NON-NLS
688  }
689  }
690  return filePath;
691  }
692 
693  public static abstract class HashDb {
694 
699  public enum KnownFilesType {
700 
701  KNOWN(NbBundle.getMessage(HashDbManager.class, "HashDbManager.known.text")),
702  KNOWN_BAD(NbBundle.getMessage(HashDbManager.class, "HashDbManager.knownBad.text"));
703  private final String displayName;
704 
705  private KnownFilesType(String displayName) {
706  this.displayName = displayName;
707  }
708 
709  public String getDisplayName() {
710  return this.displayName;
711  }
712  }
713 
717  public enum Event {
718 
719  INDEXING_DONE
720  }
721 
722  public abstract String getHashSetName();
723 
724  abstract String getDisplayName();
725 
726  public abstract String getDatabasePath() throws TskCoreException;
727 
728  public abstract HashDb.KnownFilesType getKnownFilesType();
729 
730  public abstract boolean getSearchDuringIngest();
731 
732  abstract void setSearchDuringIngest(boolean useForIngest);
733 
734  public abstract boolean getSendIngestMessages();
735 
736  abstract void setSendIngestMessages(boolean showInboxMessages);
737 
745  public abstract boolean isUpdateable() throws TskCoreException;
746 
755  public abstract void addHashes(Content content) throws TskCoreException;
756 
757  public abstract void addHashes(Content content, String comment) throws TskCoreException;
758 
759  public abstract void addHashes(List<HashEntry> hashes) throws TskCoreException;
760 
761  public abstract boolean lookupMD5Quick(Content content) throws TskCoreException;
762 
763  public abstract HashHitInfo lookupMD5(Content content) throws TskCoreException;
764 
771  abstract boolean isValid() throws TskCoreException;
772 
773  public abstract String getIndexPath() throws TskCoreException;
774 
775  public abstract boolean hasIndexOnly() throws TskCoreException;
776 
777  public abstract void firePropertyChange(String propertyName, Object oldValue, Object newValue);
778 
779  public abstract void addPropertyChangeListener(PropertyChangeListener pcl);
780 
781  public abstract void removePropertyChangeListener(PropertyChangeListener pcl);
782 
783  @Override
784  public abstract String toString();
785 
786  }
787 
792  class SleuthkitHashSet extends HashDb{
793 
794  private static final long serialVersionUID = 1L;
795  private final int handle;
796  private final String hashSetName;
797  private boolean searchDuringIngest;
798  private boolean sendIngestMessages;
799  private final HashDb.KnownFilesType knownFilesType;
800  private boolean indexing;
801  private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
802 
803  private SleuthkitHashSet(int handle, String hashSetName, boolean useForIngest, boolean sendHitMessages, KnownFilesType knownFilesType) {
804  this.handle = handle;
805  this.hashSetName = hashSetName;
806  this.searchDuringIngest = useForIngest;
807  this.sendIngestMessages = sendHitMessages;
808  this.knownFilesType = knownFilesType;
809  this.indexing = false;
810  }
811 
818  @Override
819  public void addPropertyChangeListener(PropertyChangeListener pcl) {
820  propertyChangeSupport.addPropertyChangeListener(pcl);
821  }
822 
828  @Override
829  public void removePropertyChangeListener(PropertyChangeListener pcl) {
830  propertyChangeSupport.removePropertyChangeListener(pcl);
831  }
832 
833  int getHandle(){
834  return handle;
835  }
836 
837  @Override
838  public String getHashSetName() {
839  return hashSetName;
840  }
841 
842  @Override
843  String getDisplayName(){
844  return getHashSetName();
845  }
846 
847  @Override
848  public String getDatabasePath() throws TskCoreException {
849  return SleuthkitJNI.getHashDatabasePath(handle);
850  }
851 
852  public void setIndexing(boolean indexing){
853  this.indexing = indexing;
854  }
855 
856  @Override
857  public String getIndexPath() throws TskCoreException {
858  return SleuthkitJNI.getHashDatabaseIndexPath(handle);
859  }
860 
861  @Override
862  public KnownFilesType getKnownFilesType() {
863  return knownFilesType;
864  }
865 
866  @Override
867  public boolean getSearchDuringIngest() {
868  return searchDuringIngest;
869  }
870 
871  @Override
872  void setSearchDuringIngest(boolean useForIngest) {
873  this.searchDuringIngest = useForIngest;
874  }
875 
876  @Override
877  public boolean getSendIngestMessages() {
878  return sendIngestMessages;
879  }
880 
881  @Override
882  void setSendIngestMessages(boolean showInboxMessages) {
883  this.sendIngestMessages = showInboxMessages;
884  }
885 
893  @Override
894  public boolean isUpdateable() throws TskCoreException {
895  return SleuthkitJNI.isUpdateableHashDatabase(this.handle);
896  }
897 
906  @Override
907  public void addHashes(Content content) throws TskCoreException {
908  addHashes(content, null);
909  }
910 
921  @Override
922  public void addHashes(Content content, String comment) throws TskCoreException {
923  // This only works for AbstractFiles and MD5 hashes at present.
924  assert content instanceof AbstractFile;
925  if (content instanceof AbstractFile) {
926  AbstractFile file = (AbstractFile) content;
927  if (null != file.getMd5Hash()) {
928  SleuthkitJNI.addToHashDatabase(null, file.getMd5Hash(), null, null, comment, handle);
929  }
930  }
931  }
932 
940  @Override
941  public void addHashes(List<HashEntry> hashes) throws TskCoreException {
942  SleuthkitJNI.addToHashDatabase(hashes, handle);
943  }
944 
954  @Override
955  public boolean lookupMD5Quick(Content content) throws TskCoreException {
956  boolean result = false;
957  assert content instanceof AbstractFile;
958  if (content instanceof AbstractFile) {
959  AbstractFile file = (AbstractFile) content;
960  if (null != file.getMd5Hash()) {
961  result = SleuthkitJNI.lookupInHashDatabase(file.getMd5Hash(), handle);
962  }
963  }
964  return result;
965  }
966 
976  @Override
977  public HashHitInfo lookupMD5(Content content) throws TskCoreException {
978  HashHitInfo result = null;
979  // This only works for AbstractFiles and MD5 hashes at present.
980  assert content instanceof AbstractFile;
981  if (content instanceof AbstractFile) {
982  AbstractFile file = (AbstractFile) content;
983  if (null != file.getMd5Hash()) {
984  result = SleuthkitJNI.lookupInHashDatabaseVerbose(file.getMd5Hash(), handle);
985  }
986  }
987  return result;
988  }
989 
996  @Override
997  boolean isValid() throws TskCoreException {
998  return hasIndex();
999  }
1000 
1001  boolean hasIndex() throws TskCoreException {
1002  return SleuthkitJNI.hashDatabaseHasLookupIndex(handle);
1003  }
1004 
1005  @Override
1006  public boolean hasIndexOnly() throws TskCoreException {
1007  return SleuthkitJNI.hashDatabaseIsIndexOnly(handle);
1008  }
1009 
1010  boolean canBeReIndexed() throws TskCoreException {
1011  return SleuthkitJNI.hashDatabaseCanBeReindexed(handle);
1012  }
1013 
1014  boolean isIndexing() {
1015  return indexing;
1016  }
1017 
1018  @Override
1019  public void firePropertyChange(String propertyName, Object oldValue, Object newValue){
1020  this.propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
1021  }
1022 
1023  private void close() throws TskCoreException {
1024  SleuthkitJNI.closeHashDatabase(handle);
1025  }
1026 
1027  @Override
1028  public String toString(){
1029  return getHashSetName();
1030  }
1031 
1032 
1033  @Override
1034  public int hashCode() {
1035  int code = 23;
1036  code = 47 * code + Integer.hashCode(handle);
1037  code = 47 * code + Objects.hashCode(this.hashSetName);
1038  code = 47 * code + Objects.hashCode(this.propertyChangeSupport);
1039  code = 47 * code + Objects.hashCode(this.knownFilesType);
1040  return code;
1041  }
1042 
1043  @Override
1044  public boolean equals(Object obj) {
1045  if (obj == null) {
1046  return false;
1047  }
1048  if (getClass() != obj.getClass()) {
1049  return false;
1050  }
1051  final SleuthkitHashSet other = (SleuthkitHashSet) obj;
1052  if (!Objects.equals(this.hashSetName, other.hashSetName)) {
1053  return false;
1054  }
1055  if (this.knownFilesType != other.knownFilesType) {
1056  return false;
1057  }
1058  return true;
1059  }
1060  }
1061 
1066  class CentralRepoHashSet extends HashDb{
1067 
1068  private static final long serialVersionUID = 1L;
1069  private final String hashSetName;
1070  private boolean searchDuringIngest;
1071  private boolean sendIngestMessages;
1072  private final HashDb.KnownFilesType knownFilesType;
1073  private final int referenceSetID;
1074  private final String version;
1075  private String orgName;
1076  private final boolean readOnly;
1077  private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
1078 
1079  @Messages({"HashDbManager.CentralRepoHashDb.orgError=Error loading organization"})
1080  private CentralRepoHashSet(String hashSetName, String version, int referenceSetID,
1081  boolean useForIngest, boolean sendHitMessages, HashDb.KnownFilesType knownFilesType,
1082  boolean readOnly)
1083  throws TskCoreException{
1084  this.hashSetName = hashSetName;
1085  this.version = version;
1086  this.referenceSetID = referenceSetID;
1087  this.searchDuringIngest = useForIngest;
1088  this.sendIngestMessages = sendHitMessages;
1089  this.knownFilesType = knownFilesType;
1090  this.readOnly = readOnly;
1091 
1092  try{
1093  orgName = EamDb.getInstance().getReferenceSetOrganization(referenceSetID).getName();
1094  } catch (EamDbException ex){
1095  Logger.getLogger(SleuthkitHashSet.class.getName()).log(Level.SEVERE, "Error looking up central repository organization for reference set " + referenceSetID, ex); //NON-NLS
1096  orgName = Bundle.HashDbManager_CentralRepoHashDb_orgError();
1097  }
1098  }
1099 
1106  @Override
1107  public void addPropertyChangeListener(PropertyChangeListener pcl) {
1108  propertyChangeSupport.addPropertyChangeListener(pcl);
1109  }
1110 
1116  @Override
1117  public void removePropertyChangeListener(PropertyChangeListener pcl) {
1118  propertyChangeSupport.removePropertyChangeListener(pcl);
1119  }
1120 
1121  @Override
1122  public boolean hasIndexOnly() throws TskCoreException{
1123  return true;
1124  }
1125 
1126  @Override
1127  public String getHashSetName() {
1128  return hashSetName;
1129  }
1130 
1131  @Override
1132  public String getDisplayName(){
1133  if(! getVersion().isEmpty()){
1134  return getHashSetName() + " " + getVersion() + " (remote)";
1135  } else {
1136  return getHashSetName() + " (remote)";
1137  }
1138  }
1139 
1140  String getVersion(){
1141  return version;
1142  }
1143 
1144  String getOrgName(){
1145  return orgName;
1146  }
1147 
1148  int getReferenceSetID(){
1149  return referenceSetID;
1150  }
1151 
1152  @Override
1153  public String getDatabasePath() throws TskCoreException {
1154  return "";
1155  }
1156 
1157  @Override
1158  public String getIndexPath() throws TskCoreException {
1159  return "";
1160  }
1161 
1162  @Override
1163  public HashDb.KnownFilesType getKnownFilesType() {
1164  return knownFilesType;
1165  }
1166 
1167  @Override
1168  public boolean getSearchDuringIngest() {
1169  return searchDuringIngest;
1170  }
1171 
1172  @Override
1173  void setSearchDuringIngest(boolean useForIngest) {
1174  this.searchDuringIngest = useForIngest;
1175  }
1176 
1177  @Override
1178  public boolean getSendIngestMessages() {
1179  return sendIngestMessages;
1180  }
1181 
1182  @Override
1183  void setSendIngestMessages(boolean showInboxMessages) {
1184  this.sendIngestMessages = showInboxMessages;
1185  }
1186 
1194  @Override
1195  public boolean isUpdateable() throws TskCoreException {
1196  return (! readOnly);
1197  }
1198 
1207  @Override
1208  public void addHashes(Content content) throws TskCoreException {
1209  addHashes(content, null);
1210  }
1211 
1222  @Override
1223  public void addHashes(Content content, String comment) throws TskCoreException {
1224  // This only works for AbstractFiles and MD5 hashes at present.
1225  assert content instanceof AbstractFile;
1226  if (content instanceof AbstractFile) {
1227  AbstractFile file = (AbstractFile) content;
1228  if (null != file.getMd5Hash()) {
1229  TskData.FileKnown type;
1230  if(knownFilesType.equals(HashDb.KnownFilesType.KNOWN_BAD)){
1231  type = TskData.FileKnown.BAD;
1232  } else {
1233  type = TskData.FileKnown.KNOWN;
1234  }
1235 
1236  try{
1237  EamGlobalFileInstance fileInstance = new EamGlobalFileInstance(referenceSetID, file.getMd5Hash(),
1238  type, comment);
1239  EamDb.getInstance().addReferenceInstance(fileInstance,EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.FILES_TYPE_ID));
1240  } catch (EamDbException ex){
1241  throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex);
1242  }
1243  }
1244  }
1245  }
1246 
1254  @Override
1255  public void addHashes(List<HashEntry> hashes) throws TskCoreException {
1256  Set<EamGlobalFileInstance> globalFileInstances = new HashSet<>();
1257  for(HashEntry hashEntry:hashes){
1258  TskData.FileKnown type;
1259  if(knownFilesType.equals(HashDb.KnownFilesType.KNOWN_BAD)){
1260  type = TskData.FileKnown.BAD;
1261  } else {
1262  type = TskData.FileKnown.KNOWN;
1263  }
1264  try {
1265  globalFileInstances.add(new EamGlobalFileInstance(referenceSetID, hashEntry.getMd5Hash(), type, hashEntry.getComment()));
1266  } catch (EamDbException ex){
1267  throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex);
1268  }
1269  }
1270 
1271  try{
1272  EamDb.getInstance().bulkInsertReferenceTypeEntries(globalFileInstances,
1273  EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.FILES_TYPE_ID));
1274  } catch (EamDbException ex){
1275  throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex);
1276  }
1277  }
1278 
1288  @Override
1289  public boolean lookupMD5Quick(Content content) throws TskCoreException {
1290  // This only works for AbstractFiles and MD5 hashes
1291  assert content instanceof AbstractFile;
1292  if (content instanceof AbstractFile) {
1293  AbstractFile file = (AbstractFile) content;
1294  if (null != file.getMd5Hash()) {
1295  try{
1296  return EamDb.getInstance().isFileHashInReferenceSet(file.getMd5Hash(), this.referenceSetID);
1297  } catch (EamDbException ex){
1298  Logger.getLogger(SleuthkitHashSet.class.getName()).log(Level.SEVERE, "Error performing central reposiotry hash lookup for hash "
1299  + file.getMd5Hash() + " in reference set " + referenceSetID, ex); //NON-NLS
1300  throw new TskCoreException("Error performing central reposiotry hash lookup", ex);
1301  }
1302  }
1303  }
1304  return false;
1305  }
1306 
1316  @Override
1317  public HashHitInfo lookupMD5(Content content) throws TskCoreException {
1318  HashHitInfo result = null;
1319  // This only works for AbstractFiles and MD5 hashes
1320  assert content instanceof AbstractFile;
1321  if (content instanceof AbstractFile) {
1322  AbstractFile file = (AbstractFile) content;
1323  if (null != file.getMd5Hash()) {
1324  try{
1325  if(EamDb.getInstance().isFileHashInReferenceSet(file.getMd5Hash(), this.referenceSetID)){
1326  // Make a bare-bones HashHitInfo for now
1327  result = new HashHitInfo(file.getMd5Hash(), "", "");
1328  }
1329  } catch (EamDbException ex){
1330  Logger.getLogger(SleuthkitHashSet.class.getName()).log(Level.SEVERE, "Error performing central reposiotry hash lookup for hash "
1331  + file.getMd5Hash() + " in reference set " + referenceSetID, ex); //NON-NLS
1332  throw new TskCoreException("Error performing central reposiotry hash lookup", ex);
1333  }
1334  }
1335  }
1336  return result;
1337  }
1338 
1344  @Override
1345  boolean isValid() {
1346  if(! EamDb.isEnabled()) {
1347  return false;
1348  }
1349  try{
1350  return EamDb.getInstance().referenceSetIsValid(this.referenceSetID, this.hashSetName, this.version);
1351  } catch (EamDbException ex){
1352  Logger.getLogger(CentralRepoHashSet.class.getName()).log(Level.SEVERE, "Error validating hash set " + hashSetName, ex); //NON-NLS
1353  return false;
1354  }
1355  }
1356 
1357  @Override
1358  public void firePropertyChange(String propertyName, Object oldValue, Object newValue){
1359  this.propertyChangeSupport.firePropertyChange(propertyName, oldValue, newValue);
1360  }
1361 
1362  @Override
1363  public String toString(){
1364  return getDisplayName();
1365  }
1366 
1367 
1368  @Override
1369  public int hashCode() {
1370  int code = 23;
1371  code = 47 * code + Objects.hashCode(this.hashSetName);
1372  code = 47 * code + Objects.hashCode(this.version);
1373  code = 47 * code + Integer.hashCode(this.referenceSetID);
1374  code = 47 * code + Objects.hashCode(this.knownFilesType);
1375  return code;
1376  }
1377 
1378  @Override
1379  public boolean equals(Object obj) {
1380  if (obj == null) {
1381  return false;
1382  }
1383  if (getClass() != obj.getClass()) {
1384  return false;
1385  }
1386  final CentralRepoHashSet other = (CentralRepoHashSet) obj;
1387  if (!Objects.equals(this.hashSetName, other.hashSetName)) {
1388  return false;
1389  }
1390  if (!Objects.equals(this.version, other.version)) {
1391  return false;
1392  }
1393  if (this.knownFilesType != other.knownFilesType) {
1394  return false;
1395  }
1396  return true;
1397  }
1398  }
1399 
1403  private class HashDbIndexer extends SwingWorker<Object, Void> {
1404 
1405  private ProgressHandle progress = null;
1406  private SleuthkitHashSet hashDb = null;
1407 
1408  HashDbIndexer(SleuthkitHashSet hashDb) {
1409  this.hashDb = hashDb;
1410  }
1411 
1412  @Override
1413  protected Object doInBackground() {
1414  hashDb.setIndexing(true);
1415  progress = ProgressHandle.createHandle(
1416  NbBundle.getMessage(this.getClass(), "HashDbManager.progress.indexingHashSet", hashDb.getHashSetName()));
1417  progress.start();
1418  progress.switchToIndeterminate();
1419  try {
1420  SleuthkitJNI.createLookupIndexForHashDatabase(hashDb.getHandle());
1421  } catch (TskCoreException ex) {
1422  Logger.getLogger(HashDbIndexer.class.getName()).log(Level.SEVERE, "Error indexing hash set " + hashDb.getHashSetName(), ex); //NON-NLS
1423  JOptionPane.showMessageDialog(null,
1424  NbBundle.getMessage(this.getClass(),
1425  "HashDbManager.dlgMsg.errorIndexingHashSet",
1426  hashDb.getHashSetName()),
1427  NbBundle.getMessage(this.getClass(), "HashDbManager.hashDbIndexingErr"),
1428  JOptionPane.ERROR_MESSAGE);
1429  }
1430  return null;
1431  }
1432 
1433  @Override
1434  protected void done() {
1435  hashDb.setIndexing(false);
1436  progress.finish();
1437 
1438  // see if we got any errors
1439  try {
1440  get();
1441  } catch (InterruptedException | ExecutionException ex) {
1442  logger.log(Level.SEVERE, "Error creating index", ex); //NON-NLS
1444  NbBundle.getMessage(this.getClass(), "HashDbManager.errCreatingIndex.title"),
1445  NbBundle.getMessage(this.getClass(), "HashDbManager.errCreatingIndex.msg", ex.getMessage()),
1447  } // catch and ignore if we were cancelled
1448  catch (java.util.concurrent.CancellationException ex) {
1449  }
1450 
1451  try {
1452  hashDb.firePropertyChange(SleuthkitHashSet.Event.INDEXING_DONE.toString(), null, hashDb);
1453  hashDb.firePropertyChange(HashDbManager.SetEvt.DB_INDEXED.toString(), null, hashDb.getHashSetName());
1454  } catch (Exception e) {
1455  logger.log(Level.SEVERE, "HashDbManager listener threw exception", e); //NON-NLS
1457  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErr"),
1458  NbBundle.getMessage(this.getClass(), "HashDbManager.moduleErrorListeningToUpdatesMsg"),
1460  }
1461  }
1462  }
1463 }
CorrelationAttribute.Type getCorrelationTypeById(int typeId)
static synchronized IngestManager getInstance()
synchronized void addPropertyChangeListener(PropertyChangeListener listener)
List< HashDb > getUpdateableHashSets(List< HashDb > hashDbs)
synchronized HashDb addNewHashDatabaseNoSave(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType)
abstract void firePropertyChange(String propertyName, Object oldValue, Object newValue)
synchronized void removeHashDatabaseNoSave(HashDb hashDb)
String getValidFilePath(String hashSetName, String configuredPath)
static HashDb.KnownFilesType convertFileKnown(TskData.FileKnown fileKnown)
List< EamGlobalSet > getAllReferenceSets(CorrelationAttribute.Type correlationType)
synchronized HashDb addNewHashDatabase(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType)
synchronized void removePropertyChangeListener(PropertyChangeListener listener)
void closeHashDatabases(List< HashDb > hashDatabases)
abstract void addPropertyChangeListener(PropertyChangeListener pcl)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void show(String title, String message, MessageType type, ActionListener actionListener)
SleuthkitHashSet addHashDatabase(int handle, String hashSetName, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType)
synchronized HashDb addExistingHashDatabase(String hashSetName, String path, boolean searchDuringIngest, boolean sendIngestMessages, HashDb.KnownFilesType knownFilesType)
abstract void removePropertyChangeListener(PropertyChangeListener pcl)

Copyright © 2012-2016 Basis Technology. Generated on: Tue Feb 20 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.