diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java index b72bc14330..f90584a87f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java @@ -27,6 +27,7 @@ import org.sleuthkit.datamodel.TskCoreException; import org.apache.commons.lang.StringUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskData.TSK_FS_META_FLAG_ENUM; @@ -108,14 +109,30 @@ final class DataSourceInfoUtilities { * obtained. */ static T getBaseQueryResult(String query, ResultSetHandler processor, String errorMessage) { - try (SleuthkitCase.CaseDbQuery dbQuery = Case.getCurrentCaseThrows().getSleuthkitCase().executeQuery(query)) { + return getBaseQueryResult(SleuthkitCaseProvider.DEFAULT, query, processor, errorMessage); + } + + /** + * Retrieves a result based on the provided query. + * + * @param provider The means of obtaining a SleuthkitCase. + * @param query The query. + * @param processor The result set handler. + * @param errorMessage The error message to display if there is an error + * retrieving the resultset. + * + * @return The ResultSetHandler value or null if no ResultSet could be + * obtained. + */ + static T getBaseQueryResult(SleuthkitCaseProvider provider, String query, ResultSetHandler processor, String errorMessage) { + try (SleuthkitCase.CaseDbQuery dbQuery = provider.get().executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); try { return processor.process(resultSet); } catch (SQLException ex) { logger.log(Level.WARNING, errorMessage, ex); } - } catch (TskCoreException | NoCurrentCaseException ex) { + } catch (TskCoreException | SleuthkitCaseProviderException ex) { logger.log(Level.WARNING, errorMessage, ex); } return null; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopDomainsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopDomainsSummary.java new file mode 100644 index 0000000000..cc97b62389 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopDomainsSummary.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.datamodel; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.sleuthkit.datamodel.DataSource; + +/** + * Provides summary information about top domains in a datasource. At this time, + * the data being provided is fictitious and is done as a placeholder. + */ +public class DataSourceTopDomainsSummary { + + private static final long SLEEP_TIME = 5000; + + /** + * A function to calculate a result from 2 parameters. + */ + interface Function2 { + + O apply(A1 a1, A2 a2); + } + + /** + * Gets a list of recent domains based on the datasource. + * + * @param dataSource The datasource to query for recent domains. + * @param count The max count of items to return. + * + * @return The list of items retrieved from the database. + * + * @throws InterruptedException + */ + public List getRecentDomains(DataSource dataSource, int count) throws InterruptedException { + Thread.sleep(SLEEP_TIME); + final String dId = Long.toString(dataSource.getId()); + final Function2 getId = (s, idx) -> String.format("d:%s, f:%s, i:%d", dId, s, idx); + return IntStream.range(0, count) + .mapToObj(num -> new TopDomainsResult( + getId.apply("domain", num), + getId.apply("url", num), + (long) num, + new Date(((long) num) * 1000 * 60 * 60 * 24) + )) + .collect(Collectors.toList()); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java index 7c6927cd0d..495f706f0b 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java @@ -66,6 +66,36 @@ public class DataSourceTopProgramsSummary { */ private static final String QUERY_SUFFIX = "_query"; + /** + * Functions that determine the folder name of a list of path elements. If + * not matched, function returns null. + */ + private static final List, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( + // handle Program Files and Program Files (x86) - if true, return the next folder + (pathList) -> { + if (pathList.size() < 2) { + return null; + } + + String rootParent = pathList.get(0).toUpperCase(); + if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { + return pathList.get(1); + } else { + return null; + } + }, + // if there is a folder named "APPLICATION DATA" or "APPDATA" + (pathList) -> { + for (String pathEl : pathList) { + String uppered = pathEl.toUpperCase(); + if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { + return "AppData"; + } + } + return null; + } + ); + /** * Creates a sql statement querying the blackboard attributes table for a * particular attribute type and returning a specified value. That query @@ -138,6 +168,17 @@ public class DataSourceTopProgramsSummary { private static String getLikeClause(String column, String likeString, boolean isLike) { return column + (isLike ? "" : " NOT") + " LIKE '" + likeString + "'"; } + + + private final SleuthkitCaseProvider provider; + + public DataSourceTopProgramsSummary() { + this(SleuthkitCaseProvider.DEFAULT); + } + + public DataSourceTopProgramsSummary(SleuthkitCaseProvider provider) { + this.provider = provider; + } /** * Retrieves a list of the top programs used on the data source. Currently @@ -149,7 +190,7 @@ public class DataSourceTopProgramsSummary { * * @return */ - public static List getTopPrograms(DataSource dataSource, int count) { + public List getTopPrograms(DataSource dataSource, int count) { if (dataSource == null || count <= 0) { return Collections.emptyList(); } @@ -225,38 +266,9 @@ public class DataSourceTopProgramsSummary { return progResults; }; - return getBaseQueryResult(query, handler, errorMessage); + return getBaseQueryResult(provider, query, handler, errorMessage); } - - /** - * Functions that determine the folder name of a list of path elements. If - * not matched, function returns null. - */ - private static final List, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( - // handle Program Files and Program Files (x86) - if true, return the next folder - (pathList) -> { - if (pathList.size() < 2) { - return null; - } - - String rootParent = pathList.get(0).toUpperCase(); - if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { - return pathList.get(1); - } else { - return null; - } - }, - // if there is a folder named "APPLICATION DATA" or "APPDATA" - (pathList) -> { - for (String pathEl : pathList) { - String uppered = pathEl.toUpperCase(); - if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { - return "AppData"; - } - } - return null; - } - ); + /** * Determines a short folder name if any. Otherwise, returns empty string. @@ -265,7 +277,7 @@ public class DataSourceTopProgramsSummary { * * @return The short folder name or empty string if not found. */ - public static String getShortFolderName(String strPath, String applicationName) { + public String getShortFolderName(String strPath, String applicationName) { if (strPath == null) { return ""; } @@ -289,7 +301,4 @@ public class DataSourceTopProgramsSummary { return ""; } - - private DataSourceTopProgramsSummary() { - } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/SleuthkitCaseProvider.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/SleuthkitCaseProvider.java new file mode 100644 index 0000000000..8371b2fefd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/SleuthkitCaseProvider.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.datamodel; + +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * An interface to provide the current SleuthkitCase object. This is to allow + * for SleuthkitCase objects to be created and injected in a testing scenario + * outside of the context of Case. + * + * By default, this uses Case.getCurrentCaseThrows().getSleuthkitCase(). + */ +public interface SleuthkitCaseProvider { + + /** + * Exception thrown in the event that the SleuthkitCase object cannot be + * provided. + */ + class SleuthkitCaseProviderException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Main constructor. + * + * @param string The message for the exception. + */ + public SleuthkitCaseProviderException(String string) { + super(string); + } + + /** + * Main constructor. + * + * @param string The message for the exception. + * @param thrwbl The inner exception. + */ + public SleuthkitCaseProviderException(String string, Throwable thrwbl) { + super(string, thrwbl); + } + } + + /** + * The default SleuthkitCaseProvider. This uses + * Case.getCurrentCaseThrows().getSleuthkitCase(). + */ + SleuthkitCaseProvider DEFAULT = () -> { + try { + return Case.getCurrentCaseThrows().getSleuthkitCase(); + } catch (NoCurrentCaseException e) { + throw new SleuthkitCaseProviderException("No currently open case.", e); + } + }; + + /** + * @return Returns the current SleuthkitCase object. + * + * @throws SleuthkitCaseProviderException Thrown if there is an error + * providing the case. + */ + SleuthkitCase get() throws SleuthkitCaseProviderException; +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java new file mode 100644 index 0000000000..e843e51c4c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java @@ -0,0 +1,76 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.datamodel; + +import java.util.Date; + +/** + * Describes a result of a program run on a datasource. + */ +public class TopDomainsResult { + + private final String domain; + private final String url; + private final Long visitTimes; + private final Date lastVisit; + + /** + * Describes a top domain result. + * + * @param domain The domain. + * @param url The url. + * @param visitTimes The number of times it was visited. + * @param lastVisit The date of the last visit. + */ + public TopDomainsResult(String domain, String url, Long visitTimes, Date lastVisit) { + this.domain = domain; + this.url = url; + this.visitTimes = visitTimes; + this.lastVisit = lastVisit; + } + + /** + * @return The domain for the result. + */ + public String getDomain() { + return domain; + } + + /** + * @return The url for the result. + */ + public String getUrl() { + return url; + } + + /** + * @return The number of times this site is visited. + */ + public Long getVisitTimes() { + return visitTimes; + } + + /** + * @return The date of the last visit. + */ + public Date getLastVisit() { + return lastVisit; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java new file mode 100644 index 0000000000..9a0cddd05a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java @@ -0,0 +1,67 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.ui; + +import java.util.List; +import javax.swing.JPanel; +import javax.swing.SwingWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.SwingWorkerSequentialExecutor; +import org.sleuthkit.datamodel.DataSource; + +/** + * Base class from which other tabs in data source summary derive. + */ +abstract class BaseDataSourceSummaryPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + private final SwingWorkerSequentialExecutor executor = new SwingWorkerSequentialExecutor(); + private DataSource dataSource; + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + synchronized void setDataSource(DataSource dataSource) { + DataSource oldDataSource = this.dataSource; + this.dataSource = dataSource; + if (this.dataSource != oldDataSource) { + this.executor.cancelRunning(); + onNewDataSource(this.dataSource); + } + } + + /** + * Submits the following swing workers for execution in sequential order. If + * there are any previous workers, those workers are cancelled. + * + * @param workers The workers to submit for execution. + */ + protected void submit(List> workers) { + executor.submit(workers); + } + + /** + * When a new dataSource is added, this method is called. + * + * @param dataSource The new dataSource. + */ + protected abstract void onNewDataSource(DataSource dataSource); +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties index 462d910dde..708010e07a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties @@ -35,4 +35,5 @@ DataSourceSummaryDetailsPanel.unallocatedSizeLabel.text=Unallocated Space: DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category DataSourceSummaryCountsPanel.resultsByTypeLabel.text=Results by Type -DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Recent Programs +DataSourceSummaryUserActivityPanel.recentDomainsLabel.text=Recent Domains diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index 4f56ebd2f5..98f650b3de 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -73,8 +73,13 @@ DataSourceSummaryTabbedPane_countsTab_title=Counts DataSourceSummaryTabbedPane_detailsTab_title=Details DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History DataSourceSummaryTabbedPane_userActivityTab_title=User Activity -DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Recent Programs +DataSourceSummaryUserActivityPanel.recentDomainsLabel.text=Recent Domains +DataSourceSummaryUserActivityPanel_noDataExists=No communication data exists DataSourceSummaryUserActivityPanel_tab_title=User Activity +DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header=Domain +DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Access +DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header=URL DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceBrowser.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceBrowser.java index a31bbb03f7..98522e5071 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceBrowser.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.RightAlignedTableCellRenderer; import java.awt.EventQueue; import java.beans.PropertyVetoException; import javax.swing.ListSelectionModel; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java index c6df01a92b..306a036941 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.NonEditableTableModel; import java.util.Map; import javax.swing.JLabel; import javax.swing.table.DefaultTableCellRenderer; @@ -37,7 +38,7 @@ import org.sleuthkit.datamodel.DataSource; "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count" }) -final class DataSourceSummaryCountsPanel extends javax.swing.JPanel { +class DataSourceSummaryCountsPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; @@ -60,8 +61,6 @@ final class DataSourceSummaryCountsPanel extends javax.swing.JPanel { private final FileTypePieChart fileTypePieChart = new FileTypePieChart(); - private DataSource dataSource; - /** * Creates new form DataSourceSummaryCountsPanel */ @@ -73,22 +72,8 @@ final class DataSourceSummaryCountsPanel extends javax.swing.JPanel { setDataSource(null); } - /** - * The datasource currently used as the model in this panel. - * - * @return The datasource currently being used as the model in this panel. - */ - public DataSource getDataSource() { - return dataSource; - } - - /** - * Sets datasource to visualize in the panel. - * - * @param dataSource The datasource to use in this panel. - */ - public void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; + @Override + protected void onNewDataSource(DataSource dataSource) { if (dataSource == null || !Case.isCaseOpen()) { updateCountsTableData(EMPTY_PAIRS, EMPTY_PAIRS); } else { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java index f1972d9d89..3bc0eaf36d 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java @@ -33,7 +33,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Panel to display additional details associated with a specific DataSource */ -class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { +class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { //Because this panel was made using the gridbaglayout and netbean's Customize Layout tool it will be best to continue to modify it through that private static final long serialVersionUID = 1L; @@ -41,8 +41,6 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); private static final Logger logger = Logger.getLogger(DataSourceSummaryDetailsPanel.class.getName()); - private DataSource dataSource; - /** * Creates new form DataSourceSummaryDetailsPanel */ @@ -53,23 +51,8 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { setDataSource(null); } - /** - * The datasource currently used as the model in this panel. - * - * @return The datasource currently being used as the model in this panel. - */ - public DataSource getDataSource() { - return dataSource; - } - - /** - * Sets datasource to visualize in the panel. - * - * @param dataSource The datasource to use in this panel. - */ - public void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; - + @Override + protected void onNewDataSource(DataSource dataSource) { if (dataSource == null || !Case.isCaseOpen()) { updateDetailsPanelData(null, null, null, null); } else { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 9298646a9d..7e0cd951c3 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -18,10 +18,12 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; +import java.util.Arrays; +import java.util.List; import javax.swing.JTabbedPane; +import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; -import org.sleuthkit.autopsy.datasourcesummary.ui.Bundle; import org.sleuthkit.datamodel.DataSource; /** @@ -39,9 +41,13 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { private static final long serialVersionUID = 1L; - private final DataSourceSummaryCountsPanel countsPanel = new DataSourceSummaryCountsPanel(); - private final DataSourceSummaryDetailsPanel detailsPanel = new DataSourceSummaryDetailsPanel(); - private final DataSourceSummaryUserActivityPanel userActivityPanel = new DataSourceSummaryUserActivityPanel(); + // A pair of the tab name and the corresponding BaseDataSourceSummaryTabs to be displayed. + private final List> tabs = Arrays.asList( + Pair.of(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new DataSourceSummaryDetailsPanel()), + Pair.of(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), new DataSourceSummaryCountsPanel()), + Pair.of(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new DataSourceSummaryUserActivityPanel()) + ); + private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); private DataSource dataSource = null; @@ -50,10 +56,12 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { * Constructs a tabbed pane showing the summary of a data source. */ public DataSourceSummaryTabbedPane() { + for (Pair tab : tabs) { + addTab(tab.getKey(), tab.getValue()); + } - addTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), detailsPanel); - addTab(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), countsPanel); - addTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), userActivityPanel); + // IngestJobInfoPanel is not specifically a data source summary panel + // and is called separately for that reason. addTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel); } @@ -74,9 +82,12 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; - detailsPanel.setDataSource(dataSource); - countsPanel.setDataSource(dataSource); - userActivityPanel.setDataSource(dataSource); + for (Pair tab : tabs) { + tab.getValue().setDataSource(dataSource); + } + + // IngestJobInfoPanel is not specifically a data source summary panel + // and is called separately for that reason. ingestHistoryPanel.setDataSource(dataSource); } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form index 0829b9e045..9798add7eb 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form @@ -11,59 +11,180 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - + + + + + - - - - - - - - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java index 21b2a30f22..eb3f09a81f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java @@ -21,19 +21,23 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.awt.Component; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import java.util.Locale; -import javax.swing.JLabel; -import javax.swing.JTable; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; +import java.util.stream.Collectors; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceTopDomainsSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceTopProgramsSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopDomainsResult; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.HorizontalAlign; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; import org.sleuthkit.datamodel.DataSource; /** @@ -44,207 +48,134 @@ import org.sleuthkit.datamodel.DataSource; "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program", "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder", "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times", - "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run" -}) -public class DataSourceSummaryUserActivityPanel extends javax.swing.JPanel { + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run", + "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header=Domain", + "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header=URL", + "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Access", + "DataSourceSummaryUserActivityPanel_noDataExists=No communication data exists",}) +public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); private static final int TOP_PROGS_COUNT = 10; - private static final DefaultTableCellRenderer RIGHT_ALIGNED_RENDERER = new DefaultTableCellRenderer(); + private static final int TOP_DOMAINS_COUNT = 10; - static { - RIGHT_ALIGNED_RENDERER.setHorizontalAlignment(JLabel.RIGHT); - } - - private DataSource dataSource; + private final JTablePanel topProgramsTable; + private final JTablePanel recentDomainsTable; + private final List> dataFetchComponents; + private final List> tables; /** - * Creates new form DataSourceUserActivityPanel + * Creates a new DataSourceUserActivityPanel. */ public DataSourceSummaryUserActivityPanel() { + this(new DataSourceTopProgramsSummary(), new DataSourceTopDomainsSummary()); + } + + /** + * Creates a new DataSourceSummaryUserActivityPanel. + * + * @param topProgramsData Class from which to obtain top programs data. + * @param topDomainsData Class from which to obtain recent domains data. + */ + public DataSourceSummaryUserActivityPanel(DataSourceTopProgramsSummary topProgramsData, DataSourceTopDomainsSummary topDomainsData) { + // set up recent programs table + this.topProgramsTable = JTablePanel.getJTablePanel(Arrays.asList(new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header(), + (prog) -> { + return new DefaultCellModel(prog.getProgramName()) + .setTooltip(prog.getProgramPath()); + }, + 250), + new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header(), + (prog) -> { + return new DefaultCellModel( + topProgramsData.getShortFolderName( + prog.getProgramPath(), + prog.getProgramName())); + }, + 150), + new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header(), + (prog) -> { + String runTimes = prog.getRunTimes() == null ? "" : Long.toString(prog.getRunTimes()); + return new DefaultCellModel(runTimes) + .setHorizontalAlignment(HorizontalAlign.RIGHT); + }, + 80), + new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header(), + (prog) -> { + String date = prog.getLastRun() == null ? "" : DATETIME_FORMAT.format(prog.getLastRun()); + return new DefaultCellModel(date) + .setHorizontalAlignment(HorizontalAlign.RIGHT); + }, + 150) + )); + + // set up recent domains table + this.recentDomainsTable = JTablePanel.getJTablePanel(Arrays.asList(new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header(), + (d) -> new DefaultCellModel(d.getDomain()), + 250), + new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header(), + (d) -> new DefaultCellModel(d.getUrl()), + 250), + new ColumnModel<>( + Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header(), + (prog) -> { + String lastVisit = prog.getLastVisit() == null ? "" : DATETIME_FORMAT.format(prog.getLastVisit()); + return new DefaultCellModel(lastVisit) + .setHorizontalAlignment(HorizontalAlign.RIGHT); + }, + 150) + )); + + this.tables = Arrays.asList( + topProgramsTable, + recentDomainsTable + ); + + // set up data acquisition methods + dataFetchComponents = Arrays.asList( + new DataFetchComponents>( + (dataSource) -> topProgramsData.getTopPrograms(dataSource, TOP_PROGS_COUNT), + (result) -> topProgramsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), + Bundle.DataSourceSummaryUserActivityPanel_noDataExists())), + new DataFetchComponents>( + (dataSource) -> topDomainsData.getRecentDomains(dataSource, TOP_DOMAINS_COUNT), + (result) -> recentDomainsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), + Bundle.DataSourceSummaryUserActivityPanel_noDataExists())) + ); + initComponents(); - topProgramsTable.getTableHeader().setReorderingAllowed(false); } - /** - * The datasource currently used as the model in this panel. - * - * @return The datasource currently being used as the model in this panel. - */ - public DataSource getDataSource() { - return dataSource; - } - - /** - * Sets datasource to visualize in the panel. - * - * @param dataSource The datasource to use in this panel. - */ - public void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; + @Override + protected void onNewDataSource(DataSource dataSource) { + // if no data source is present or the case is not open, + // set results for tables to null. if (dataSource == null || !Case.isCaseOpen()) { - updateTopPrograms(new TopProgramsModel(null)); + this.dataFetchComponents.forEach((item) -> item.getResultHandler() + .accept(DataFetchResult.getSuccessResult(null))); + } else { - updateTopPrograms(getTopProgramsModel(dataSource)); + // set tables to display loading screen + this.tables.forEach((table) -> table.showDefaultLoadingMessage()); + + // create swing workers to run for each table + List> workers = dataFetchComponents + .stream() + .map((components) -> new DataFetchWorker<>(components, dataSource)) + .collect(Collectors.toList()); + + // submit swing workers to run + submit(workers); } } - /** - * Updates the Top Programs Table in the gui. - * - * @param data The data in Object[][] form to be used by the - * DefaultTableModel. - */ - private void updateTopPrograms(TopProgramsModel model) { - topProgramsTable.setModel(model); - topProgramsTable.getColumnModel().getColumn(0).setPreferredWidth(250); - topProgramsTable.getColumnModel().getColumn(0).setCellRenderer(PATH_CELL_RENDERER); - topProgramsTable.getColumnModel().getColumn(1).setPreferredWidth(150); - topProgramsTable.getColumnModel().getColumn(2).setCellRenderer(RIGHT_ALIGNED_RENDERER); - topProgramsTable.getColumnModel().getColumn(2).setPreferredWidth(80); - topProgramsTable.getColumnModel().getColumn(3).setPreferredWidth(150); - topProgramsScrollPane.getVerticalScrollBar().setValue(0); - this.repaint(); - } - - /** - * The counts of top programs run. - * - * @param selectedDataSource The DataSource. - * - * @return The JTable data model of counts of program runs. - */ - private static TopProgramsModel getTopProgramsModel(DataSource selectedDataSource) { - List topProgramList - = DataSourceTopProgramsSummary.getTopPrograms(selectedDataSource, TOP_PROGS_COUNT); - - if (topProgramList == null) { - return new TopProgramsModel(null); - } else { - return new TopProgramsModel(topProgramList); - } - } - - /** - * A POJO defining the values present in the name cell. Defines the name as - * well as the path for the tooltip. - */ - private static class ProgramNameCellValue { - - private final String programName; - private final String programPath; - - ProgramNameCellValue(String programName, String programPath) { - this.programName = programName; - this.programPath = programPath; - } - - @Override - public String toString() { - // override so that the value in the cell reads as programName - return programName; - } - - /** - * @return The program name. - */ - String getProgramName() { - return programName; - } - - /** - * @return The path of the program. - */ - String getProgramPath() { - return programPath; - } - } - - /** - * Defines a cell renderer for the first cell rendering the name as the text - * and path as the tooltip. - */ - private static TableCellRenderer PATH_CELL_RENDERER = new DefaultTableCellRenderer() { - - public Component getTableCellRendererComponent( - JTable table, Object value, - boolean isSelected, boolean hasFocus, - int row, int column) { - JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - if (value instanceof ProgramNameCellValue) { - ProgramNameCellValue cellValue = (ProgramNameCellValue) value; - c.setToolTipText(cellValue.getProgramPath()); - } - return c; - } - }; - - /** - * Defines the table model for a JTable of the programs. Accepts a list of - * TopProgramsResult objects as rows data source. - */ - private static class TopProgramsModel extends AbstractTableModel { - - private static final long serialVersionUID = 1L; - - // column headers for artifact counts table - private static final String[] TOP_PROGS_COLUMN_HEADERS = new String[]{ - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header(), - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header(), - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header(), - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header() - }; - - private final List programResults; - - /** - * Main constructor. - * - * @param programResults The results to display. - */ - TopProgramsModel(List programResults) { - this.programResults = programResults == null ? new ArrayList<>() : Collections.unmodifiableList(programResults); - } - - @Override - public String getColumnName(int column) { - return column < 0 || column >= TOP_PROGS_COLUMN_HEADERS.length ? null : TOP_PROGS_COLUMN_HEADERS[column]; - } - - @Override - public int getRowCount() { - return programResults.size(); - } - - @Override - public int getColumnCount() { - return TOP_PROGS_COLUMN_HEADERS.length; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (rowIndex < 0 || rowIndex >= programResults.size()) { - return null; - } - - TopProgramsResult result = programResults.get(rowIndex); - switch (columnIndex) { - case 0: - return new ProgramNameCellValue(result.getProgramName(), result.getProgramPath()); - case 1: - return DataSourceTopProgramsSummary.getShortFolderName(result.getProgramPath(), result.getProgramName()); - case 2: - return result.getRunTimes(); - case 3: - return result.getLastRun() == null ? null : DATETIME_FORMAT.format(result.getLastRun()); - default: - return null; - } - } - - } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -254,40 +185,56 @@ public class DataSourceSummaryUserActivityPanel extends javax.swing.JPanel { // //GEN-BEGIN:initComponents private void initComponents() { + javax.swing.JScrollPane contentScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel contentPanel = new javax.swing.JPanel(); javax.swing.JLabel programsRunLabel = new javax.swing.JLabel(); - topProgramsScrollPane = new javax.swing.JScrollPane(); - topProgramsTable = new javax.swing.JTable(); + javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); + javax.swing.JPanel topProgramsTablePanel = topProgramsTable; + javax.swing.Box.Filler filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20)); + javax.swing.JLabel recentDomainsLabel = new javax.swing.JLabel(); + javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); + javax.swing.JPanel recentDomainsTablePanel = recentDomainsTable; + setMaximumSize(null); + setLayout(new java.awt.BorderLayout()); + + contentScrollPane.setMaximumSize(null); + contentScrollPane.setMinimumSize(null); + + contentPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); + contentPanel.setMaximumSize(new java.awt.Dimension(720, 450)); + contentPanel.setMinimumSize(new java.awt.Dimension(720, 450)); + contentPanel.setLayout(new javax.swing.BoxLayout(contentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + + programsRunLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); org.openide.awt.Mnemonics.setLocalizedText(programsRunLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.programsRunLabel.text")); // NOI18N + programsRunLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + contentPanel.add(programsRunLabel); + contentPanel.add(filler1); - topProgramsScrollPane.setPreferredSize(new java.awt.Dimension(750, 187)); - topProgramsScrollPane.setViewportView(topProgramsTable); + topProgramsTablePanel.setAlignmentX(0.0F); + topProgramsTablePanel.setMaximumSize(new java.awt.Dimension(700, 187)); + topProgramsTablePanel.setMinimumSize(new java.awt.Dimension(700, 187)); + topProgramsTablePanel.setPreferredSize(new java.awt.Dimension(700, 187)); + contentPanel.add(topProgramsTablePanel); + contentPanel.add(filler3); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(programsRunLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 460, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addContainerGap(128, Short.MAX_VALUE)) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(programsRunLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); + recentDomainsLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + org.openide.awt.Mnemonics.setLocalizedText(recentDomainsLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.recentDomainsLabel.text")); // NOI18N + contentPanel.add(recentDomainsLabel); + contentPanel.add(filler2); + + recentDomainsTablePanel.setAlignmentX(0.0F); + recentDomainsTablePanel.setMaximumSize(new java.awt.Dimension(700, 187)); + recentDomainsTablePanel.setMinimumSize(new java.awt.Dimension(700, 187)); + recentDomainsTablePanel.setPreferredSize(new java.awt.Dimension(700, 187)); + contentPanel.add(recentDomainsTablePanel); + + contentScrollPane.setViewportView(contentPanel); + + add(contentScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JScrollPane topProgramsScrollPane; - private javax.swing.JTable topProgramsTable; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED new file mode 100644 index 0000000000..332a884f05 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED @@ -0,0 +1,3 @@ +JTablePanel_errorMessage_defaultText=There was an error loading results. +JTablePanel_loadingMessage_defaultText=Loading results... +JTablePanel_noDataExists_defaultText=No data exists. diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModelTableCellRenderer.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModelTableCellRenderer.java new file mode 100644 index 0000000000..cb826fefbd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModelTableCellRenderer.java @@ -0,0 +1,198 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import org.apache.commons.lang3.StringUtils; + +/** + * A Table cell renderer that renders a cell of a table based off of the + * CellModel interface provided within this class. + */ +public class CellModelTableCellRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + + /** + * Describes the horizontal alignment. + */ + public enum HorizontalAlign { + LEFT(JLabel.LEFT), + CENTER(JLabel.CENTER), + RIGHT(JLabel.RIGHT); + + private final int jlabelAlignment; + + /** + * Constructor for a HorizontalAlign enum. + * + * @param jlabelAlignment The corresponding JLabel horizontal alignment + * number. + */ + HorizontalAlign(int jlabelAlignment) { + this.jlabelAlignment = jlabelAlignment; + } + + /** + * @return The corresponding JLabel horizontal alignment (i.e. + * JLabel.LEFT). + */ + int getJLabelAlignment() { + return this.jlabelAlignment; + } + } + + /** + * Basic interface for a cell model. + */ + public interface CellModel { + + /** + * @return The text to be shown in the cell. + */ + String getText(); + + /** + * @return The tooltip (if any) to be displayed in the cell. + */ + String getTooltip(); + + /** + * @return The horizontal alignment for the text in the cell. + */ + HorizontalAlign getHorizontalAlignment(); + } + + /** + * The default cell model. + */ + public static class DefaultCellModel implements CellModel { + + private final String text; + private String tooltip; + private HorizontalAlign horizontalAlignment; + + /** + * Main constructor. + * + * @param text The text to be displayed in the cell. + */ + public DefaultCellModel(String text) { + this.text = text; + } + + @Override + public String getText() { + return text; + } + + @Override + public String getTooltip() { + return tooltip; + } + + /** + * Sets the tooltip for this cell model. + * + * @param tooltip The tooltip for the cell model. + * + * @return As a utility, returns this. + */ + public DefaultCellModel setTooltip(String tooltip) { + this.tooltip = tooltip; + return this; + } + + @Override + public HorizontalAlign getHorizontalAlignment() { + return horizontalAlignment; + } + + /** + * Sets the horizontal alignment for this cell model. + * + * @param alignment The horizontal alignment for the cell model. + * + * @return As a utility, returns this. + */ + public DefaultCellModel setHorizontalAlignment(HorizontalAlign alignment) { + this.horizontalAlignment = alignment; + return this; + } + + @Override + public String toString() { + return getText(); + } + } + + private static int DEFAULT_ALIGNMENT = JLabel.LEFT; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + + JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof CellModel) { + return getTableCellRendererComponent(c, (CellModel) value); + } else { + return c; + } + + } + + /** + * Customizes the jlabel to match the column model and cell model provided. + * + * @param defaultCell The cell to customize that will be displayed in the + * jtable. + * @param cellModel The cell model for this cell. + * + * @return The provided defaultCell. + */ + protected Component getTableCellRendererComponent(JLabel defaultCell, CellModel cellModel) { + // sets the text for the cell or null if not present. + String text = cellModel.getText(); + if (StringUtils.isNotBlank(text)) { + defaultCell.setText(text); + } else { + defaultCell.setText(null); + } + + // sets the tooltip for the cell if present. + String tooltip = cellModel.getTooltip(); + if (StringUtils.isNotBlank(tooltip)) { + defaultCell.setToolTipText(tooltip); + } else { + defaultCell.setToolTipText(null); + } + + // sets the JLabel alignment (left, center, right) or default alignment + // if no alignment is specified + int alignment = (cellModel.getHorizontalAlignment() == null) + ? DEFAULT_ALIGNMENT + : cellModel.getHorizontalAlignment().getJLabelAlignment(); + defaultCell.setHorizontalAlignment(alignment); + + return defaultCell; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java new file mode 100644 index 0000000000..93cc24f5fe --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java @@ -0,0 +1,93 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +/** + * The result of a loading process. + */ +public final class DataFetchResult { + + /** + * The type of result. + */ + public enum ResultType { + SUCCESS, ERROR + } + + /** + * Creates a DataFetchResult of loaded data including the data. + * + * @param data The data. + * + * @return The loaded data result. + */ + public static DataFetchResult getSuccessResult(R data) { + return new DataFetchResult<>(ResultType.SUCCESS, data, null); + } + + /** + * Returns an error result. + * + * @param e The exception (if any) present with the error. + * + * @return The error result. + */ + public static DataFetchResult getErrorResult(Throwable e) { + return new DataFetchResult<>(ResultType.ERROR, null, e); + } + + private final ResultType state; + private final R data; + private final Throwable exception; + + /** + * Main constructor for the DataLoadingResult. + * + * @param state The state of the result. + * @param data If the result is SUCCESS, the data related to this + * result. + * @param exception If the result is ERROR, the related exception. + */ + private DataFetchResult(ResultType state, R data, Throwable exception) { + this.state = state; + this.data = data; + this.exception = exception; + } + + /** + * @return The current loading state. + */ + public ResultType getResultType() { + return state; + } + + /** + * @return The data if the state is SUCCESS. + */ + public R getData() { + return data; + } + + /** + * @return The exception if the state is ERROR. + */ + public Throwable getException() { + return exception; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java new file mode 100644 index 0000000000..89f11f544c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java @@ -0,0 +1,152 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * A Swing worker that accepts an argument of a data fetcher and a result + * handler. If the data fetcher throws an InterruptedException, it is treated as + * a cancellation and not passed to the result handler. + */ +public class DataFetchWorker extends SwingWorker { + + /** + * Holds the functions necessary for a DataFetchWorker. Includes the fetcher + * and result handler. The args are not included since they are likely + * dynamic. + */ + public static class DataFetchComponents { + + private final DataFetcher fetcher; + private final Consumer> resultHandler; + + /** + * Main constructor. + * + * @param fetcher The fetcher to be used as an argument for the + * DataFetchWorker. + * @param resultHandler The result handler to be used as an argument for + * the DataFetchWorker. + */ + public DataFetchComponents(DataFetcher fetcher, Consumer> resultHandler) { + this.fetcher = fetcher; + this.resultHandler = resultHandler; + } + + /** + * @return The function that fetches the data. + */ + public DataFetcher getFetcher() { + return fetcher; + } + + /** + * @return When those results are received, this function handles + * presenting the results in the UI. + */ + public Consumer> getResultHandler() { + return resultHandler; + } + } + + private static final Logger logger = Logger.getLogger(DataFetchWorker.class.getName()); + + private final A args; + private final DataFetcher processor; + private final Consumer> resultHandler; + + /** + * Main constructor for this swing worker. + * + * @param components Accepts a components arg which provides a data + * processor and a results consumer. + * @param args The argument to be provided to the data processor. + */ + public DataFetchWorker(DataFetchComponents components, A args) { + this(components.getFetcher(), components.getResultHandler(), args); + } + + /** + * Main constructor for this swing worker. + * + * @param processor The function that will do the fetching of the data + * provided the given args. InterruptedException's are + * treated as cancellations and are not passed to the + * result handler. + * @param resultHandler The ui function that will handle the result of the + * data processing. + * @param args The args provided to the data processor. + */ + public DataFetchWorker( + DataFetcher processor, + Consumer> resultHandler, + A args) { + + this.args = args; + this.processor = processor; + this.resultHandler = resultHandler; + } + + @Override + protected R doInBackground() throws Exception { + return processor.runQuery(args); + } + + @Override + protected void done() { + // if cancelled, simply return + if (Thread.interrupted() || isCancelled()) { + return; + } + + R result = null; + try { + result = get(); + } catch (InterruptedException ignored) { + // if cancelled, simply return + return; + } catch (ExecutionException ex) { + Throwable inner = ex.getCause(); + // if cancelled during operation, simply return + if (inner instanceof InterruptedException) { + return; + } + + // otherwise, there is an error to log + logger.log(Level.WARNING, "There was an error while fetching results.", ex); + + // and pass the result to the client + resultHandler.accept(DataFetchResult.getErrorResult(inner)); + return; + } + + // if cancelled, simply return + if (Thread.interrupted() || isCancelled()) { + return; + } + + // if the data is loaded, send the data to the consumer. + resultHandler.accept(DataFetchResult.getSuccessResult(result)); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetcher.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetcher.java new file mode 100644 index 0000000000..6eabe79634 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetcher.java @@ -0,0 +1,43 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +/** + * A function that accepts input of type I and outputs type O. This function is + * meant to be utilized with DataFetchWorker and can therefore, throw an + * interrupted exception if the processing is cancelled or an Exception of on + * another type in the event that the fetching encountered an error. + */ +@FunctionalInterface +public interface DataFetcher { + + /** + * A function that accepts an input argument and outputs a result. Since it + * is meant to be used with the DataFetchWorker, it may throw an interrupted + * exception if the thread has been interrupted. It throws another type of + * exception if there is an error during fetching. + * + * @param input The input argument. + * + * @return The output result. + * + * @throws Exception + */ + O runQuery(I input) throws Exception; +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultListTableModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultListTableModel.java new file mode 100644 index 0000000000..ed472230c2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultListTableModel.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import javax.swing.table.AbstractTableModel; + +/** + * A TableModel for a JTable designed to show a list of data where each item in + * the list represents a row. + */ +public class DefaultListTableModel extends AbstractTableModel implements ListTableModel { + + private static final long serialVersionUID = 1L; + private final List> columns; + private List dataRows = Collections.emptyList(); + + /** + * Main constructor. + * + * @param columns A list of functions where the index of each function + * represents the data to be displayed at each column index. + * The data displayed at row 'r' and column 'c' will be the + * result of columns.get(c).apply(dataRows.get(r)). + */ + public DefaultListTableModel(List> columns) { + this.columns = columns; + } + + @Override + public List getDataRows() { + return dataRows; + } + + @Override + public void setDataRows(List dataRows) { + this.dataRows = dataRows == null ? Collections.emptyList() : new ArrayList<>(dataRows); + super.fireTableDataChanged(); + } + + @Override + public int getRowCount() { + return dataRows.size(); + } + + @Override + public int getColumnCount() { + return columns.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + // if index requested is null, return null + if (rowIndex < 0 || rowIndex >= dataRows.size() || columnIndex < 0 || columnIndex >= columns.size()) { + return null; + } + + // otherwise, get the corresponding row and use the corresponding + // column function to get the value + return columns.get(columnIndex).apply(dataRows.get(rowIndex)); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java new file mode 100644 index 0000000000..1e4184d85b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java @@ -0,0 +1,419 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.BorderLayout; +import java.awt.Graphics; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JLayer; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.plaf.LayerUI; +import javax.swing.table.DefaultTableColumnModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.CellModel; + +/** + * A table that displays a list of items and also can display messages for + * loading, load error, and not loaded. + */ +@Messages({ + "JTablePanel_loadingMessage_defaultText=Loading results...", + "JTablePanel_errorMessage_defaultText=There was an error loading results.", + "JTablePanel_noDataExists_defaultText=No data exists.",}) +public class JTablePanel extends JPanel { + + /** + * JTables don't allow displaying messages. So this LayerUI is used to + * display the contents of a child JLabel. Inspired by TableWaitLayerTest + * (Animating a Busy Indicator): + * https://docs.oracle.com/javase/tutorial/uiswing/misc/jlayer.html. + */ + private static class Overlay extends LayerUI { + + private static final long serialVersionUID = 1L; + + private final JLabel label; + private boolean visible; + + /** + * Main constructor for the Overlay. + */ + Overlay() { + label = new JLabel(); + label.setHorizontalAlignment(JLabel.CENTER); + label.setVerticalAlignment(JLabel.CENTER); + label.setOpaque(false); + + } + + /** + * @return Whether or not this message overlay should be visible. + */ + boolean isVisible() { + return visible; + } + + /** + * Sets this layer visible when painted. In order to be shown in UI, + * this component needs to be repainted. + * + * @param visible Whether or not it is visible. + */ + void setVisible(boolean visible) { + this.visible = visible; + } + + /** + * Sets the message to be displayed in the child jlabel. + * + * @param message The message to be displayed. + */ + void setMessage(String message) { + label.setText(String.format("
%s
", + message == null ? "" : message)); + } + + @Override + public void paint(Graphics g, JComponent c) { + // Paint the underlying view. + super.paint(g, c); + + if (!visible) { + return; + } + + int w = c.getWidth(); + int h = c.getHeight(); + + // paint the jlabel if visible. + label.setBounds(0, 0, w, h); + label.paint(g); + } + } + + /** + * Describes aspects of a column which can be used with getTableModel or + * getJTablePanel. 'T' represents the object that will represent rows in the + * table. + */ + public static class ColumnModel { + + private final String headerTitle; + private final Function cellRenderer; + private final Integer width; + + /** + * Constructor for a DataResultColumnModel. + * + * @param headerTitle The title for the column. + * @param cellRenderer The method that generates a CellModel for the + * column based on the data. + */ + public ColumnModel(String headerTitle, Function cellRenderer) { + this(headerTitle, cellRenderer, null); + } + + /** + * Constructor for a DataResultColumnModel. + * + * @param headerTitle The title for the column. + * @param cellRenderer The method that generates a CellModel for the + * column based on the data. + * @param width The preferred width of the column. + */ + public ColumnModel(String headerTitle, Function cellRenderer, Integer width) { + this.headerTitle = headerTitle; + this.cellRenderer = cellRenderer; + this.width = width; + } + + /** + * @return The title for the column. + */ + public String getHeaderTitle() { + return headerTitle; + } + + /** + * @return The method that generates a CellModel for the column based on + * the data. + */ + public Function getCellRenderer() { + return cellRenderer; + } + + /** + * @return The preferred width of the column (can be null). + */ + public Integer getWidth() { + return width; + } + } + + private static final long serialVersionUID = 1L; + + private static final Logger logger = Logger.getLogger(JTablePanel.class.getName()); + + private static final String DEFAULT_LOADING_MESSAGE = Bundle.JTablePanel_loadingMessage_defaultText(); + private static final String DEFAULT_ERROR_MESSAGE = Bundle.JTablePanel_errorMessage_defaultText(); + private static final String DEFAULT_NO_RESULTS_MESSAGE = Bundle.JTablePanel_noDataExists_defaultText(); + + private static final CellModelTableCellRenderer DEFAULT_CELL_RENDERER = new CellModelTableCellRenderer(); + + /** + * Generates a TableColumnModel based on the column definitions. + * + * @param columns The column definitions. + * + * @return The corresponding TableColumnModel to be used with a JTable. + */ + public static TableColumnModel getTableColumnModel(List> columns) { + TableColumnModel tableModel = new DefaultTableColumnModel(); + + for (int i = 0; i < columns.size(); i++) { + TableColumn col = new TableColumn(i); + ColumnModel model = columns.get(i); + // if a preferred width is specified in the column definition, + // set the underlying TableColumn preferred width. + if (model.getWidth() != null && model.getWidth() >= 0) { + col.setPreferredWidth(model.getWidth()); + } + + // set the title + col.setHeaderValue(model.getHeaderTitle()); + + // use the cell model renderer in this instance + col.setCellRenderer(DEFAULT_CELL_RENDERER); + + tableModel.addColumn(col); + } + + return tableModel; + } + + /** + * Generates a ListTableModel based on the column definitions provided where + * 'T' is the object representing each row. + * + * @param columns The column definitions. + * + * @return The corresponding ListTableModel. + */ + public static ListTableModel getTableModel(List> columns) { + List> columnRenderers = columns.stream() + .map((colModel) -> colModel.getCellRenderer()) + .collect(Collectors.toList()); + + return new DefaultListTableModel(columnRenderers); + } + + /** + * Generates a JTablePanel corresponding to the provided column definitions + * where 'T' is the object representing each row. + * + * @param columns The column definitions. + * + * @return The corresponding JTablePanel. + */ + public static JTablePanel getJTablePanel(List> columns) { + ListTableModel tableModel = getTableModel(columns); + JTablePanel resultTable = new JTablePanel<>(tableModel); + return resultTable.setColumnModel(getTableColumnModel(columns)); + } + + /** + * @return The default error message. + */ + public static String getDefaultErrorMessage() { + return DEFAULT_ERROR_MESSAGE; + } + + /** + * @return The default message for no results. + */ + public static String getDefaultNoResultsMessage() { + return DEFAULT_NO_RESULTS_MESSAGE; + } + + private final JScrollPane tableScrollPane; + private final Overlay overlayLayer; + private final ListTableModel tableModel; + private final JTable table; + + /** + * Main constructor. + * + * @param tableModel The model to use for the table. + */ + public JTablePanel(ListTableModel tableModel) { + this.tableModel = tableModel; + this.table = new JTable(tableModel); + this.table.getTableHeader().setReorderingAllowed(false); + + this.overlayLayer = new Overlay(); + this.tableScrollPane = new JScrollPane(table); + JLayer dualLayer = new JLayer(tableScrollPane, overlayLayer); + setLayout(new BorderLayout()); + add(dualLayer, BorderLayout.CENTER); + } + + /** + * @return The underlying JTable's column model. + */ + public TableColumnModel getColumnModel() { + return this.table.getColumnModel(); + } + + /** + * Sets the underlying JTable's column model. + * + * @param columnModel The table column model to use with the JTable. + * + * @return As a utility, returns this. + */ + public JTablePanel setColumnModel(TableColumnModel columnModel) { + this.table.setColumnModel(columnModel); + return this; + } + + /** + * Sets the data to be shown in the JTable. Repaint is not handled in this + * method and should be handled separately. + * + * @param data The list of data objects to be shown. + */ + private void setResultList(List data) { + // set the list of data to be shown as either the data or an empty list + // on null. + List dataToSet = (data == null) ? Collections.emptyList() : data; + + // since the data is being reset, scroll to the top. + tableScrollPane.getVerticalScrollBar().setValue(0); + + // set the underlying table model's data. + this.tableModel.setDataRows(dataToSet); + } + + /** + * Sets the message and visibility of the overlay. Repaint is not handled in + * this method and should be handled separately. + * + * @param visible The visibility of the overlay. + * @param message The message in the overlay. + */ + private void setOverlay(boolean visible, String message) { + this.overlayLayer.setVisible(visible); + this.overlayLayer.setMessage(message); + } + + /** + * Clears the results from the underlying JTable and shows the provided + * message. + * + * @param message The message to be shown. + */ + public synchronized void showMessage(String message) { + setResultList(null); + setOverlay(true, message); + repaint(); + } + + /** + * Shows a default loading message on the table. This will clear any results + * in the table. + */ + public void showDefaultLoadingMessage() { + showMessage(DEFAULT_LOADING_MESSAGE); + } + + /** + * Shows the list as rows of data in the table. If overlay message will be + * cleared if present. + * + * @param data The data to be shown where each item represents a row of + * data. + */ + public synchronized void showResults(List data) { + setOverlay(false, null); + setResultList(data); + repaint(); + } + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the errorMessage will be displayed. If the operation completed + * successfully and no data is present, noResultsMessage will be shown. + * Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + * @param errorMessage The error message to be shown in the event of an + * error. + * @param noResultsMessage The message to be shown if there are no results + * but the operation completed successfully. + */ + public void showDataFetchResult(DataFetchResult> result, String errorMessage, String noResultsMessage) { + if (result == null) { + logger.log(Level.SEVERE, "Null data processor result received."); + return; + } + + switch (result.getResultType()) { + case SUCCESS: + if (result.getData() == null || result.getData().isEmpty()) { + showMessage(noResultsMessage); + } else { + showResults(result.getData()); + } + break; + case ERROR: + // if there is an error, log accordingly, set result list to + // empty and display error message + logger.log(Level.WARNING, "An exception was caused while results were loaded.", result.getException()); + showMessage(errorMessage); + break; + default: + // an unknown loading state was specified. log accordingly. + logger.log(Level.SEVERE, "No known loading state was found in result."); + break; + } + } + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the DEFAULT_ERROR_MESSAGE will be displayed. If the operation + * completed successfully and no data is present, DEFAULT_NO_RESULTS_MESSAGE + * will be shown. Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + */ + public void showDataFetchResult(DataFetchResult> result) { + showDataFetchResult(result, DEFAULT_ERROR_MESSAGE, DEFAULT_NO_RESULTS_MESSAGE); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ListTableModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ListTableModel.java new file mode 100644 index 0000000000..bf5f1d1552 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ListTableModel.java @@ -0,0 +1,42 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.List; +import javax.swing.table.TableModel; + +/** + * An interface to be used with the JTablePanel that specifies a TableModel to + * be used with the underlying JTable based on a list of object type T. + */ +public interface ListTableModel extends TableModel { + + /** + * @return The list of objects supporting the rows to be displayed in the + * table. + */ + List getDataRows(); + + /** + * Sets the list of objects to be displayed in the table. + * + * @param dataRows The datarows to be displayed. + */ + void setDataRows(List dataRows); +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/NonEditableTableModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/NonEditableTableModel.java similarity index 74% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/NonEditableTableModel.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/NonEditableTableModel.java index 3274415a89..0461e514d8 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/NonEditableTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/NonEditableTableModel.java @@ -16,17 +16,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.datasourcesummary.ui; +package org.sleuthkit.autopsy.datasourcesummary.uiutils; import javax.swing.table.DefaultTableModel; /** * A Table model where cells are not editable. */ -class NonEditableTableModel extends DefaultTableModel { +public class NonEditableTableModel extends DefaultTableModel { + private static final long serialVersionUID = 1L; - - NonEditableTableModel(Object[][] data, Object[] columnNames) { + + /** + * Main constructor. + * + * @param data The data to be displayed. + * @param columnNames The column names. + */ + public NonEditableTableModel(Object[][] data, Object[] columnNames) { super(data, columnNames); } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RightAlignedTableCellRenderer.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/RightAlignedTableCellRenderer.java similarity index 94% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RightAlignedTableCellRenderer.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/RightAlignedTableCellRenderer.java index 989e97da82..6869ceea11 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RightAlignedTableCellRenderer.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/RightAlignedTableCellRenderer.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.datasourcesummary.ui; +package org.sleuthkit.autopsy.datasourcesummary.uiutils; import java.awt.Component; import java.lang.reflect.InvocationTargetException; @@ -31,7 +31,7 @@ import org.sleuthkit.autopsy.guiutils.GrayableCellRenderer; * cell contains a NodeProperty the value of that NodeProperty sets text to * empty string if null. */ -final class RightAlignedTableCellRenderer extends GrayableCellRenderer { +public class RightAlignedTableCellRenderer extends GrayableCellRenderer { private static final long serialVersionUID = 1L; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java new file mode 100644 index 0000000000..54abdb0ad9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import javax.swing.SwingWorker; + +/** + * Runs a list of swing workers in sequential order. Also, provides the ability + * to reset or cancel a run. + * + * Based on: + * https://stackoverflow.com/questions/31580805/java-swingworker-one-after-another-and-update-gui + */ +public class SwingWorkerSequentialExecutor { + + private final ExecutorService executorService = Executors.newFixedThreadPool(1); + private List> workers = Collections.emptyList(); + private List> futures = Collections.emptyList(); + + /** + * Cancels currently running operations and starts running the new list of + * swing workers. + * + * @param submittedWorkers The list of submitted swing workers. + */ + public synchronized void submit(List> submittedWorkers) { + // cancel currently running operations + cancelRunning(); + + // if no workers, there is nothing to run + if (submittedWorkers == null) { + return; + } + + this.workers = new ArrayList<>(submittedWorkers); + + // start running the workers and capture the futures if there is a need to cancel them. + this.futures = this.workers.stream() + .map((w) -> executorService.submit(w)) + .collect(Collectors.toList()); + } + + /** + * Cancels currently running items. + */ + public synchronized void cancelRunning() { + futures.forEach((f) -> f.cancel(true)); + workers = Collections.emptyList(); + futures = Collections.emptyList(); + } +}