Java tutorial
/* * Autopsy Forensic Browser * * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> 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.awt.Cursor; import java.awt.Frame; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Collection; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.UUID; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.apache.commons.io.FileUtils; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.CaseMetadata.CaseMetadataException; import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent; import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.CoreComponentControl; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.coreutils.NetworkUtils; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.core.UserPreferencesException; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskException; /** * Stores all information for a given case. Only a single case can currently be * open at a time. Use getCurrentCase() to retrieve the object for the current * case. */ public class Case implements SleuthkitCase.ErrorObserver { private static final String autopsyVer = Version.getVersion(); // current version of autopsy. Change it when the version is changed private static final String EVENT_CHANNEL_NAME = "%s-Case-Events"; private static String appName = null; volatile private IntervalErrorReportData tskErrorReporter = null; private static final int MIN_SECONDS_BETWEEN_ERROR_REPORTS = 60; // No less than 60 seconds between warnings for errors private static final int MAX_SANITIZED_NAME_LENGTH = 47; /** * Name for the property that determines whether to show the dialog at * startup */ public static final String propStartup = "LBL_StartupDialog"; //NON-NLS /** * The event publisher is static so that subscribers only have to subscribe * once to receive events for all cases. */ private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); /** * Events that the case module will fire. Event listeners can get the event * name by using String returned by toString() method on a specific event. */ public enum Events { /** * Property name that indicates the name of the current case has * changed. The old value is the old case name, the new value is the new * case name. */ NAME, /** * Property name that indicates the number of the current case has * changed. Fired with the case number is changed. The value is an int: * the number of the case. -1 is used for no case number set. */ NUMBER, /** * Property name that indicates the examiner of the current case has * changed. Fired with the case examiner is changed. The value is a * String: the name of the examiner. The empty string ("") is used for * no examiner set. */ EXAMINER, /** * Property name used for a property change event that indicates a new * data source (image, local/logical file or local disk) is being added * to the current case. The old and new values of the * PropertyChangeEvent are null - cast the PropertyChangeEvent to * org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent to * access event data. */ ADDING_DATA_SOURCE, /** * Property name used for a property change event that indicates a * failure adding a new data source (image, local/logical file or local * disk) to the current case. The old and new values of the * PropertyChangeEvent are null - cast the PropertyChangeEvent to * org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent * to access event data. */ ADDING_DATA_SOURCE_FAILED, /** * Property name that indicates a new data source (image, disk or local * file) has been added to the current case. The new value is the * newly-added instance of the new data source, and the old value is * always null. */ DATA_SOURCE_ADDED, /** * Property name that indicates a data source has been removed from the * current case. The "old value" is the (int) content ID of the data * source that was removed, the new value is the instance of the data * source. */ DATA_SOURCE_DELETED, /** * Property name that indicates the currently open case has changed. * When a case is opened, the "new value" will be an instance of the * opened Case object and the "old value" will be null. When a case is * closed, the "new value" will be null and the "old value" will be the * instance of the Case object being closed. */ CURRENT_CASE, /** * Name for property change events fired when a report is added to the * case. The old value supplied by the event object is null and the new * value is a reference to a Report object representing the new report. */ REPORT_ADDED, /** * Name for the property change event when a report is deleted from the * case. Both the old value and the new value supplied by the event * object are null. */ REPORT_DELETED, /** * Property name for the event when a new BlackBoardArtifactTag is * added. The new value is tag added, the old value is empty */ BLACKBOARD_ARTIFACT_TAG_ADDED, /** * Property name for the event when a new BlackBoardArtifactTag is * deleted. The new value is empty, the old value is a * {@link BlackBoardArtifactTagDeletedEvent.DeletedBlackboardArtifactTagInfo} * object with info about the deleted tag. */ BLACKBOARD_ARTIFACT_TAG_DELETED, /** * Property name for the event when a new ContentTag is added. The new * value is tag added, the old value is empty */ CONTENT_TAG_ADDED, /** * Property name for the event when a new ContentTag is deleted. The new * value is empty, the old value is a * {@link ContentTagDeletedEvent.DeletedContentTagInfo} object with info * about the deleted tag. */ CONTENT_TAG_DELETED; }; /** * This enum describes the type of case, either single-user (standalone) or * multi-user (using PostgreSql) */ public enum CaseType { SINGLE_USER_CASE("Single-user case"), MULTI_USER_CASE("Multi-user case"); private final String caseType; private CaseType(String s) { caseType = s; } public boolean equalsName(String otherType) { return (otherType == null) ? false : caseType.equals(otherType); } public static CaseType fromString(String typeName) { if (typeName != null) { for (CaseType c : CaseType.values()) { if (typeName.equalsIgnoreCase(c.caseType)) { return c; } } } return null; } @Override public String toString() { return caseType; } }; private String name; private String number; private String examiner; private String configFilePath; private final XMLCaseManagement xmlcm; private final SleuthkitCase db; // Track the current case (only set with changeCase() method) private static Case currentCase = null; private final CaseType caseType; private final Services services; private static final Logger logger = Logger.getLogger(Case.class.getName()); static final String CASE_EXTENSION = "aut"; //NON-NLS static final String CASE_DOT_EXTENSION = "." + CASE_EXTENSION; private final static String CACHE_FOLDER = "Cache"; //NON-NLS private final static String EXPORT_FOLDER = "Export"; //NON-NLS private final static String LOG_FOLDER = "Log"; //NON-NLS final static String MODULE_FOLDER = "ModuleOutput"; //NON-NLS private final static String REPORTS_FOLDER = "Reports"; //NON-NLS private final static String TEMP_FOLDER = "Temp"; //NON-NLS // we cache if the case has data in it yet since a few places ask for it and we dont' need to keep going to DB private boolean hasData = false; private CollaborationMonitor collaborationMonitor; /** * Constructor for the Case class */ private Case(String name, String number, String examiner, String configFilePath, XMLCaseManagement xmlcm, SleuthkitCase db, CaseType type) { this.name = name; this.number = number; this.examiner = examiner; this.configFilePath = configFilePath; this.xmlcm = xmlcm; this.caseType = type; this.db = db; this.services = new Services(db); } /** * Gets the currently opened case, if there is one. * * @return the current open case * * @throws IllegalStateException if there is no case open. */ public static Case getCurrentCase() { if (currentCase != null) { return currentCase; } else { throw new IllegalStateException(NbBundle.getMessage(Case.class, "Case.getCurCase.exception.noneOpen")); } } /** * Check if case is currently open * * @return true if case is open */ public static boolean isCaseOpen() { return currentCase != null; } /** * Updates the current case to the given case and fires off the appropriate * property-change * * @param newCase the new current case or null if case is being closed * */ private static void changeCase(Case newCase) { // close the existing case Case oldCase = Case.currentCase; Case.currentCase = null; if (oldCase != null) { SwingUtilities.invokeLater(() -> { WindowManager.getDefault().getMainWindow() .setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); }); IngestManager.getInstance().cancelAllIngestJobs(); doCaseChange(null); //closes windows, etc if (null != oldCase.tskErrorReporter) { oldCase.tskErrorReporter.shutdown(); // stop listening for TSK errors for the old case oldCase.tskErrorReporter = null; } eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), oldCase, null)); if (CaseType.MULTI_USER_CASE == oldCase.getCaseType()) { if (null != oldCase.collaborationMonitor) { oldCase.collaborationMonitor.shutdown(); } eventPublisher.closeRemoteEventChannel(); } } if (newCase != null) { currentCase = newCase; Logger.setLogDirectory(currentCase.getLogDirectoryPath()); // sanity check if (null != currentCase.tskErrorReporter) { currentCase.tskErrorReporter.shutdown(); } // start listening for TSK errors for the new case currentCase.tskErrorReporter = new IntervalErrorReportData(currentCase, MIN_SECONDS_BETWEEN_ERROR_REPORTS, NbBundle.getMessage(Case.class, "IntervalErrorReport.ErrorText")); doCaseChange(currentCase); SwingUtilities.invokeLater(() -> { RecentCases.getInstance().addRecentCase(currentCase.name, currentCase.configFilePath); // update the recent cases }); if (CaseType.MULTI_USER_CASE == newCase.getCaseType()) { try { /** * Use the text index name as the remote event channel name * prefix since it is unique, the same as the case database * name for a multiuser case, and is readily available * through the Case.getTextIndexName() API. */ eventPublisher .openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, newCase.getTextIndexName())); currentCase.collaborationMonitor = new CollaborationMonitor(); } catch (AutopsyEventException | CollaborationMonitor.CollaborationMonitorException ex) { logger.log(Level.SEVERE, "Failed to setup for collaboration", ex); MessageNotifyUtil.Notify.error( NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.Title"), NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.ErrMsg")); } } eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), null, currentCase)); } else { Logger.setLogDirectory(PlatformUtil.getLogDirectory()); } SwingUtilities.invokeLater(() -> { WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); }); } @Override public void receiveError(String context, String errorMessage) { /* NOTE: We are accessing tskErrorReporter from two different threads. * This is ok as long as we only read the value of tskErrorReporter * because tskErrorReporter is declared as volatile. */ if (null != tskErrorReporter) { tskErrorReporter.addProblems(context, errorMessage); } } AddImageProcess makeAddImageProcess(String timezone, boolean processUnallocSpace, boolean noFatOrphans) { return this.db.makeAddImageProcess(timezone, processUnallocSpace, noFatOrphans); } /** * Creates a new case (create the XML config file and database). Overload * for API consistency, defaults to a single-user case. * * @param caseDir The directory to store case data in. Will be created if * it doesn't already exist. If it exists, it should have * all of the needed sub dirs that createCaseDirectory() * will create. * @param caseName the name of case * @param caseNumber the case number * @param examiner the examiner for this case * * @throws org.sleuthkit.autopsy.casemodule.CaseActionException */ public static void create(String caseDir, String caseName, String caseNumber, String examiner) throws CaseActionException { create(caseDir, caseName, caseNumber, examiner, CaseType.SINGLE_USER_CASE); } /** * Creates a new case (create the XML config file and database) * * @param caseDir The directory to store case data in. Will be created if * it doesn't already exist. If it exists, it should have * all of the needed sub dirs that createCaseDirectory() * will create. * @param caseName the name of case * @param caseNumber the case number * @param examiner the examiner for this case * @param caseType the type of case, single-user or multi-user */ public static void create(String caseDir, String caseName, String caseNumber, String examiner, CaseType caseType) throws CaseActionException { logger.log(Level.INFO, "Creating new case.\ncaseDir: {0}\ncaseName: {1}", new Object[] { caseDir, caseName }); //NON-NLS // create case directory if it doesn't already exist. if (new File(caseDir).exists() == false) { Case.createCaseDirectory(caseDir, caseType); } String configFilePath = caseDir + File.separator + caseName + CASE_DOT_EXTENSION; XMLCaseManagement xmlcm = new XMLCaseManagement(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); Date date = new Date(); String santizedCaseName = sanitizeCaseName(caseName); String indexName = santizedCaseName + "_" + dateFormat.format(date); String dbName = null; // figure out the database name and index name for text extraction if (caseType == CaseType.SINGLE_USER_CASE) { dbName = caseDir + File.separator + "autopsy.db"; //NON-NLS } else if (caseType == CaseType.MULTI_USER_CASE) { dbName = indexName; } xmlcm.create(caseDir, caseName, examiner, caseNumber, caseType, dbName, indexName); // create a new XML config file xmlcm.writeFile(); SleuthkitCase db = null; try { if (caseType == CaseType.SINGLE_USER_CASE) { db = SleuthkitCase.newCase(dbName); } else if (caseType == CaseType.MULTI_USER_CASE) { db = SleuthkitCase.newCase(dbName, UserPreferences.getDatabaseConnectionInfo(), caseDir); } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error creating a case: " + caseName + " in dir " + caseDir + " " + ex.getMessage(), ex); //NON-NLS SwingUtilities.invokeLater(() -> { WindowManager.getDefault().getMainWindow() .setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); }); throw new CaseActionException(ex.getMessage(), ex); //NON-NLS } catch (UserPreferencesException ex) { logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); } /** * Two-stage initialization to avoid leaking reference to "this" in * constructor. */ Case newCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db, caseType); changeCase(newCase); } /** * Sanitize the case name for PostgreSQL database, Solr cores, and ActiveMQ * topics. Makes it plain-vanilla enough that each item should be able to * use it. * * Sanitize the PostgreSQL/Solr core, and ActiveMQ name by excluding: * Control characters Non-ASCII characters Various others shown below * * Solr: * http://stackoverflow.com/questions/29977519/what-makes-an-invalid-core-name * may not be / \ : * * ActiveMQ: * http://activemq.2283324.n4.nabble.com/What-are-limitations-restrictions-on-destination-name-td4664141.html * may not be ? * * PostgreSQL: * http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html 63 * chars max, must start with a-z or _ following chars can be letters _ or * digits * * SQLite: Uses autopsy.db for the database name follows Windows naming * convention * * @param caseName The name of the case as typed in by the user * * @return the sanitized case name to use for Database, Solr, and ActiveMQ */ static String sanitizeCaseName(String caseName) { String result; // Remove all non-ASCII characters result = caseName.replaceAll("[^\\p{ASCII}]", "_"); // Remove all control characters result = result.replaceAll("[\\p{Cntrl}]", "_"); // Remove / \ : ? space ' " result = result.replaceAll("[ /?:'\"\\\\]", "_"); // Make it all lowercase result = result.toLowerCase(); // Must start with letter or underscore for PostgreSQL. If not, prepend an underscore. if (result.length() > 0 && !(Character.isLetter(result.codePointAt(0))) && !(result.codePointAt(0) == '_')) { result = "_" + result; } // Chop to 63-16=47 left (63 max for PostgreSQL, taking 16 for the date _20151225_123456) if (result.length() > MAX_SANITIZED_NAME_LENGTH) { result = result.substring(0, MAX_SANITIZED_NAME_LENGTH); } if (result.isEmpty()) { result = "case"; } return result; } /** * Opens an existing case. * * @param caseMetadataFilePath The path of the case metadata file for the * case to be opened. * * @throws CaseActionException */ /** * TODO: Deprecate this and throw a more general exception. */ public static void open(String caseMetadataFilePath) throws CaseActionException { if (!caseMetadataFilePath.endsWith(CASE_DOT_EXTENSION)) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.open.exception.checkFile.msg", CASE_DOT_EXTENSION)); } logger.log(Level.INFO, "Opening case, case metadata file path: {0}", caseMetadataFilePath); //NON-NLS try { /** * Get the case metadata from the file. */ CaseMetadata metadata = new CaseMetadata(Paths.get(caseMetadataFilePath)); String caseName = metadata.getCaseName(); String caseNumber = metadata.getCaseNumber(); String examiner = metadata.getExaminer(); CaseType caseType = metadata.getCaseType(); String caseDir = metadata.getCaseDirectory(); /** * Open the case database. */ SleuthkitCase db; if (caseType == CaseType.SINGLE_USER_CASE) { String dbPath = Paths.get(caseDir, "autopsy.db").toString(); //NON-NLS db = SleuthkitCase.openCase(dbPath); } else { if (!UserPreferences.getIsMultiUserModeEnabled()) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled")); } try { db = SleuthkitCase.openCase(metadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseDir); } catch (UserPreferencesException ex) { logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); } } /** * Do things that require a UI. */ if (RuntimeProperties.coreComponentsAreActive()) { /** * If the case database was upgraded for a new schema, notify * the user. */ if (null != db.getBackupDatabasePath()) { SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog(null, NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.msg", db.getBackupDatabasePath()), NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.title"), JOptionPane.INFORMATION_MESSAGE); }); } /** * TODO: This currently has no value if it there is no user to * interact with a fid missing images dialog. */ checkImagesExist(db); } /** * Two-stage initialization to avoid leaking reference to "this" in * constructor. TODO: Remove use of obsolete XMLCaseManagement * class. */ XMLCaseManagement xmlcm = new XMLCaseManagement(); xmlcm.open(caseMetadataFilePath); Case openedCase = new Case(caseName, caseNumber, examiner, caseMetadataFilePath, xmlcm, db, caseType); changeCase(openedCase); } catch (CaseMetadataException ex) { /** * Clean-up the case if it was actually opened. TODO: Do this * better. */ try { Case badCase = Case.getCurrentCase(); badCase.closeCase(); } catch (IllegalStateException unused) { // Already logged. } throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.open.exception.gen.msg") + ": " + ex.getMessage(), ex); //NON-NLS } catch (TskCoreException ex) { try { Case badCase = Case.getCurrentCase(); badCase.closeCase(); } catch (CaseActionException | IllegalStateException unused) { // Already logged. } SwingUtilities.invokeLater(() -> { WindowManager.getDefault().getMainWindow() .setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); }); throw new CaseActionException(ex.getMessage(), ex); //NON-NLS } } static Map<Long, String> getImagePaths(SleuthkitCase db) { //TODO: clean this up Map<Long, String> imgPaths = new HashMap<>(); try { Map<Long, List<String>> imgPathsList = db.getImagePaths(); for (Map.Entry<Long, List<String>> entry : imgPathsList.entrySet()) { if (entry.getValue().size() > 0) { imgPaths.put(entry.getKey(), entry.getValue().get(0)); } } } catch (TskException ex) { logger.log(Level.WARNING, "Error getting image paths", ex); //NON-NLS } return imgPaths; } /** * Ensure that all image paths point to valid image files */ private static void checkImagesExist(SleuthkitCase db) { Map<Long, String> imgPaths = getImagePaths(db); for (Map.Entry<Long, String> entry : imgPaths.entrySet()) { long obj_id = entry.getKey(); String path = entry.getValue(); boolean fileExists = (pathExists(path) || driveExists(path)); if (!fileExists) { int ret = JOptionPane.showConfirmDialog(null, NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.msg", getAppName(), path), NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.title"), JOptionPane.YES_NO_OPTION); if (ret == JOptionPane.YES_OPTION) { MissingImageDialog.makeDialog(obj_id, db); } else { logger.log(Level.WARNING, "Selected image files don't match old files!"); //NON-NLS } } } } /** * Adds the image to the current case after it has been added to the DB. * Sends out event and reopens windows if needed. * * @param imgPaths the paths of the image that being added * @param imgId the ID of the image that being added * @param timeZone the timeZone of the image where it's added * * @deprecated As of release 4.0, replaced by {@link #notifyAddingDataSource(java.util.UUID) and * {@link #notifyDataSourceAdded(org.sleuthkit.datamodel.Content, java.util.UUID) and * {@link #notifyFailedAddingDataSource(java.util.UUID)} */ @Deprecated public Image addImage(String imgPath, long imgId, String timeZone) throws CaseActionException { try { Image newDataSource = db.getImageById(imgId); notifyDataSourceAdded(newDataSource, UUID.randomUUID()); return newDataSource; } catch (Exception ex) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.addImg.exception.msg"), ex); } } /** * Finishes adding new local data source to the case. Sends out event and * reopens windows if needed. * * @param newDataSource new data source added * * @deprecated As of release 4.0, replaced by {@link #notifyAddingDataSource(java.util.UUID) and * {@link #notifyDataSourceAdded(org.sleuthkit.datamodel.Content, java.util.UUID) and * {@link #notifyFailedAddingDataSource(java.util.UUID)} */ @Deprecated void addLocalDataSource(Content newDataSource) { notifyDataSourceAdded(newDataSource, UUID.randomUUID()); } /** * Notifies case event subscribers (property change listeners) that a data * source is being added to the case database. * * This should not be called from the event dispatch thread (EDT) * * @param dataSourceId A unique identifier for the data source. This UUID * should be used to call notifyNewDataSource() after * the data source is added. */ public void notifyAddingDataSource(UUID dataSourceId) { eventPublisher.publish(new AddingDataSourceEvent(dataSourceId)); } /** * Notifies case event subscribers (property change listeners) that a data * source failed to be added to the case database. * * This should not be called from the event dispatch thread (EDT) * * @param dataSourceId A unique identifier for the data source. */ public void notifyFailedAddingDataSource(UUID dataSourceId) { eventPublisher.publish(new AddingDataSourceFailedEvent(dataSourceId)); } /** * Notifies case event subscribers (property change listeners) that a data * source is being added to the case database. * * This should not be called from the event dispatch thread (EDT) * * @param newDataSource New data source added. * @param dataSourceId A unique identifier for the data source. Should be * the same UUID used to call * notifyAddingNewDataSource() when the process of * adding the data source began. */ public void notifyDataSourceAdded(Content newDataSource, UUID dataSourceId) { eventPublisher.publish(new DataSourceAddedEvent(newDataSource, dataSourceId)); } /** * Notifies the UI that a new ContentTag has been added. * * This should not be called from the event dispatch thread (EDT) * * @param newTag new ContentTag added */ public void notifyContentTagAdded(ContentTag newTag) { eventPublisher.publish(new ContentTagAddedEvent(newTag)); } /** * Notifies the UI that a ContentTag has been deleted. * * This should not be called from the event dispatch thread (EDT) * * @param deletedTag ContentTag deleted */ public void notifyContentTagDeleted(ContentTag deletedTag) { eventPublisher.publish(new ContentTagDeletedEvent(deletedTag)); } /** * Notifies the UI that a new BlackboardArtifactTag has been added. * * This should not be called from the event dispatch thread (EDT) * * @param newTag new BlackboardArtifactTag added */ public void notifyBlackBoardArtifactTagAdded(BlackboardArtifactTag newTag) { eventPublisher.publish(new BlackBoardArtifactTagAddedEvent(newTag)); } /** * Notifies the UI that a BlackboardArtifactTag has been deleted. * * This should not be called from the event dispatch thread (EDT) * * @param deletedTag BlackboardArtifactTag deleted */ public void notifyBlackBoardArtifactTagDeleted(BlackboardArtifactTag deletedTag) { eventPublisher.publish(new BlackBoardArtifactTagDeletedEvent(deletedTag)); } /** * @return The Services object for this case. */ public Services getServices() { return services; } /** * Get the underlying SleuthkitCase instance from the Sleuth Kit bindings * library. * * @return */ public SleuthkitCase getSleuthkitCase() { return this.db; } /** * Closes this case. This methods close the xml and clear all the fields. */ public void closeCase() throws CaseActionException { changeCase(null); try { services.close(); this.xmlcm.close(); // close the xmlcm this.db.close(); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.closeCase.exception.msg"), e); } } /** * Delete this case. This methods delete all folders and files of this case. * * @param caseDir case dir to delete * * @throws CaseActionException exception throw if case could not be deleted */ void deleteCase(File caseDir) throws CaseActionException { logger.log(Level.INFO, "Deleting case.\ncaseDir: {0}", caseDir); //NON-NLS try { xmlcm.close(); // close the xmlcm boolean result = deleteCaseDirectory(caseDir); // delete the directory RecentCases.getInstance().removeRecentCase(this.name, this.configFilePath); // remove it from the recent case Case.changeCase(null); if (result == false) { throw new CaseActionException( NbBundle.getMessage(this.getClass(), "Case.deleteCase.exception.msg", caseDir)); } } catch (Exception ex) { logger.log(Level.SEVERE, "Error deleting the current case dir: " + caseDir, ex); //NON-NLS throw new CaseActionException( NbBundle.getMessage(this.getClass(), "Case.deleteCase.exception.msg2", caseDir), ex); } } /** * Updates the case name. * * This should not be called from the EDT. * * @param oldCaseName the old case name that wants to be updated * @param oldPath the old path that wants to be updated * @param newCaseName the new case name * @param newPath the new path */ void updateCaseName(String oldCaseName, String oldPath, String newCaseName, String newPath) throws CaseActionException { try { xmlcm.setCaseName(newCaseName); // set the case name = newCaseName; // change the local value eventPublisher.publish(new AutopsyEvent(Events.NAME.toString(), oldCaseName, newCaseName)); SwingUtilities.invokeLater(() -> { try { RecentCases.getInstance().updateRecentCase(oldCaseName, oldPath, newCaseName, newPath); // update the recent case updateMainWindowTitle(newCaseName); } catch (Exception e) { Logger.getLogger(Case.class.getName()).log(Level.WARNING, "Error: problem updating case name.", e); //NON-NLS } }); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateCaseName.exception.msg"), e); } } /** * Updates the case examiner * * This should not be called from the EDT. * * @param oldExaminer the old examiner * @param newExaminer the new examiner */ void updateExaminer(String oldExaminer, String newExaminer) throws CaseActionException { try { xmlcm.setCaseExaminer(newExaminer); // set the examiner examiner = newExaminer; eventPublisher.publish(new AutopsyEvent(Events.EXAMINER.toString(), oldExaminer, newExaminer)); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateExaminer.exception.msg"), e); } } /** * Updates the case number * * This should not be called from the EDT. * * @param oldCaseNumber the old case number * @param newCaseNumber the new case number */ void updateCaseNumber(String oldCaseNumber, String newCaseNumber) throws CaseActionException { try { xmlcm.setCaseNumber(newCaseNumber); // set the case number number = newCaseNumber; eventPublisher.publish(new AutopsyEvent(Events.NUMBER.toString(), oldCaseNumber, newCaseNumber)); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateCaseNum.exception.msg"), e); } } /** * Checks whether there is a current case open. * * @return True if a case is open. */ public static boolean existsCurrentCase() { return currentCase != null; } /** * Uses the given path to store it as the configuration file path * * @param givenPath the given config file path */ private void setConfigFilePath(String givenPath) { configFilePath = givenPath; } /** * Get the config file path in the given path * * @return configFilePath the path of the configuration file */ String getConfigFilePath() { return configFilePath; } /** * Returns the current version of Autopsy * * @return autopsyVer */ public static String getAutopsyVersion() { return autopsyVer; } /** * Gets the application name * * @return appName */ public static String getAppName() { if ((appName == null) || appName.equals("")) { appName = WindowManager.getDefault().getMainWindow().getTitle(); } return appName; } /** * Gets the case name * * @return name */ public String getName() { return name; } /** * Gets the case number * * @return number */ public String getNumber() { return number; } /** * Gets the Examiner name * * @return examiner */ public String getExaminer() { return examiner; } /** * Gets the case directory path * * @return caseDirectoryPath */ public String getCaseDirectory() { if (xmlcm == null) { return ""; } else { return xmlcm.getCaseDirectory(); } } /** * Get the case type. * * @return */ public CaseType getCaseType() { return this.caseType; } /** * Gets the full path to the temp directory of this case. Will create it if * it does not already exist. * * @return tempDirectoryPath */ public String getTempDirectory() { return getDirectory(TEMP_FOLDER); } /** * Gets the full path to the cache directory of this case. Will create it if * it does not already exist. * * @return cacheDirectoryPath */ public String getCacheDirectory() { return getDirectory(CACHE_FOLDER); } /** * Gets the full path to the export directory of this case. Will create it * if it does not already exist. * * @return exportDirectoryPath */ public String getExportDirectory() { return getDirectory(EXPORT_FOLDER); } /** * Gets the full path to the log directory of this case. Will create it if * it does not already exist. * * @return logDirectoryPath */ public String getLogDirectoryPath() { return getDirectory(LOG_FOLDER); } /** * Get the reports directory path where modules should save their reports. * Will create it if it does not already exist. * * @return absolute path to the report output directory */ public String getReportDirectory() { return getDirectory(REPORTS_FOLDER); } /** * Get module output directory path where modules should save their * permanent data. * * @return absolute path to the module output directory */ public String getModuleDirectory() { return getDirectory(MODULE_FOLDER); } /** * Get the output directory path where modules should save their permanent * data. If single-user case, the directory is a subdirectory of the case * directory. If multi-user case, the directory is a subdirectory of * HostName, which is a subdirectory of the case directory. * * @return the path to the host output directory */ public String getOutputDirectory() { return getHostDirectory(); } /** * Get the specified directory path, create it if it does not already exist. * * @return absolute path to the directory */ private String getDirectory(String input) { File theDirectory = new File(getHostDirectory() + File.separator + input); if (!theDirectory.exists()) { // Create it if it doesn't exist already. theDirectory.mkdirs(); } return theDirectory.toString(); } /** * Get relative (with respect to case dir) module output directory path * where modules should save their permanent data. The directory is a * subdirectory of this case dir. * * @return relative path to the module output dir */ public String getModuleOutputDirectoryRelativePath() { Path thePath; if (getCaseType() == CaseType.MULTI_USER_CASE) { thePath = Paths.get(NetworkUtils.getLocalHostName(), MODULE_FOLDER); } else { thePath = Paths.get(MODULE_FOLDER); } // Do not autocreate this relative path. It will have already been // created when the case was made. return thePath.toString(); } /** * Get the host output directory path where modules should save their * permanent data. If single-user case, the directory is a subdirectory of * the case directory. If multi-user case, the directory is a subdirectory * of the hostName, which is a subdirectory of the case directory. * * @return the path to the host output directory */ private String getHostDirectory() { String caseDirectory = getCaseDirectory(); Path hostPath; if (caseType == CaseType.MULTI_USER_CASE) { hostPath = Paths.get(caseDirectory, NetworkUtils.getLocalHostName()); } else { hostPath = Paths.get(caseDirectory); } if (!hostPath.toFile().exists()) { hostPath.toFile().mkdirs(); } return hostPath.toString(); } /** * Get module output directory path where modules should save their * permanent data. * * @return absolute path to the module output directory * * @deprecated Use getModuleDirectory() instead. */ @Deprecated public String getModulesOutputDirAbsPath() { return getModuleDirectory(); } /** * Get relative (with respect to case dir) module output directory path * where modules should save their permanent data. The directory is a * subdirectory of this case dir. * * @return relative path to the module output dir * * @deprecated Use getModuleOutputDirectoryRelativePath() instead */ @Deprecated public static String getModulesOutputDirRelPath() { return "ModuleOutput"; //NON-NLS } /** * Gets a PropertyChangeSupport object. The PropertyChangeSupport object * returned is not used by instances of this class and does not have any * PropertyChangeListeners. * * @return A new PropertyChangeSupport object. * * @deprecated Do not use. */ @Deprecated public static PropertyChangeSupport getPropertyChangeSupport() { return new PropertyChangeSupport(Case.class); } /** * Get the data model Content objects in the root of this case's hierarchy. * * @return a list of the root objects * * @throws org.sleuthkit.datamodel.TskCoreException */ public List<Content> getDataSources() throws TskCoreException { List<Content> list = db.getRootObjects(); hasData = (list.size() > 0); return list; } /** * get the created date of this case * * @return case creation date */ public String getCreatedDate() { if (xmlcm == null) { return ""; } else { return xmlcm.getCreatedDate(); } } /** * Get the name of the index where extracted text is stored for the case. * * @return Index name. */ public String getTextIndexName() { if (xmlcm == null) { return ""; } else { return xmlcm.getTextIndexName(); } } /** * Gets the time zone(s) of the image(s) in this case. * * @return time zones the set of time zones */ public Set<TimeZone> getTimeZone() { Set<TimeZone> timezones = new HashSet<>(); try { for (Content c : getDataSources()) { final Content dataSource = c.getDataSource(); if ((dataSource != null) && (dataSource instanceof Image)) { Image image = (Image) dataSource; timezones.add(TimeZone.getTimeZone(image.getTimeZone())); } } } catch (TskCoreException ex) { logger.log(Level.INFO, "Error getting time zones", ex); //NON-NLS } return timezones; } /** * Adds a subscriber to all case events from this Autopsy node and other * Autopsy nodes. To subscribe to only specific events, use one of the * overloads of addEventSubscriber(). * * @param listener The subscriber to add. */ public static synchronized void addPropertyChangeListener(PropertyChangeListener listener) { addEventSubscriber(Stream.of(Events.values()).map(Events::toString).collect(Collectors.toSet()), listener); } /** * Removes a subscriber from all case events from this Autopsy node and * other Autopsy nodes. To remove a subscription to only specific events, * use one of the overloads of removeEventSubscriber(). * * @param listener The subscriber to add. */ public static synchronized void removePropertyChangeListener(PropertyChangeListener listener) { removeEventSubscriber(Stream.of(Events.values()).map(Events::toString).collect(Collectors.toSet()), listener); } /** * Adds a subscriber to events from this Autopsy node and other Autopsy * nodes. * * @param eventNames The events the subscriber is interested in. * @param subscriber The subscriber to add. */ public static void addEventSubscriber(Set<String> eventNames, PropertyChangeListener subscriber) { eventPublisher.addSubscriber(eventNames, subscriber); } /** * Adds a subscriber to events from this Autopsy node and other Autopsy * nodes. * * @param eventName The event the subscriber is interested in. * @param subscriber The subscriber to add. */ public static void addEventSubscriber(String eventName, PropertyChangeListener subscriber) { eventPublisher.addSubscriber(eventName, subscriber); } /** * Adds a subscriber to events from this Autopsy node and other Autopsy * nodes. * * @param eventName The event the subscriber is no longer interested in. * @param subscriber The subscriber to add. */ public static void removeEventSubscriber(String eventName, PropertyChangeListener subscriber) { eventPublisher.removeSubscriber(eventName, subscriber); } /** * Removes a subscriber to events from this Autopsy node and other Autopsy * nodes. * * @param eventNames The event the subscriber is no longer interested in. * @param subscriber The subscriber to add. */ public static void removeEventSubscriber(Set<String> eventNames, PropertyChangeListener subscriber) { eventPublisher.removeSubscriber(eventNames, subscriber); } /** * Check if image from the given image path exists. * * @param imgPath the image path * * @return isExist whether the path exists */ public static boolean pathExists(String imgPath) { return new File(imgPath).isFile(); } /** * Does the given string refer to a physical drive? */ private static final String pdisk = "\\\\.\\physicaldrive"; //NON-NLS private static final String dev = "/dev/"; //NON-NLS static boolean isPhysicalDrive(String path) { return path.toLowerCase().startsWith(pdisk) || path.toLowerCase().startsWith(dev); } /** * Does the given string refer to a local drive / partition? */ static boolean isPartition(String path) { return path.toLowerCase().startsWith("\\\\.\\") && path.toLowerCase().endsWith(":"); } /** * Does the given drive path exist? * * @param path to drive * * @return true if the drive exists, false otherwise */ static boolean driveExists(String path) { // Test the drive by reading the first byte and checking if it's -1 BufferedInputStream br = null; try { File tmp = new File(path); br = new BufferedInputStream(new FileInputStream(tmp)); int b = br.read(); return b != -1; } catch (Exception ex) { return false; } finally { try { if (br != null) { br.close(); } } catch (IOException ex) { } } } /** * Convert the Java timezone ID to the "formatted" string that can be * accepted by the C/C++ code. Example: "America/New_York" converted to * "EST5EDT", etc * * @param timezoneID * * @return */ public static String convertTimeZone(String timezoneID) { TimeZone zone = TimeZone.getTimeZone(timezoneID); int offset = zone.getRawOffset() / 1000; int hour = offset / 3600; int min = (offset % 3600) / 60; DateFormat dfm = new SimpleDateFormat("z"); dfm.setTimeZone(zone); boolean hasDaylight = zone.useDaylightTime(); String first = dfm.format(new GregorianCalendar(2010, 1, 1).getTime()).substring(0, 3); // make it only 3 letters code String second = dfm.format(new GregorianCalendar(2011, 6, 6).getTime()).substring(0, 3); // make it only 3 letters code int mid = hour * -1; String result = first + Integer.toString(mid); if (min != 0) { result = result + ":" + Integer.toString(min); } if (hasDaylight) { result = result + second; } return result; } /** * to create the case directory * * @param caseDir Path to the case directory (typically base + case name) * @param caseName the case name (used only for error messages) * * @throws CaseActionException throw if could not create the case dir * @Deprecated */ @Deprecated static void createCaseDirectory(String caseDir, String caseName) throws CaseActionException { createCaseDirectory(caseDir, CaseType.SINGLE_USER_CASE); } /** * Create the case directory and its needed subfolders. * * @param caseDir Path to the case directory (typically base + case name) * @param caseType The type of case, single-user or multi-user * * @throws CaseActionException throw if could not create the case dir */ static void createCaseDirectory(String caseDir, CaseType caseType) throws CaseActionException { File caseDirF = new File(caseDir); if (caseDirF.exists()) { if (caseDirF.isFile()) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existNotDir", caseDir)); } else if (!caseDirF.canRead() || !caseDirF.canWrite()) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existCantRW", caseDir)); } } try { boolean result = (caseDirF).mkdirs(); // create root case Directory if (result == false) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreate", caseDir)); } // create the folders inside the case directory String hostClause = ""; if (caseType == CaseType.MULTI_USER_CASE) { hostClause = File.separator + NetworkUtils.getLocalHostName(); } result = result && (new File(caseDir + hostClause + File.separator + EXPORT_FOLDER)).mkdirs() && (new File(caseDir + hostClause + File.separator + LOG_FOLDER)).mkdirs() && (new File(caseDir + hostClause + File.separator + TEMP_FOLDER)).mkdirs() && (new File(caseDir + hostClause + File.separator + CACHE_FOLDER)).mkdirs(); if (result == false) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", caseDir)); } final String modulesOutDir = caseDir + hostClause + File.separator + MODULE_FOLDER; result = new File(modulesOutDir).mkdir(); if (result == false) { throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateModDir", modulesOutDir)); } final String reportsOutDir = caseDir + hostClause + File.separator + REPORTS_FOLDER; result = new File(reportsOutDir).mkdir(); if (result == false) { throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateReportsDir", modulesOutDir)); } } catch (Exception e) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.gen", caseDir), e); } } /** * delete the given case directory * * @param casePath the case path * * @return boolean whether the case directory is successfully deleted or not */ static boolean deleteCaseDirectory(File casePath) { logger.log(Level.INFO, "Deleting case directory: {0}", casePath.getAbsolutePath()); //NON-NLS return FileUtil.deleteDir(casePath); } /** * Invoke the creation of startup dialog window. */ static public void invokeStartupDialog() { StartupWindowProvider.getInstance().open(); } /** * Checks if a String is a valid case name * * @param caseName the candidate String * * @return true if the candidate String is a valid case name */ static public boolean isValidName(String caseName) { return !(caseName.contains("\\") || caseName.contains("/") || caseName.contains(":") || caseName.contains("*") || caseName.contains("?") || caseName.contains("\"") || caseName.contains("<") || caseName.contains(">") || caseName.contains("|")); } static private void clearTempFolder() { File tempFolder = new File(currentCase.getTempDirectory()); if (tempFolder.isDirectory()) { File[] files = tempFolder.listFiles(); if (files.length > 0) { for (File file : files) { if (file.isDirectory()) { deleteCaseDirectory(file); } else { file.delete(); } } } } } /** * Check for existence of certain case sub dirs and create them if needed. * * @param openedCase */ private static void checkSubFolders(Case openedCase) { String modulesOutputDir = openedCase.getModuleDirectory(); File modulesOutputDirF = new File(modulesOutputDir); if (!modulesOutputDirF.exists()) { logger.log(Level.INFO, "Creating modules output dir for the case."); //NON-NLS try { if (!modulesOutputDirF.mkdir()) { logger.log(Level.SEVERE, "Error creating modules output dir for the case, dir: {0}", modulesOutputDir); //NON-NLS } } catch (SecurityException e) { logger.log(Level.SEVERE, "Error creating modules output dir for the case, dir: " + modulesOutputDir, e); //NON-NLS } } } //case change helper private static void doCaseChange(Case toChangeTo) { logger.log(Level.INFO, "Changing Case to: {0}", toChangeTo); //NON-NLS if (toChangeTo != null) { // new case is open // clear the temp folder when the case is created / opened Case.clearTempFolder(); checkSubFolders(toChangeTo); if (RuntimeProperties.coreComponentsAreActive()) { // enable these menus SwingUtilities.invokeLater(() -> { 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 (toChangeTo.hasData()) { // open all top components SwingUtilities.invokeLater(() -> { CoreComponentControl.openCoreWindows(); }); } else { // close all top components SwingUtilities.invokeLater(() -> { CoreComponentControl.closeCoreWindows(); }); } } if (RuntimeProperties.coreComponentsAreActive()) { SwingUtilities.invokeLater(() -> { updateMainWindowTitle(currentCase.name); }); } else { SwingUtilities.invokeLater(() -> { Frame f = WindowManager.getDefault().getMainWindow(); f.setTitle(Case.getAppName()); // set the window name to just application name }); } } else { // case is closed if (RuntimeProperties.coreComponentsAreActive()) { SwingUtilities.invokeLater(() -> { // close all top components first CoreComponentControl.closeCoreWindows(); // 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 }); } //clear pending notifications SwingUtilities.invokeLater(() -> { MessageNotifyUtil.Notify.clear(); }); SwingUtilities.invokeLater(() -> { Frame f = WindowManager.getDefault().getMainWindow(); f.setTitle(Case.getAppName()); // set the window name to just application name }); //try to force gc to happen System.gc(); System.gc(); } //log memory usage after case changed logger.log(Level.INFO, PlatformUtil.getAllMemUsageInfo()); } //case name change helper private static void updateMainWindowTitle(String newCaseName) { // update case name if (!newCaseName.equals("")) { Frame f = WindowManager.getDefault().getMainWindow(); f.setTitle(newCaseName + " - " + Case.getAppName()); // set the window name to the new value } } /** * Adds a report to the case. * * @param localPath The path of the report file, must be in the case * directory or one of its subdirectories. * @param srcModuleName The name of the module that created the report. * @param reportName The report name, may be empty. * * @throws TskCoreException */ public void addReport(String localPath, String srcModuleName, String reportName) throws TskCoreException { String normalizedLocalPath; try { normalizedLocalPath = Paths.get(localPath).normalize().toString(); } catch (InvalidPathException ex) { String errorMsg = "Invalid local path provided: " + localPath; // NON-NLS throw new TskCoreException(errorMsg, ex); } Report report = this.db.addReport(normalizedLocalPath, srcModuleName, reportName); eventPublisher.publish(new ReportAddedEvent(report)); } public List<Report> getAllReports() throws TskCoreException { return this.db.getAllReports(); } /** * Deletes reports from the case - deletes it from the disk as well as the * database. * * @param reports Collection of Report to be deleted from the case. * @param deleteFromDisk Set true to perform reports file deletion from * disk. * * @throws TskCoreException */ public void deleteReports(Collection<? extends Report> reports, boolean deleteFromDisk) throws TskCoreException { String pathToReportsFolder = Paths.get(this.db.getDbDirPath(), "Reports").normalize().toString(); // NON-NLS for (Report report : reports) { // delete from the database. this.db.deleteReport(report); if (deleteFromDisk) { // traverse to the root directory of Report report. String reportPath = report.getPath(); while (!Paths.get(reportPath, "..").normalize().toString().equals(pathToReportsFolder)) { // NON-NLS reportPath = Paths.get(reportPath, "..").normalize().toString(); // NON-NLS } // delete from the disk. try { FileUtils.deleteDirectory(new File(reportPath)); } catch (IOException | SecurityException ex) { logger.log(Level.WARNING, NbBundle.getMessage(Case.class, "Case.deleteReports.deleteFromDiskException.log.msg"), ex); JOptionPane.showMessageDialog(null, NbBundle.getMessage(Case.class, "Case.deleteReports.deleteFromDiskException.msg", report.getReportName(), reportPath)); } } eventPublisher.publish(new AutopsyEvent(Events.REPORT_DELETED.toString(), null, null)); } } /** * Returns if the case has data in it yet. * * @return */ public boolean hasData() { // false is also the initial value, so make the DB trip if it is still false if (!hasData) { try { hasData = (getDataSources().size() > 0); } catch (TskCoreException ex) { } } return hasData; } }