diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java new file mode 100644 index 0000000000..b3a8f0eee3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java @@ -0,0 +1,430 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil; +import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides information about how a datasource relates to a previous case. NOTE: + * This code is fragile and has certain expectations about how the central + * repository handles creating artifacts. So, if the central repository changes + * ingest process, this code could break. This code expects that the central + * repository ingest module: + * + * a) Creates a TSK_INTERESTING_FILE_HIT artifact for a file whose hash is in + * the central repository as a notable file. + * + * b) Creates a TSK_INTERESTING_ARTIFACT_HIT artifact for a matching id in the + * central repository. + * + * c) The created artifact will have a TSK_COMMENT attribute attached where one + * of the sources for the attribute matches + * CentralRepoIngestModuleFactory.getModuleName(). The module display name at + * time of ingest will match CentralRepoIngestModuleFactory.getModuleName() as + * well. + * + * d) The content of that TSK_COMMENT attribute will be of the form "Previous + * Case: case1,case2...caseN" + */ +public class PastCasesSummary { + + /** + * Exception that is thrown in the event that a data source has not been + * ingested with a particular ingest module. + */ + public static class NotIngestedWithModuleException extends Exception { + private static final long serialVersionUID = 1L; + + private final String moduleDisplayName; + + /** + * Constructor. + * + * @param moduleName The module name. + * @param message The message for the exception. + */ + public NotIngestedWithModuleException(String moduleName, String message) { + super(message); + this.moduleDisplayName = moduleName; + } + + /** + * Constructor. + * + * @param moduleName The module name. + * @param message The message for the exception. + * @param thrwbl Inner exception if applicable. + */ + public NotIngestedWithModuleException(String moduleName, String message, Throwable thrwbl) { + super(message, thrwbl); + this.moduleDisplayName = moduleName; + } + + /** + * @return The module display name. + */ + public String getModuleDisplayName() { + return moduleDisplayName; + } + } + + /** + * Return type for results items in the past cases tab. + */ + public static class PastCasesResult { + + private final List> sameIdsResults; + private final List> taggedNotable; + + /** + * Main constructor. + * + * @param sameIdsResults Data for the cases with same id table. + * @param taggedNotable Data for the tagged notable table. + */ + public PastCasesResult(List> sameIdsResults, List> taggedNotable) { + this.sameIdsResults = sameIdsResults; + this.taggedNotable = taggedNotable; + } + + /** + * @return Data for the cases with same id table. + */ + public List> getSameIdsResults() { + return sameIdsResults; + } + + /** + * @return Data for the tagged notable table. + */ + public List> getTaggedNotable() { + return taggedNotable; + } + } + + private static final String CENTRAL_REPO_INGEST_NAME = CentralRepoIngestModuleFactory.getModuleName().toUpperCase().trim(); + private static final BlackboardAttribute.Type TYPE_COMMENT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_COMMENT); + private static final BlackboardAttribute.Type TYPE_ASSOCIATED_ARTIFACT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT); + + private static final Set CR_DEVICE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), + ARTIFACT_TYPE.TSK_DEVICE_INFO.getTypeID(), + ARTIFACT_TYPE.TSK_SIM_ATTACHED.getTypeID(), + ARTIFACT_TYPE.TSK_WIFI_NETWORK_ADAPTER.getTypeID() + )); + + private static final String CASE_SEPARATOR = ","; + private static final String PREFIX_END = ":"; + + private final SleuthkitCaseProvider caseProvider; + private final java.util.logging.Logger logger; + + /** + * Main constructor. + */ + public PastCasesSummary() { + this( + SleuthkitCaseProvider.DEFAULT, + (artifact) -> CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact), + org.sleuthkit.autopsy.coreutils.Logger.getLogger(DataSourceUserActivitySummary.class.getName()) + ); + + } + + /** + * Main constructor with external dependencies specified. This constructor + * is designed with unit testing in mind since mocked dependencies can be + * utilized. + * + * @param provider The object providing the current SleuthkitCase. + * @param logger The logger to use. + */ + public PastCasesSummary( + SleuthkitCaseProvider provider, + Function> corrAttrRetriever, + java.util.logging.Logger logger) { + + this.caseProvider = provider; + this.logger = logger; + } + + /** + * Given the provided sources for an attribute, aims to determine if one of + * those sources is the Central Repository Ingest Module. + * + * @param sources The list of sources found on an attribute. + * + * @return Whether or not this attribute (and subsequently the parent + * artifact) is created by the Central Repository Ingest Module. + */ + private static boolean isCentralRepoGenerated(List sources) { + if (sources == null) { + return false; + } + + return sources.stream().anyMatch((str) -> { + return str != null && CENTRAL_REPO_INGEST_NAME.equalsIgnoreCase(str.trim()); + }); + } + + /** + * Gets a list of cases from the TSK_COMMENT of an artifact. The cases + * string is expected to be of a form of "Previous Case: + * case1,case2...caseN". + * + * @param artifact The artifact. + * + * @return The list of cases if found or empty list if not. + */ + private static List getCasesFromArtifact(BlackboardArtifact artifact) { + if (artifact == null) { + return Collections.emptyList(); + } + + BlackboardAttribute commentAttr = null; + try { + commentAttr = artifact.getAttribute(TYPE_COMMENT); + } catch (TskCoreException ignored) { + // ignore if no attribute can be found + } + + if (commentAttr == null) { + return Collections.emptyList(); + } + + if (!isCentralRepoGenerated(commentAttr.getSources())) { + return Collections.emptyList(); + } + + String commentStr = commentAttr.getValueString(); + + int prefixCharIdx = commentStr.indexOf(PREFIX_END); + if (prefixCharIdx < 0 || prefixCharIdx >= commentStr.length() - 1) { + return Collections.emptyList(); + } + + String justCasesStr = commentStr.substring(prefixCharIdx + 1).trim(); + return Stream.of(justCasesStr.split(CASE_SEPARATOR)) + .map(String::trim) + .collect(Collectors.toList()); + + } + + /** + * Given a stream of case ids, groups the strings in a case-insensitive + * manner, and then provides a list of cases and the occurrence count sorted + * from max to min. + * + * @param cases A stream of cases. + * + * @return The list of unique cases and their occurrences sorted from max to min. + */ + private List> getCaseCounts(Stream cases) { + Collection> groupedCases = cases + // group by case insensitive compare of cases + .collect(Collectors.groupingBy((caseStr) -> caseStr.toUpperCase().trim())) + .values(); + + return groupedCases + .stream() + // get any cases where an actual case is found + .filter((lst) -> lst != null && lst.size() > 0) + // get non-normalized (i.e. not all caps) case name and number of items found + .map((lst) -> Pair.of(lst.get(0), (long) lst.size())) + // sorted descending + .sorted((a, b) -> -Long.compare(a.getValue(), b.getValue())) + .collect(Collectors.toList()); + } + + /** + * Given an artifact with a TYPE_ASSOCIATED_ARTIFACT attribute, retrieves the related artifact. + * @param skCase The sleuthkit case. + * @param artifact The artifact with the TYPE_ASSOCIATED_ARTIFACT attribute. + * @return The artifact if found or null if not. + * @throws SleuthkitCaseProviderException + */ + private BlackboardArtifact getParentArtifact(BlackboardArtifact artifact) throws SleuthkitCaseProviderException { + Long parentId = DataSourceInfoUtilities.getLongOrNull(artifact, TYPE_ASSOCIATED_ARTIFACT); + if (parentId == null) { + return null; + } + + SleuthkitCase skCase = caseProvider.get(); + try { + return skCase.getArtifactByArtifactId(parentId); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, + String.format("There was an error fetching the parent artifact of a TSK_INTERESTING_ARTIFACT_HIT (parent id: %d)", parentId), + ex); + return null; + } + } + + /** + * Returns true if the artifact has an associated artifact of a device type. + * @param artifact The artifact. + * @return True if there is a device associated artifact. + * @throws SleuthkitCaseProviderException + */ + private boolean hasDeviceAssociatedArtifact(BlackboardArtifact artifact) throws SleuthkitCaseProviderException { + BlackboardArtifact parent = getParentArtifact(artifact); + if (parent == null) { + return false; + } + + return CR_DEVICE_TYPE_IDS.contains(parent.getArtifactTypeID()); + } + + + /** + * Returns the past cases data to be shown in the past cases tab. + * @param dataSource The data source. + * @return The retrieved data. + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws NotIngestedWithModuleException + */ + public PastCasesResult getPastCasesData(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, NotIngestedWithModuleException { + + throwOnNotCentralRepoIngested(dataSource); + + SleuthkitCase skCase = caseProvider.get(); + + List deviceArtifactCases = new ArrayList<>(); + List nonDeviceArtifactCases = new ArrayList<>(); + + for (BlackboardArtifact artifact : skCase.getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID(), dataSource.getId())) { + List cases = getCasesFromArtifact(artifact); + if (cases == null || cases.isEmpty()) { + continue; + } + + if (hasDeviceAssociatedArtifact(artifact)) { + deviceArtifactCases.addAll(cases); + } else { + nonDeviceArtifactCases.addAll(cases); + } + } + + Stream filesCases = skCase.getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), dataSource.getId()).stream() + .flatMap((art) -> getCasesFromArtifact(art).stream()); + + return new PastCasesResult( + getCaseCounts(deviceArtifactCases.stream()), + getCaseCounts(Stream.concat(filesCases, nonDeviceArtifactCases.stream())) + ); + } + + + /** + * Returns true if the ingest job info contains an ingest module that + * matches the Central Repo Module ingest display name. + * + * @param info The info. + * + * @return True if there is a central repo ingest match. + */ + private boolean hasCentralRepoIngest(IngestJobInfo info) { + if (info == null || info.getIngestModuleInfo() == null) { + return false; + } + + return info.getIngestModuleInfo().stream() + .anyMatch((moduleInfo) -> { + return StringUtils.isNotBlank(moduleInfo.getDisplayName()) + && moduleInfo.getDisplayName().trim().equalsIgnoreCase(CENTRAL_REPO_INGEST_NAME); + }); + } + + /** + * Returns true if the central repository ingest module has been run on the + * datasource. + * + * @param dataSource The data source. + * + * @return True if there is an ingest job pertaining to the data source + * where an ingest module matches the central repo ingest module + * display name. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + public boolean isCentralRepoIngested(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException { + if (dataSource == null) { + return false; + } + + long dataSourceId = dataSource.getId(); + + return this.caseProvider.get().getIngestJobs().stream() + .anyMatch((ingestJob) -> { + return ingestJob != null + && ingestJob.getObjectId() == dataSourceId + && hasCentralRepoIngest(ingestJob); + }); + + } + + /** + * Throws an exception if the current data source has not been ingested with + * the Central Repository Ingest Module. + * + * @param dataSource The data source to check if it has been ingested with + * the Central Repository Ingest Module. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws NotIngestedWithModuleException + */ + private void throwOnNotCentralRepoIngested(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, NotIngestedWithModuleException { + + if (!isCentralRepoIngested(dataSource)) { + String objectId = (dataSource == null) ? "" : String.valueOf(dataSource.getId()); + String message = String.format("Data source: %s has not been ingested with the Central Repository Ingest Module.", objectId); + throw new NotIngestedWithModuleException(CENTRAL_REPO_INGEST_NAME, message); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties index c099f7e206..4886f25f68 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties @@ -39,3 +39,5 @@ AnalysisPanel.interestingItemLabel.text=Interesting Item Hits RecentFilesPanel.openDocsLabel.text=Recently Opened Documents RecentFilesPanel.downloadLabel.text=Recent Downloads RecentFilesPanel.attachmentLabel.text=Recent Attachements +PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable +PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs 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 0b14791df5..96515e7e1a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -44,6 +44,7 @@ DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source DataSourceSummaryTabbedPane_analysisTab_title=Analysis DataSourceSummaryTabbedPane_detailsTab_title=Container DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History +DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files DataSourceSummaryTabbedPane_typesTab_title=Types DataSourceSummaryTabbedPane_userActivityTab_title=User Activity @@ -72,6 +73,9 @@ DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_dateAccessed_header=Date Accessed DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_searchString_header=Search String DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_translatedResult_header=Translated +PastCasesPanel_caseColumn_title=Case +PastCasesPanel_countColumn_title=Count +PastCasesPanel_onNoCrIngest_message=No results will be shown because the Central Repository module was not run. RecentFilePanel_col_header_domain=Domain RecentFilePanel_col_header_path=Path RecentFilePanel_col_header_sender=Sender @@ -79,6 +83,8 @@ RecentFilePanel_no_open_documents=No recently open documents found. RecentFilesPanel.openDocsLabel.text=Recently Opened Documents RecentFilesPanel.downloadLabel.text=Recent Downloads RecentFilesPanel.attachmentLabel.text=Recent Attachements +PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable +PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs RecentFilesPanel_col_head_date=Date SizeRepresentationUtil_units_bytes=\ bytes SizeRepresentationUtil_units_gigabytes=\ GB diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 6e6a4947e0..91e320d97b 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -38,6 +38,7 @@ import org.sleuthkit.datamodel.DataSource; "DataSourceSummaryTabbedPane_userActivityTab_title=User Activity", "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History", "DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files", + "DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases", "DataSourceSummaryTabbedPane_analysisTab_title=Analysis" }) public class DataSourceSummaryTabbedPane extends JTabbedPane { @@ -106,8 +107,9 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { private final List tabs = Arrays.asList( new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_typesTab_title(), new TypesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), new DataSourceSummaryUserActivityPanel()), - new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_analysisTab_title(), new AnalysisPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_pastCasesTab_title(), new PastCasesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new DataSourceSummaryDetailsPanel()) ); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form new file mode 100644 index 0000000000..13ddb081bd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form @@ -0,0 +1,194 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java new file mode 100644 index 0000000000..295cd035dd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java @@ -0,0 +1,222 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.NotIngestedWithModuleException; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.PastCasesResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; +import org.sleuthkit.datamodel.DataSource; + +/** + * A tab shown in data source summary displaying information about a datasource + * and how it pertains to other cases. + */ +@Messages({ + "PastCasesPanel_caseColumn_title=Case", + "PastCasesPanel_countColumn_title=Count", + "PastCasesPanel_onNoCrIngest_message=No results will be shown because the Central Repository module was not run." + +}) +public class PastCasesPanel extends BaseDataSourceSummaryPanel { + + private static final long serialVersionUID = 1L; + + private static final ColumnModel> CASE_COL = new ColumnModel<>( + Bundle.PastCasesPanel_caseColumn_title(), + (pair) -> new DefaultCellModel(pair.getKey()), + 300 + ); + + private static final ColumnModel> COUNT_COL = new ColumnModel<>( + Bundle.PastCasesPanel_countColumn_title(), + (pair) -> new DefaultCellModel(String.valueOf(pair.getValue())), + 100 + ); + + private static final List>> DEFAULT_COLUMNS = Arrays.asList(CASE_COL, COUNT_COL); + + private final JTablePanel> notableFileTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + + private final JTablePanel> sameIdTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + + private final List> tables = Arrays.asList( + notableFileTable, + sameIdTable + ); + + private final List> dataFetchComponents; + + public PastCasesPanel() { + this(new PastCasesSummary()); + } + + /** + * Creates new form PastCasesPanel + */ + public PastCasesPanel(PastCasesSummary pastCaseData) { + // set up data acquisition methods + dataFetchComponents = Arrays.asList( + new DataFetchWorker.DataFetchComponents<>( + (dataSource) -> pastCaseData.getPastCasesData(dataSource), + (result) -> handleResult(result)) + ); + + initComponents(); + } + + /** + * handles displaying the result for the table. If a + * NotCentralRepoIngestedException is thrown, then an appropriate message is + * shown. Otherwise, this method uses the tables default showDataFetchResult + * method. + * + * @param result The result. + */ + private void handleResult(DataFetchResult result) { + if (result.getResultType() == ResultType.ERROR && result.getException() instanceof NotIngestedWithModuleException) { + notableFileTable.showMessage(Bundle.PastCasesPanel_onNoCrIngest_message()); + sameIdTable.showMessage(Bundle.PastCasesPanel_onNoCrIngest_message()); + } else { + notableFileTable.showDataFetchResult(getSubResult(result, (res) -> res.getTaggedNotable())); + sameIdTable.showDataFetchResult(getSubResult(result, (res) -> res.getSameIdsResults())); + } + } + + /** + * Given an input data fetch result, creates an error result if the original + * is an error. Otherwise, uses the getSubResult function on the underlying + * data to create a new DataFetchResult. + * + * @param inputResult The input result. + * @param getSubComponent The means of getting the data given the original + * data. + * + * @return The new result with the error of the original or the processed + * data. + */ + private DataFetchResult getSubResult(DataFetchResult inputResult, Function getSubResult) { + if (inputResult.getResultType() == ResultType.SUCCESS) { + return DataFetchResult.getSuccessResult(getSubResult.apply(inputResult.getData())); + } else { + return DataFetchResult.getErrorResult(inputResult.getException()); + } + } + + @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()) { + this.dataFetchComponents.forEach((item) -> item.getResultHandler() + .accept(DataFetchResult.getSuccessResult(null))); + + } else { + // 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); + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel mainContentPanel = new javax.swing.JPanel(); + javax.swing.JLabel notableFileLabel = new javax.swing.JLabel(); + 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(32767, 2)); + javax.swing.JPanel notableFilePanel = notableFileTable; + javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20), new java.awt.Dimension(32767, 20)); + javax.swing.JLabel sameIdLabel = new javax.swing.JLabel(); + javax.swing.Box.Filler filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(32767, 2)); + javax.swing.JPanel sameIdPanel = sameIdTable; + javax.swing.Box.Filler filler5 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); + + mainContentPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainContentPanel.setLayout(new javax.swing.BoxLayout(mainContentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + + org.openide.awt.Mnemonics.setLocalizedText(notableFileLabel, org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N + mainContentPanel.add(notableFileLabel); + notableFileLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N + + mainContentPanel.add(filler1); + + notableFilePanel.setAlignmentX(0.0F); + notableFilePanel.setMaximumSize(new java.awt.Dimension(32767, 106)); + notableFilePanel.setMinimumSize(new java.awt.Dimension(100, 106)); + notableFilePanel.setPreferredSize(new java.awt.Dimension(100, 106)); + mainContentPanel.add(notableFilePanel); + mainContentPanel.add(filler2); + + org.openide.awt.Mnemonics.setLocalizedText(sameIdLabel, org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.sameIdLabel.text")); // NOI18N + mainContentPanel.add(sameIdLabel); + mainContentPanel.add(filler3); + + sameIdPanel.setAlignmentX(0.0F); + sameIdPanel.setMaximumSize(new java.awt.Dimension(32767, 106)); + sameIdPanel.setMinimumSize(new java.awt.Dimension(100, 106)); + sameIdPanel.setPreferredSize(new java.awt.Dimension(100, 106)); + mainContentPanel.add(sameIdPanel); + mainContentPanel.add(filler5); + + mainScrollPane.setViewportView(mainContentPanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +}