mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-17 10:17:41 +00:00
Merge remote-tracking branch 'upstream/develop' into 7415-batch-save-files-in-file-ingest-pipeline
This commit is contained in:
commit
9795a55861
@ -89,6 +89,11 @@
|
||||
<fileset dir="${thirdparty.dir}/ImageMagick-7.0.10-27-portable-Q16-x64"/>
|
||||
</copy>
|
||||
|
||||
<!--Copy DomainCategorization to release-->
|
||||
<copy todir="${basedir}/release/DomainCategorization" >
|
||||
<fileset dir="${thirdparty.dir}/DomainCategorization"/>
|
||||
</copy>
|
||||
|
||||
<!-- The 'libgstlibav.dll' file is too big to store on GitHub, so we
|
||||
have it stored in a ZIP file. We'll extract it in place and remove
|
||||
the ZIP file afterward. -->
|
||||
|
@ -191,11 +191,10 @@ public class OsAccountDataPanel extends JPanel {
|
||||
private SectionData buildRealmProperties(OsAccountRealm realm) {
|
||||
SectionData data = new SectionData(Bundle.OsAccountDataPanel_realm_title());
|
||||
|
||||
Optional<String> optional = realm.getRealmName();
|
||||
data.addData(Bundle.OsAccountDataPanel_realm_name(),
|
||||
optional.isPresent() ? optional.get() : Bundle.OsAccountDataPanel_realm_unknown());
|
||||
String realmName = realm.getRealmNames().isEmpty() ? Bundle.OsAccountDataPanel_realm_unknown() : realm.getRealmNames().get(0);
|
||||
data.addData(Bundle.OsAccountDataPanel_realm_name(), realmName);
|
||||
|
||||
optional = realm.getRealmAddr();
|
||||
Optional<String> optional = realm.getRealmAddr();
|
||||
data.addData(Bundle.OsAccountDataPanel_realm_address(),
|
||||
optional.isPresent() ? optional.get() : "");
|
||||
|
||||
|
@ -256,12 +256,12 @@ public final class OsAccounts implements AutopsyVisitableItem {
|
||||
Bundle.OsAccounts_loginNameProperty_desc(),
|
||||
optional.isPresent() ? optional.get() : ""));
|
||||
|
||||
optional = account.getRealm().getRealmName();
|
||||
String realmName = account.getRealm().getRealmNames().isEmpty() ? "" : account.getRealm().getRealmNames().get(0);
|
||||
propertiesSet.put(new NodeProperty<>(
|
||||
Bundle.OsAccounts_accountRealmNameProperty_name(),
|
||||
Bundle.OsAccounts_accountRealmNameProperty_displayName(),
|
||||
Bundle.OsAccounts_accountRealmNameProperty_desc(),
|
||||
optional.isPresent() ? optional.get() : ""));
|
||||
realmName));
|
||||
|
||||
Optional<Long> creationTimeValue = account.getCreationTime();
|
||||
String timeDisplayStr
|
||||
|
@ -76,6 +76,8 @@ ExcelExportAction_exportToXLSX_gatheringTabData=Fetching Data for {0} Tab...
|
||||
ExcelExportAction_exportToXLSX_writingToFile=Writing to File...
|
||||
ExcelExportAction_getXLSXPath_directory=DataSourceSummary
|
||||
ExcelExportAction_moduleName=Data Source Summary
|
||||
ExcelExportAction_runXLSXExport_errorMessage=There was an error while exporting.
|
||||
ExcelExportAction_runXLSXExport_errorTitle=Error While Exporting
|
||||
ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling...
|
||||
ExcelExportAction_runXLSXExport_progressCancelTitle=Cancel
|
||||
# {0} - dataSource
|
||||
@ -114,6 +116,12 @@ SizeRepresentationUtil_units_megabytes=MB
|
||||
SizeRepresentationUtil_units_petabytes=PB
|
||||
SizeRepresentationUtil_units_terabytes=TB
|
||||
TimelinePanel_earliestLabel_title=Earliest
|
||||
TimelinePanel_getExports_activityRange=Activity Range
|
||||
TimelinePanel_getExports_chartName=Last 30 Days
|
||||
TimelinePanel_getExports_dateColumnHeader=Date
|
||||
TimelinePanel_getExports_earliest=Earliest:
|
||||
TimelinePanel_getExports_latest=Latest:
|
||||
TimelinePanel_getExports_sheetName=Timeline
|
||||
TimelinePanel_latestLabel_title=Latest
|
||||
TimlinePanel_last30DaysChart_artifactEvts_title=Result Events
|
||||
TimlinePanel_last30DaysChart_fileEvts_title=File Events
|
||||
@ -128,6 +136,7 @@ TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed
|
||||
TypesPanel_fileMimeTypesChart_other_title=Other
|
||||
TypesPanel_fileMimeTypesChart_title=File Types
|
||||
TypesPanel_fileMimeTypesChart_unknown_title=Unknown
|
||||
TypesPanel_fileMimeTypesChart_valueLabel=Count
|
||||
TypesPanel_fileMimeTypesChart_videos_title=Videos
|
||||
TypesPanel_filesByCategoryTable_allocatedRow_title=Allocated Files
|
||||
TypesPanel_filesByCategoryTable_directoryRow_title=Directories
|
||||
|
@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datasourcesummary.ui;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.File;
|
||||
@ -34,6 +33,7 @@ import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.SwingWorker;
|
||||
import org.openide.util.NbBundle;
|
||||
@ -186,7 +186,9 @@ class ExcelExportAction implements Consumer<DataSource> {
|
||||
"# {0} - dataSource",
|
||||
"ExcelExportAction_runXLSXExport_progressTitle=Exporting {0} to XLSX",
|
||||
"ExcelExportAction_runXLSXExport_progressCancelTitle=Cancel",
|
||||
"ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling..."
|
||||
"ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling...",
|
||||
"ExcelExportAction_runXLSXExport_errorTitle=Error While Exporting",
|
||||
"ExcelExportAction_runXLSXExport_errorMessage=There was an error while exporting.",
|
||||
})
|
||||
private void runXLSXExport(DataSource dataSource, File path) {
|
||||
|
||||
@ -213,6 +215,10 @@ class ExcelExportAction implements Consumer<DataSource> {
|
||||
get();
|
||||
} catch (ExecutionException ex) {
|
||||
logger.log(Level.WARNING, "Error while trying to export data source summary to xlsx.", ex);
|
||||
JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(),
|
||||
Bundle.ExcelExportAction_runXLSXExport_errorMessage(),
|
||||
Bundle.ExcelExportAction_runXLSXExport_errorTitle(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
} catch (InterruptedException | CancellationException ex) {
|
||||
// no op on cancellation
|
||||
} finally {
|
||||
|
@ -39,14 +39,20 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineDataSourceUtils
|
||||
import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.DailyActivityAmount;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.TimelineSummaryData;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartItem;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartSeries;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.OrderedKey;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries.BarChartItem;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetcher;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.KeyValueItemExportable;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.TitledExportable;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel;
|
||||
@ -70,7 +76,9 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(TimelinePanel.class.getName());
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final DateFormat EARLIEST_LATEST_FORMAT = getUtcFormat("MMM d, yyyy");
|
||||
|
||||
private static final String EARLIEST_LATEST_FORMAT_STR = "MMM d, yyyy";
|
||||
private static final DateFormat EARLIEST_LATEST_FORMAT = getUtcFormat(EARLIEST_LATEST_FORMAT_STR);
|
||||
private static final DateFormat CHART_FORMAT = getUtcFormat("MMM d, yyyy");
|
||||
private static final int MOST_RECENT_DAYS_COUNT = 30;
|
||||
|
||||
@ -94,6 +102,8 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
// all loadable components on this tab
|
||||
private final List<LoadableComponent<?>> loadableComponents = Arrays.asList(earliestLabel, latestLabel, last30DaysChart);
|
||||
|
||||
private final DataFetcher<DataSource, TimelineSummaryData> dataFetcher;
|
||||
|
||||
// actions to load data for this tab
|
||||
private final List<DataFetchComponents<DataSource, ?>> dataFetchComponents;
|
||||
|
||||
@ -107,12 +117,11 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
public TimelinePanel(TimelineSummary timelineData) {
|
||||
super(timelineData);
|
||||
|
||||
dataFetcher = (dataSource) -> timelineData.getData(dataSource, MOST_RECENT_DAYS_COUNT);
|
||||
|
||||
// set up data acquisition methods
|
||||
dataFetchComponents = Arrays.asList(
|
||||
new DataFetchWorker.DataFetchComponents<>(
|
||||
(dataSource) -> timelineData.getData(dataSource, MOST_RECENT_DAYS_COUNT),
|
||||
(result) -> handleResult(result))
|
||||
);
|
||||
new DataFetchWorker.DataFetchComponents<>(dataFetcher, (result) -> handleResult(result)));
|
||||
|
||||
initComponents();
|
||||
}
|
||||
@ -138,9 +147,11 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
* data to be displayed as a bar chart.
|
||||
*
|
||||
* @param recentDaysActivity The data retrieved from TimelineSummary.
|
||||
* @param showIntermediateDates If true, shows all dates. If false, shows
|
||||
* only first and last date.
|
||||
* @return The data to be displayed in the BarChart.
|
||||
*/
|
||||
private List<BarChartSeries> parseChartData(List<DailyActivityAmount> recentDaysActivity) {
|
||||
private List<BarChartSeries> parseChartData(List<DailyActivityAmount> recentDaysActivity, boolean showIntermediateDates) {
|
||||
// if no data, return null indicating no result.
|
||||
if (CollectionUtils.isEmpty(recentDaysActivity)) {
|
||||
return null;
|
||||
@ -155,7 +166,7 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
|
||||
long fileAmt = curItem.getFileActivityCount();
|
||||
long artifactAmt = curItem.getArtifactActivityCount() * 100;
|
||||
String formattedDate = (i == 0 || i == recentDaysActivity.size() - 1)
|
||||
String formattedDate = (showIntermediateDates || i == 0 || i == recentDaysActivity.size() - 1)
|
||||
? formatDate(curItem.getDay(), CHART_FORMAT) : "";
|
||||
|
||||
OrderedKey thisKey = new OrderedKey(formattedDate, i);
|
||||
@ -182,7 +193,7 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
private void handleResult(DataFetchResult<TimelineSummaryData> result) {
|
||||
earliestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMinDate(), EARLIEST_LATEST_FORMAT)));
|
||||
latestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMaxDate(), EARLIEST_LATEST_FORMAT)));
|
||||
last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity())));
|
||||
last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity(), false)));
|
||||
|
||||
if (result != null
|
||||
&& result.getResultType() == DataFetchResult.ResultType.SUCCESS
|
||||
@ -282,11 +293,43 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel {
|
||||
super.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default cell model to be use with excel export in the earliest /
|
||||
* latest date format.
|
||||
*
|
||||
* @param date The date.
|
||||
* @return The cell model.
|
||||
*/
|
||||
private static DefaultCellModel<?> getEarliestLatestCell(Date date) {
|
||||
return new DefaultCellModel<>(date, (dt) -> dt == null ? "" : EARLIEST_LATEST_FORMAT.format(dt), EARLIEST_LATEST_FORMAT_STR);
|
||||
}
|
||||
|
||||
@Messages({
|
||||
"TimelinePanel_getExports_sheetName=Timeline",
|
||||
"TimelinePanel_getExports_activityRange=Activity Range",
|
||||
"TimelinePanel_getExports_earliest=Earliest:",
|
||||
"TimelinePanel_getExports_latest=Latest:",
|
||||
"TimelinePanel_getExports_dateColumnHeader=Date",
|
||||
"TimelinePanel_getExports_chartName=Last 30 Days",})
|
||||
@Override
|
||||
List<ExcelExport.ExcelSheetExport> getExports(DataSource dataSource) {
|
||||
TimelineSummaryData summaryData = getFetchResult(dataFetcher, "Timeline", dataSource);
|
||||
if (summaryData == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Arrays.asList(
|
||||
new ExcelSpecialFormatExport(Bundle.TimelinePanel_getExports_sheetName(),
|
||||
Arrays.asList(
|
||||
new TitledExportable(Bundle.TimelinePanel_getExports_activityRange(), Collections.emptyList()),
|
||||
new KeyValueItemExportable(Bundle.TimelinePanel_getExports_earliest(), getEarliestLatestCell(summaryData.getMinDate())),
|
||||
new KeyValueItemExportable(Bundle.TimelinePanel_getExports_latest(), getEarliestLatestCell(summaryData.getMaxDate())),
|
||||
new BarChartExport(Bundle.TimelinePanel_getExports_dateColumnHeader(),
|
||||
"#,###",
|
||||
Bundle.TimelinePanel_getExports_chartName(),
|
||||
parseChartData(summaryData.getMostRecentDaysActivity(), true)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from within the constructor to initialize the form.
|
||||
* WARNING: Do NOT modify this code. The content of this method is always
|
||||
|
@ -47,8 +47,9 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel.PieChartItem;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartItem;
|
||||
|
||||
import org.sleuthkit.datamodel.DataSource;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
@ -64,6 +65,7 @@ import org.sleuthkit.datamodel.TskCoreException;
|
||||
"TypesPanel_filesByCategoryTable_slackRow_title=Slack Files",
|
||||
"TypesPanel_filesByCategoryTable_directoryRow_title=Directories",
|
||||
"TypesPanel_fileMimeTypesChart_title=File Types",
|
||||
"TypesPanel_fileMimeTypesChart_valueLabel=Count",
|
||||
"TypesPanel_fileMimeTypesChart_audio_title=Audio",
|
||||
"TypesPanel_fileMimeTypesChart_documents_title=Documents",
|
||||
"TypesPanel_fileMimeTypesChart_executables_title=Executables",
|
||||
@ -197,6 +199,8 @@ class TypesPanel extends BaseDataSourceSummaryPanel {
|
||||
private final DataFetcher<DataSource, String> osFetcher;
|
||||
private final DataFetcher<DataSource, Long> sizeFetcher;
|
||||
|
||||
private final DataFetcher<DataSource, TypesPieChartData> typesFetcher;
|
||||
|
||||
private final DataFetcher<DataSource, Long> allocatedFetcher;
|
||||
private final DataFetcher<DataSource, Long> unallocatedFetcher;
|
||||
private final DataFetcher<DataSource, Long> slackFetcher;
|
||||
@ -262,6 +266,8 @@ class TypesPanel extends BaseDataSourceSummaryPanel {
|
||||
|
||||
this.sizeFetcher = (dataSource) -> dataSource == null ? null : dataSource.getSize();
|
||||
|
||||
this.typesFetcher = (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource);
|
||||
|
||||
this.allocatedFetcher = (dataSource) -> typeData.getCountOfAllocatedFiles(dataSource);
|
||||
this.unallocatedFetcher = (dataSource) -> typeData.getCountOfUnallocatedFiles(dataSource);
|
||||
this.slackFetcher = (dataSource) -> typeData.getCountOfSlackFiles(dataSource);
|
||||
@ -274,9 +280,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel {
|
||||
(sizeResult) -> sizeLabel.showDataFetchResult(
|
||||
DataFetchResult.getSubResult(sizeResult,
|
||||
size -> SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false)))),
|
||||
new DataFetchWorker.DataFetchComponents<>(
|
||||
(dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource),
|
||||
this::showMimeTypeCategories),
|
||||
new DataFetchWorker.DataFetchComponents<>(typesFetcher, this::showMimeTypeCategories),
|
||||
new DataFetchWorker.DataFetchComponents<>(allocatedFetcher,
|
||||
countRes -> allocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))),
|
||||
new DataFetchWorker.DataFetchComponents<>(unallocatedFetcher,
|
||||
@ -443,12 +447,23 @@ class TypesPanel extends BaseDataSourceSummaryPanel {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Retrieve data to create the types pie chart
|
||||
TypesPieChartData typesData = TypesPanel.getFetchResult(typesFetcher, "Types", dataSource);
|
||||
PieChartExport typesChart = (typesData == null || !typesData.isUsefulContent()) ? null :
|
||||
new PieChartExport(
|
||||
Bundle.TypesPanel_fileMimeTypesChart_title(),
|
||||
Bundle.TypesPanel_fileMimeTypesChart_valueLabel(),
|
||||
"#,###",
|
||||
Bundle.TypesPanel_fileMimeTypesChart_title(),
|
||||
typesData.getPieSlices());
|
||||
|
||||
return Arrays.asList(new ExcelSpecialFormatExport(Bundle.TypesPanel_excelTabName(),
|
||||
Stream.of(
|
||||
getStrExportable(usageFetcher, Bundle.TypesPanel_usageLabel_title(), dataSource),
|
||||
getStrExportable(osFetcher, Bundle.TypesPanel_osLabel_title(), dataSource),
|
||||
new KeyValueItemExportable(Bundle.TypesPanel_sizeLabel_title(),
|
||||
SizeRepresentationUtil.getBytesCell(getFetchResult(sizeFetcher, "Types", dataSource))),
|
||||
typesChart,
|
||||
getCountExportable(allocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_allocatedRow_title(), dataSource),
|
||||
getCountExportable(unallocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_unallocatedRow_title(), dataSource),
|
||||
getCountExportable(slackFetcher, Bundle.TypesPanel_filesByCategoryTable_slackRow_title(), dataSource),
|
||||
|
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datasourcesummary.uiutils;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.xddf.usermodel.XDDFColor;
|
||||
import org.apache.poi.xddf.usermodel.XDDFShapeProperties;
|
||||
import org.apache.poi.xddf.usermodel.XDDFSolidFillProperties;
|
||||
import org.apache.poi.xddf.usermodel.chart.AxisCrosses;
|
||||
import org.apache.poi.xddf.usermodel.chart.AxisPosition;
|
||||
import org.apache.poi.xddf.usermodel.chart.BarDirection;
|
||||
import org.apache.poi.xddf.usermodel.chart.BarGrouping;
|
||||
import org.apache.poi.xddf.usermodel.chart.ChartTypes;
|
||||
import org.apache.poi.xddf.usermodel.chart.LegendPosition;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFCategoryAxis;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFChartData;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFValueAxis;
|
||||
import org.apache.poi.xssf.usermodel.XSSFChart;
|
||||
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDrawing;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ItemDimensions;
|
||||
|
||||
/**
|
||||
* Class that creates an excel stacked bar chart along with data table.
|
||||
*/
|
||||
public class BarChartExport implements ExcelItemExportable, ExcelSheetExport {
|
||||
|
||||
/**
|
||||
* Creates an excel table model to be written to an excel sheet and used as
|
||||
* a datasource for the chart.
|
||||
*
|
||||
* @param categories The categories with their data.
|
||||
* @param keyColumnHeader The header column name for the table descriptions
|
||||
* (i.e. types: file types / artifact types).
|
||||
* @param valueFormatString The excel format string to use for values.
|
||||
* @return An excel table export to be used as the data source for the chart
|
||||
* in the excel document.
|
||||
*/
|
||||
private static ExcelTableExport<Pair<Object, List<Double>>, ? extends ExcelCellModel> getTableModel(
|
||||
List<BarChartSeries> categories, String keyColumnHeader, String chartTitle) {
|
||||
|
||||
// get the row keys by finding the series with the largest set of bar items
|
||||
// (they should all be equal, but just in case)
|
||||
List<? extends Object> rowKeys = categories.stream()
|
||||
.filter(cat -> cat != null && cat.getItems() != null)
|
||||
.map(cat -> cat.getItems())
|
||||
.max((items1, items2) -> Integer.compare(items1.size(), items2.size()))
|
||||
.orElse(Collections.emptyList())
|
||||
.stream()
|
||||
.map((barChartItem) -> barChartItem.getKey())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// map of (bar chart category index, bar chart item index) -> value
|
||||
Map<Pair<Integer, Integer>, Double> valueMap = IntStream.range(0, categories.size())
|
||||
.mapToObj(idx -> Pair.of(idx, categories.get(idx)))
|
||||
.filter(pair -> pair.getValue() != null && pair.getValue().getItems() != null)
|
||||
.flatMap(categoryPair -> {
|
||||
return IntStream.range(0, categoryPair.getValue().getItems().size())
|
||||
.mapToObj(idx -> Pair.of(idx, categoryPair.getValue().getItems().get(idx)))
|
||||
.map(itemPair -> Pair.of(
|
||||
Pair.of(categoryPair.getKey(), itemPair.getKey()),
|
||||
itemPair.getValue() == null ? null : itemPair.getValue().getValue()));
|
||||
})
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue(), (v1, v2) -> v1));
|
||||
|
||||
// Create rows of data to be displayed where each row is a tuple of the bar chart item
|
||||
// key and the list of values in category order.
|
||||
List<Pair<Object, List<Double>>> values = IntStream.range(0, rowKeys.size())
|
||||
.mapToObj(idx -> Pair.of(idx, rowKeys.get(idx)))
|
||||
.map((rowPair) -> {
|
||||
List<Double> items = IntStream.range(0, categories.size())
|
||||
.mapToObj(idx -> valueMap.get(Pair.of(idx, rowPair.getKey())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Pair.of(rowPair.getValue(), items);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Create the model for the category column
|
||||
ColumnModel<Pair<Object, List<Double>>, DefaultCellModel<?>> categoryColumn
|
||||
= new ColumnModel<>(keyColumnHeader, (row) -> new DefaultCellModel<>(row.getKey()));
|
||||
|
||||
// create the models for each category of data to be displayed
|
||||
Stream<ColumnModel<Pair<Object, List<Double>>, DefaultCellModel<?>>> dataColumns = IntStream.range(0, categories.size())
|
||||
.mapToObj(idx -> new ColumnModel<>(
|
||||
categories.get(idx).getKey().toString(),
|
||||
(row) -> new DefaultCellModel<>(row.getValue().get(idx))));
|
||||
|
||||
// create table
|
||||
return new ExcelTableExport<Pair<Object, List<Double>>, DefaultCellModel<?>>(
|
||||
chartTitle,
|
||||
Stream.concat(Stream.of(categoryColumn), dataColumns)
|
||||
.collect(Collectors.toList()),
|
||||
values
|
||||
);
|
||||
}
|
||||
|
||||
private static final int DEFAULT_ROW_SIZE = 15;
|
||||
private static final int DEFAULT_COL_SIZE = 10;
|
||||
private static final int DEFAULT_ROW_PADDING = 1;
|
||||
private static final int DEFAULT_COL_OFFSET = 1;
|
||||
|
||||
private final ExcelTableExport<Pair<Object, List<Double>>, ? extends ExcelCellModel> tableExport;
|
||||
private final int colOffset;
|
||||
private final int rowPadding;
|
||||
private final int colSize;
|
||||
private final int rowSize;
|
||||
private final String chartTitle;
|
||||
private final String sheetName;
|
||||
private final List<BarChartSeries> categories;
|
||||
private final String keyColumnHeader;
|
||||
|
||||
/**
|
||||
* Main constructor that assumes some defaults (i.e. chart size follows
|
||||
* defaults and sheet name is chart title).
|
||||
*
|
||||
* @param keyColumnHeader The header column name for the table descriptions
|
||||
* (i.e. types: file types / artifact types).
|
||||
* @param valueFormatString The excel format string to use for values.
|
||||
* @param chartTitle The title for the chart.
|
||||
* @param categories The categories along with data.
|
||||
*/
|
||||
public BarChartExport(String keyColumnHeader,
|
||||
String valueFormatString,
|
||||
String chartTitle,
|
||||
List<BarChartSeries> categories) {
|
||||
this(keyColumnHeader, valueFormatString, chartTitle, chartTitle, categories,
|
||||
DEFAULT_COL_OFFSET, DEFAULT_ROW_PADDING, DEFAULT_COL_SIZE, DEFAULT_ROW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param keyColumnHeader The header column name for the table descriptions
|
||||
* (i.e. types: file types / artifact types).
|
||||
* @param valueFormatString The excel format string to use for values.
|
||||
* @param chartTitle The title for the chart.
|
||||
* @param sheetName The sheet name if used as a sheet export.
|
||||
* @param categories The categories along with data.
|
||||
* @param colOffset The column spacing between the table and the chart.
|
||||
* @param rowPadding The padding between this and data above or below (if
|
||||
* used as an ExcelItemExportable).
|
||||
* @param colSize The column size of the chart.
|
||||
* @param rowSize The row size of the chart.
|
||||
*/
|
||||
public BarChartExport(String keyColumnHeader, String valueFormatString,
|
||||
String chartTitle, String sheetName,
|
||||
List<BarChartSeries> categories,
|
||||
int colOffset, int rowPadding, int colSize, int rowSize) {
|
||||
|
||||
this.keyColumnHeader = keyColumnHeader;
|
||||
this.tableExport = getTableModel(categories, keyColumnHeader, chartTitle);
|
||||
this.colOffset = colOffset;
|
||||
this.rowPadding = rowPadding;
|
||||
this.colSize = colSize;
|
||||
this.rowSize = rowSize;
|
||||
this.chartTitle = chartTitle;
|
||||
this.sheetName = sheetName;
|
||||
this.categories = categories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSheetName() {
|
||||
return sheetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExport.ExcelExportException {
|
||||
write(sheet, 0, 0, env);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException {
|
||||
if (!(sheet instanceof XSSFSheet)) {
|
||||
throw new ExcelExportException("Sheet must be an XSSFSheet in order to write.");
|
||||
}
|
||||
|
||||
XSSFSheet xssfSheet = (XSSFSheet) sheet;
|
||||
|
||||
// write pie chart table data
|
||||
ItemDimensions tableDimensions = tableExport.write(xssfSheet, rowStart + rowPadding, colStart, env);
|
||||
|
||||
XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();
|
||||
|
||||
int chartColStart = colStart + categories.size() + 1 + colOffset;
|
||||
|
||||
//createAnchor has arguments of (int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2);
|
||||
XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, chartColStart, rowStart + rowPadding, chartColStart + colSize + 1, rowStart + rowSize + 1);
|
||||
|
||||
XSSFChart chart = drawing.createChart(anchor);
|
||||
chart.setTitleText(chartTitle);
|
||||
chart.setTitleOverlay(false);
|
||||
XDDFChartLegend legend = chart.getOrAddLegend();
|
||||
legend.setPosition(LegendPosition.BOTTOM);
|
||||
|
||||
// Use a category axis for the bottom axis.
|
||||
XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
|
||||
bottomAxis.setTitle(keyColumnHeader);
|
||||
XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
|
||||
leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);
|
||||
leftAxis.setVisible(false);
|
||||
|
||||
XDDFBarChartData data = (XDDFBarChartData) chart.createData(ChartTypes.BAR, bottomAxis, leftAxis);
|
||||
data.setBarGrouping(BarGrouping.STACKED);
|
||||
|
||||
XDDFDataSource<String> headerSource = XDDFDataSourcesFactory.fromStringCellRange(xssfSheet,
|
||||
new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(),
|
||||
tableDimensions.getColStart(), tableDimensions.getColStart()));
|
||||
|
||||
data.setBarDirection(BarDirection.COL);
|
||||
|
||||
// set data for each series and set color if applicable
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
XDDFChartData.Series series = data.addSeries(headerSource,
|
||||
XDDFDataSourcesFactory.fromNumericCellRange(xssfSheet,
|
||||
new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(),
|
||||
tableDimensions.getColStart() + 1 + i, tableDimensions.getColStart() + 1 + i)));
|
||||
|
||||
series.setTitle(categories.size() > i && categories.get(i).getKey() != null ? categories.get(i).getKey().toString() : "", null);
|
||||
if (categories.get(i).getColor() != null) {
|
||||
Color color = categories.get(i).getColor();
|
||||
byte[] colorArrARGB = ByteBuffer.allocate(4).putInt(color.getRGB()).array();
|
||||
byte[] colorArrRGB = new byte[]{colorArrARGB[1], colorArrARGB[2], colorArrARGB[3]};
|
||||
XDDFSolidFillProperties fill = new XDDFSolidFillProperties(XDDFColor.from(colorArrRGB));
|
||||
XDDFShapeProperties properties = series.getShapeProperties();
|
||||
if (properties == null) {
|
||||
properties = new XDDFShapeProperties();
|
||||
}
|
||||
properties.setFillProperties(fill);
|
||||
series.setShapeProperties(properties);
|
||||
}
|
||||
}
|
||||
|
||||
chart.plot(data);
|
||||
|
||||
return new ItemDimensions(rowStart, colStart, Math.max(tableDimensions.getRowEnd(), rowStart + rowSize) + rowPadding, chartColStart + colSize);
|
||||
}
|
||||
|
||||
}
|
@ -19,9 +19,7 @@
|
||||
package org.sleuthkit.autopsy.datasourcesummary.uiutils;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.swing.JLabel;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
@ -34,90 +32,12 @@ import org.jfree.chart.plot.PlotOrientation;
|
||||
import org.jfree.chart.renderer.category.BarRenderer;
|
||||
import org.jfree.chart.renderer.category.StandardBarPainter;
|
||||
import org.jfree.data.category.DefaultCategoryDataset;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries.BarChartItem;
|
||||
|
||||
/**
|
||||
* A bar chart panel.
|
||||
*/
|
||||
public class BarChartPanel extends AbstractLoadableComponent<List<BarChartPanel.BarChartSeries>> {
|
||||
|
||||
/**
|
||||
* Represents a series in a bar chart where all items pertain to one
|
||||
* category.
|
||||
*/
|
||||
public static class BarChartSeries {
|
||||
|
||||
private final Comparable<?> key;
|
||||
private final Color color;
|
||||
private final List<BarChartItem> items;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param key The key.
|
||||
* @param color The color for this series.
|
||||
* @param items The bars to be displayed for this series.
|
||||
*/
|
||||
public BarChartSeries(Comparable<?> key, Color color, List<BarChartItem> items) {
|
||||
this.key = key;
|
||||
this.color = color;
|
||||
this.items = (items == null) ? Collections.emptyList() : Collections.unmodifiableList(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The color for this series.
|
||||
*/
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The bars to be displayed for this series.
|
||||
*/
|
||||
public List<BarChartItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The key for this item.
|
||||
*/
|
||||
public Comparable<?> getKey() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An individual bar to be displayed in the bar chart.
|
||||
*/
|
||||
public static class BarChartItem {
|
||||
|
||||
private final Comparable<?> key;
|
||||
private final double value;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param key The key.
|
||||
* @param value The value for this item.
|
||||
*/
|
||||
public BarChartItem(Comparable<?> key, double value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The key for this item.
|
||||
*/
|
||||
public Comparable<?> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value for this item.
|
||||
*/
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
public class BarChartPanel extends AbstractLoadableComponent<List<BarChartSeries>> {
|
||||
|
||||
/**
|
||||
* JFreeChart bar charts don't preserve the order of bars provided to the
|
||||
@ -285,12 +205,12 @@ public class BarChartPanel extends AbstractLoadableComponent<List<BarChartPanel.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setResults(List<BarChartPanel.BarChartSeries> data) {
|
||||
protected void setResults(List<BarChartSeries> data) {
|
||||
this.dataset.clear();
|
||||
|
||||
if (CollectionUtils.isNotEmpty(data)) {
|
||||
for (int s = 0; s < data.size(); s++) {
|
||||
BarChartPanel.BarChartSeries series = data.get(s);
|
||||
BarChartSeries series = data.get(s);
|
||||
if (series != null && CollectionUtils.isNotEmpty(series.getItems())) {
|
||||
if (series.getColor() != null) {
|
||||
this.plot.getRenderer().setSeriesPaint(s, series.getColor());
|
||||
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datasourcesummary.uiutils;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a series in a bar chart where all items pertain to one category.
|
||||
*/
|
||||
public class BarChartSeries {
|
||||
|
||||
/**
|
||||
* An individual bar to be displayed in the bar chart.
|
||||
*/
|
||||
public static class BarChartItem {
|
||||
|
||||
private final Comparable<?> key;
|
||||
private final double value;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param key The key.
|
||||
* @param value The value for this item.
|
||||
*/
|
||||
public BarChartItem(Comparable<?> key, double value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The key for this item.
|
||||
*/
|
||||
public Comparable<?> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value for this item.
|
||||
*/
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
private final Comparable<?> key;
|
||||
private final Color color;
|
||||
private final List<BarChartItem> items;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param key The key.
|
||||
* @param color The color for this series.
|
||||
* @param items The bars to be displayed for this series.
|
||||
*/
|
||||
public BarChartSeries(Comparable<?> key, Color color, List<BarChartItem> items) {
|
||||
this.key = key;
|
||||
this.color = color;
|
||||
this.items = (items == null) ? Collections.emptyList() : Collections.unmodifiableList(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The color for this series.
|
||||
*/
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The bars to be displayed for this series.
|
||||
*/
|
||||
public List<BarChartItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The key for this item.
|
||||
*/
|
||||
public Comparable<?> getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
@ -81,6 +81,8 @@ public class ExcelTableExport<T, C extends ExcelCellModel> implements ExcelSheet
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
// freeze header row
|
||||
sheet.createFreezePane(0, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -119,10 +121,8 @@ public class ExcelTableExport<T, C extends ExcelCellModel> implements ExcelSheet
|
||||
cell.setCellValue(columns.get(i).getHeaderTitle());
|
||||
cell.setCellStyle(worksheetEnv.getHeaderStyle());
|
||||
}
|
||||
// freeze header row
|
||||
sheet.createFreezePane(0, 1);
|
||||
// Create Cell Style for each column (if one is needed)
|
||||
|
||||
// Create Cell Style for each column (if one is needed)
|
||||
for (int rowNum = 0; rowNum < safeData.size(); rowNum++) {
|
||||
T rowData = safeData.get(rowNum);
|
||||
Row row = sheet.createRow(rowNum + rowStart + 1);
|
||||
|
@ -352,6 +352,20 @@ public class JTablePanel<T> extends AbstractLoadableComponent<List<T>> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected items or null if no item is selected.
|
||||
* @return The selected items or null if no item is selected.
|
||||
*/
|
||||
public List<T> getSelectedItems() {
|
||||
int selectedRow = this.table.getSelectedRow();
|
||||
int count = this.table.getSelectedRowCount();
|
||||
if (selectedRow < 0 || this.tableModel == null || selectedRow + count > this.tableModel.getDataRows().size()) {
|
||||
return null;
|
||||
} else {
|
||||
return this.tableModel.getDataRows().subList(selectedRow, selectedRow + count);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void setResults(List<T> data) {
|
||||
// get previously selected value
|
||||
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datasourcesummary.uiutils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import org.apache.poi.xddf.usermodel.chart.LegendPosition;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource;
|
||||
import org.apache.poi.xddf.usermodel.chart.XDDFPieChartData;
|
||||
import org.apache.poi.xssf.usermodel.XSSFChart;
|
||||
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDrawing;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.openxmlformats.schemas.drawingml.x2006.chart.CTPieChart;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ItemDimensions;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class that creates an excel pie chart along with data table.
|
||||
*/
|
||||
public class PieChartExport implements ExcelItemExportable, ExcelSheetExport {
|
||||
|
||||
private static final int DEFAULT_ROW_SIZE = 20;
|
||||
private static final int DEFAULT_COL_SIZE = 10;
|
||||
private static final int DEFAULT_ROW_PADDING = 1;
|
||||
private static final int DEFAULT_COL_OFFSET = 1;
|
||||
|
||||
private final ExcelTableExport<PieChartItem, ? extends ExcelCellModel> tableExport;
|
||||
private final int colOffset;
|
||||
private final int rowPadding;
|
||||
private final int colSize;
|
||||
private final int rowSize;
|
||||
private final String chartTitle;
|
||||
private final String sheetName;
|
||||
|
||||
/**
|
||||
* Main constructor assuming defaults.
|
||||
*
|
||||
* @param keyColumnHeader The header column name for the table descriptions
|
||||
* (i.e. file types).
|
||||
* @param valueColumnHeader The header column name for the values.
|
||||
* @param valueFormatString The excel format string to use for values.
|
||||
* @param chartTitle The title for the chart.
|
||||
* @param slices The values for the pie slices.
|
||||
*/
|
||||
public PieChartExport(String keyColumnHeader,
|
||||
String valueColumnHeader, String valueFormatString,
|
||||
String chartTitle,
|
||||
List<PieChartItem> slices) {
|
||||
this(keyColumnHeader, valueColumnHeader, valueFormatString, chartTitle, chartTitle, slices,
|
||||
DEFAULT_COL_OFFSET, DEFAULT_ROW_PADDING, DEFAULT_COL_SIZE, DEFAULT_ROW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param keyColumnHeader The header column name for the table descriptions
|
||||
* (i.e. file types).
|
||||
* @param valueColumnHeader The header column name for the values.
|
||||
* @param valueFormatString The excel format string to use for values.
|
||||
* @param chartTitle The title for the chart.
|
||||
* @param sheetName The sheet name if used as a sheet export.
|
||||
* @param slices The values for the pie slices.
|
||||
* @param colOffset The column spacing between the table and the chart.
|
||||
* @param rowPadding The padding between this and data above or below (if
|
||||
* used as an ExcelItemExportable).
|
||||
* @param colSize The column size of the chart.
|
||||
* @param rowSize The row size of the chart.
|
||||
*/
|
||||
public PieChartExport(String keyColumnHeader,
|
||||
String valueColumnHeader, String valueFormatString,
|
||||
String chartTitle, String sheetName,
|
||||
List<PieChartItem> slices,
|
||||
int colOffset, int rowPadding, int colSize, int rowSize) {
|
||||
|
||||
this.tableExport = new ExcelTableExport<>(chartTitle,
|
||||
Arrays.asList(
|
||||
new ColumnModel<>(keyColumnHeader, (slice) -> new DefaultCellModel<>(slice.getLabel())),
|
||||
new ColumnModel<>(valueColumnHeader, (slice) -> new DefaultCellModel<>(slice.getValue(), null, valueFormatString))
|
||||
),
|
||||
slices);
|
||||
this.colOffset = colOffset;
|
||||
this.rowPadding = rowPadding;
|
||||
this.colSize = colSize;
|
||||
this.rowSize = rowSize;
|
||||
this.chartTitle = chartTitle;
|
||||
this.sheetName = sheetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSheetName() {
|
||||
return sheetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExport.ExcelExportException {
|
||||
write(sheet, 0, 0, env);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException {
|
||||
if (!(sheet instanceof XSSFSheet)) {
|
||||
throw new ExcelExportException("Sheet must be an XSSFSheet in order to write.");
|
||||
}
|
||||
|
||||
XSSFSheet xssfSheet = (XSSFSheet) sheet;
|
||||
|
||||
// write pie chart table data
|
||||
ItemDimensions tableDimensions = tableExport.write(xssfSheet, rowStart + rowPadding, colStart, env);
|
||||
|
||||
XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();
|
||||
|
||||
int chartColStart = colStart + 2 + colOffset;
|
||||
|
||||
//createAnchor has arguments of (int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2);
|
||||
XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, chartColStart, rowStart + rowPadding, chartColStart + colSize + 1, rowStart + rowSize + 1);
|
||||
|
||||
XSSFChart chart = drawing.createChart(anchor);
|
||||
chart.setTitleText(chartTitle);
|
||||
chart.setTitleOverlay(false);
|
||||
XDDFChartLegend legend = chart.getOrAddLegend();
|
||||
legend.setPosition(LegendPosition.RIGHT);
|
||||
|
||||
// CellRangeAddress has arguments of (int firstRow, int lastRow, int firstCol, int lastCol)
|
||||
XDDFDataSource<String> cat = XDDFDataSourcesFactory.fromStringCellRange(xssfSheet,
|
||||
new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(),
|
||||
tableDimensions.getColStart(), tableDimensions.getColStart()));
|
||||
|
||||
XDDFNumericalDataSource<Double> val = XDDFDataSourcesFactory.fromNumericCellRange(xssfSheet,
|
||||
new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(),
|
||||
tableDimensions.getColStart() + 1, tableDimensions.getColStart() + 1));
|
||||
|
||||
// NOTE: There appears to be a classpath issue with POI (a version of 4.0.1 and 4.1.1 simultaneously)
|
||||
// that is causing conflicts for XDDFPieChartData creation (i.e. the compiler thinks its using 4.1.1
|
||||
// and the runtime thinks its using 4.0.1) Reflection is used below to use the 4.0.1 method while
|
||||
// sidestepping compiler issues.
|
||||
// XDDFPieChartData creation that can be used in poi >= 4.1.1:
|
||||
// XDDFPieChartData data = (XDDFPieChartData) chart.createData(ChartTypes.PIE, bottomAxis, leftAxis);
|
||||
// XDDFPieChartData creation that can be used in 4.0.1:
|
||||
// XDDFPieChartData data = new XDDFPieChartData(chart.getCTChart().getPlotArea().addNewPieChart());
|
||||
XDDFPieChartData data;
|
||||
try {
|
||||
Constructor<XDDFPieChartData> constructor = XDDFPieChartData.class.getConstructor(CTPieChart.class);
|
||||
constructor.setAccessible(true);
|
||||
data = constructor.newInstance(chart.getCTChart().getPlotArea().addNewPieChart());
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | IllegalArgumentException ex) {
|
||||
throw new ExcelExportException("Error while instantiating chart data.", ex);
|
||||
}
|
||||
|
||||
data.setVaryColors(true);
|
||||
data.addSeries(cat, val);
|
||||
|
||||
// Add data labels
|
||||
if (!chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).isSetDLbls()) {
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).addNewDLbls();
|
||||
}
|
||||
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowVal().setVal(true);
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowSerName().setVal(false);
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowCatName().setVal(true);
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowPercent().setVal(true);
|
||||
chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowLegendKey().setVal(false);
|
||||
|
||||
chart.plot(data);
|
||||
|
||||
return new ItemDimensions(rowStart, colStart, Math.max(tableDimensions.getRowEnd(), rowStart + rowSize) + rowPadding, chartColStart + colSize);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datasourcesummary.uiutils;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* An individual pie chart slice in the pie chart.
|
||||
*/
|
||||
public class PieChartItem {
|
||||
|
||||
private final String label;
|
||||
private final double value;
|
||||
private final Color color;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param label The label for this pie slice.
|
||||
* @param value The value for this item.
|
||||
* @param color The color for the pie slice. Can be null for
|
||||
* auto-determined.
|
||||
*/
|
||||
public PieChartItem(String label, double value, Color color) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The label for this item.
|
||||
*/
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value for this item.
|
||||
*/
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The color for the pie slice or null for auto-determined.
|
||||
*/
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 Basis Technology Corp.
|
||||
* Copyright 2020 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -39,52 +39,7 @@ import org.openide.util.NbBundle.Messages;
|
||||
@Messages({
|
||||
"PieChartPanel_noDataLabel=No Data"
|
||||
})
|
||||
public class PieChartPanel extends AbstractLoadableComponent<List<PieChartPanel.PieChartItem>> {
|
||||
|
||||
/**
|
||||
* An individual pie chart slice in the pie chart.
|
||||
*/
|
||||
public static class PieChartItem {
|
||||
|
||||
private final String label;
|
||||
private final double value;
|
||||
private final Color color;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param label The label for this pie slice.
|
||||
* @param value The value for this item.
|
||||
* @param color The color for the pie slice. Can be null for
|
||||
* auto-determined.
|
||||
*/
|
||||
public PieChartItem(String label, double value, Color color) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The label for this item.
|
||||
*/
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The value for this item.
|
||||
*/
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The color for the pie slice or null for auto-determined.
|
||||
*/
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
public class PieChartPanel extends AbstractLoadableComponent<List<PieChartItem>> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ -176,12 +131,12 @@ public class PieChartPanel extends AbstractLoadableComponent<List<PieChartPanel.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setResults(List<PieChartPanel.PieChartItem> data) {
|
||||
protected void setResults(List<PieChartItem> data) {
|
||||
this.dataset.clear();
|
||||
this.plot.clearSectionPaints(false);
|
||||
|
||||
if (data != null && !data.isEmpty()) {
|
||||
for (PieChartPanel.PieChartItem slice : data) {
|
||||
for (PieChartItem slice : data) {
|
||||
this.dataset.setValue(slice.getLabel(), slice.getValue());
|
||||
if (slice.getColor() != null) {
|
||||
this.plot.setSectionPaint(slice.getLabel(), slice.getColor());
|
||||
@ -202,7 +157,7 @@ public class PieChartPanel extends AbstractLoadableComponent<List<PieChartPanel.
|
||||
* @param data The data.
|
||||
* @param message The message.
|
||||
*/
|
||||
public synchronized void showDataWithMessage(List<PieChartPanel.PieChartItem> data, String message) {
|
||||
public synchronized void showDataWithMessage(List<PieChartItem> data, String message) {
|
||||
setResults(data);
|
||||
setMessage(true, message);
|
||||
repaint();
|
||||
|
@ -85,9 +85,8 @@ public final class IngestProgressSnapshotDialog extends JDialog {
|
||||
this.getRootPane().registerKeyboardAction(e -> {
|
||||
this.dispose();
|
||||
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
add(new IngestProgressSnapshotPanel(this, provider));
|
||||
add(new IngestProgressSnapshotPanel(this, provider), BorderLayout.CENTER);
|
||||
pack();
|
||||
setResizable(false);
|
||||
if (shouldBeModal) { // if called from a modal dialog, become modal, otherwise don't.
|
||||
setModal(true);
|
||||
}
|
||||
|
@ -1,6 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
|
||||
<Properties>
|
||||
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[500, 500]"/>
|
||||
</Property>
|
||||
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[600, 500]"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
|
||||
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
|
||||
@ -11,52 +19,20 @@
|
||||
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
|
||||
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,-12,0,0,3,-123"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout>
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="1" attributes="0">
|
||||
<Component id="snapshotsScrollPane" pref="881" max="32767" attributes="0"/>
|
||||
<Group type="102" attributes="0">
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
<Component id="refreshButton" linkSize="1" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="closeButton" linkSize="1" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="jobScrollPane" pref="881" max="32767" attributes="0"/>
|
||||
<Component id="moduleScrollPane" pref="881" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="snapshotsScrollPane" pref="102" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="jobScrollPane" pref="102" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="moduleScrollPane" pref="100" max="32767" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="refreshButton" linkSize="2" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="closeButton" linkSize="2" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
</Layout>
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
|
||||
<SubComponents>
|
||||
<Container class="javax.swing.JScrollPane" name="snapshotsScrollPane">
|
||||
<AuxValues>
|
||||
<AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="0" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="11" insetsLeft="10" insetsBottom="0" insetsRight="10" anchor="18" weightX="1.0" weightY="1.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
|
||||
<SubComponents>
|
||||
@ -79,6 +55,11 @@
|
||||
<AuxValues>
|
||||
<AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="1" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="6" insetsLeft="10" insetsBottom="0" insetsRight="10" anchor="18" weightX="1.0" weightY="1.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
|
||||
<SubComponents>
|
||||
@ -106,6 +87,11 @@
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="refreshButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="13" weightX="1.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="closeButton">
|
||||
<Properties>
|
||||
@ -116,11 +102,21 @@
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="closeButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="1" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="6" insetsLeft="6" insetsBottom="11" insetsRight="10" anchor="13" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Container class="javax.swing.JScrollPane" name="moduleScrollPane">
|
||||
<AuxValues>
|
||||
<AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="2" gridWidth="2" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="6" insetsLeft="10" insetsBottom="0" insetsRight="10" anchor="18" weightX="1.0" weightY="1.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
|
||||
<SubComponents>
|
||||
|
@ -361,6 +361,7 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
@SuppressWarnings("unchecked")
|
||||
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
|
||||
private void initComponents() {
|
||||
java.awt.GridBagConstraints gridBagConstraints;
|
||||
|
||||
snapshotsScrollPane = new javax.swing.JScrollPane();
|
||||
threadActivitySnapshotsTable = new javax.swing.JTable();
|
||||
@ -371,6 +372,10 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
moduleScrollPane = new javax.swing.JScrollPane();
|
||||
moduleTable = new javax.swing.JTable();
|
||||
|
||||
setMinimumSize(new java.awt.Dimension(500, 500));
|
||||
setPreferredSize(new java.awt.Dimension(600, 500));
|
||||
setLayout(new java.awt.GridBagLayout());
|
||||
|
||||
threadActivitySnapshotsTable.setModel(new javax.swing.table.DefaultTableModel(
|
||||
new Object [][] {
|
||||
|
||||
@ -381,6 +386,17 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
));
|
||||
snapshotsScrollPane.setViewportView(threadActivitySnapshotsTable);
|
||||
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 0;
|
||||
gridBagConstraints.gridwidth = 2;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
|
||||
gridBagConstraints.weightx = 1.0;
|
||||
gridBagConstraints.weighty = 1.0;
|
||||
gridBagConstraints.insets = new java.awt.Insets(11, 10, 0, 10);
|
||||
add(snapshotsScrollPane, gridBagConstraints);
|
||||
|
||||
jobTable.setModel(new javax.swing.table.DefaultTableModel(
|
||||
new Object [][] {
|
||||
|
||||
@ -391,12 +407,29 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
));
|
||||
jobScrollPane.setViewportView(jobTable);
|
||||
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 1;
|
||||
gridBagConstraints.gridwidth = 2;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
|
||||
gridBagConstraints.weightx = 1.0;
|
||||
gridBagConstraints.weighty = 1.0;
|
||||
gridBagConstraints.insets = new java.awt.Insets(6, 10, 0, 10);
|
||||
add(jobScrollPane, gridBagConstraints);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(IngestProgressSnapshotPanel.class, "IngestProgressSnapshotPanel.refreshButton.text")); // NOI18N
|
||||
refreshButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
refreshButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 3;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST;
|
||||
gridBagConstraints.weightx = 1.0;
|
||||
add(refreshButton, gridBagConstraints);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(IngestProgressSnapshotPanel.class, "IngestProgressSnapshotPanel.closeButton.text")); // NOI18N
|
||||
closeButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
@ -404,6 +437,12 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
closeButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 1;
|
||||
gridBagConstraints.gridy = 3;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST;
|
||||
gridBagConstraints.insets = new java.awt.Insets(6, 6, 11, 10);
|
||||
add(closeButton, gridBagConstraints);
|
||||
|
||||
moduleTable.setModel(new javax.swing.table.DefaultTableModel(
|
||||
new Object [][] {
|
||||
@ -415,44 +454,16 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel {
|
||||
));
|
||||
moduleScrollPane.setViewportView(moduleTable);
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
|
||||
.addComponent(snapshotsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGap(0, 0, Short.MAX_VALUE)
|
||||
.addComponent(refreshButton)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(closeButton))
|
||||
.addComponent(jobScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE)
|
||||
.addComponent(moduleScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE))
|
||||
.addContainerGap())
|
||||
);
|
||||
|
||||
layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {closeButton, refreshButton});
|
||||
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(snapshotsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 102, Short.MAX_VALUE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(jobScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 102, Short.MAX_VALUE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(moduleScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 100, Short.MAX_VALUE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(refreshButton)
|
||||
.addComponent(closeButton))
|
||||
.addContainerGap())
|
||||
);
|
||||
|
||||
layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {closeButton, refreshButton});
|
||||
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 2;
|
||||
gridBagConstraints.gridwidth = 2;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
|
||||
gridBagConstraints.weightx = 1.0;
|
||||
gridBagConstraints.weighty = 1.0;
|
||||
gridBagConstraints.insets = new java.awt.Insets(6, 10, 0, 10);
|
||||
add(moduleScrollPane, gridBagConstraints);
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
|
||||
private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed
|
||||
|
@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<Form version="1.3" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JDialogFormInfo">
|
||||
<Properties>
|
||||
<Property name="defaultCloseOperation" type="int" value="2"/>
|
||||
</Properties>
|
||||
<SyntheticProperties>
|
||||
<SyntheticProperty name="formSizePolicy" type="int" value="1"/>
|
||||
<SyntheticProperty name="generateCenter" type="boolean" value="false"/>
|
||||
</SyntheticProperties>
|
||||
<AuxValues>
|
||||
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
|
||||
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout>
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
<Component id="saveButton" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="cancelButton" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="validationLabel" alignment="0" max="32767" attributes="0"/>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="domainSuffixLabel" alignment="0" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="categoryLabel" alignment="0" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="categoryTextField" pref="276" max="32767" attributes="0"/>
|
||||
<Component id="domainSuffixTextField" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="domainSuffixTextField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="domainSuffixLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="categoryTextField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="categoryLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="validationLabel" min="-2" pref="46" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="cancelButton" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="saveButton" alignment="0" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace pref="8" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
</Layout>
|
||||
<SubComponents>
|
||||
<Component class="javax.swing.JTextField" name="categoryTextField">
|
||||
</Component>
|
||||
<Component class="javax.swing.JTextField" name="domainSuffixTextField">
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="categoryLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="AddEditCategoryDialog.categoryLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="domainSuffixLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="AddEditCategoryDialog.domainSuffixLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="validationLabel">
|
||||
<Properties>
|
||||
<Property name="foreground" type="java.awt.Color" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
|
||||
<Connection code="java.awt.Color.RED" type="code"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" value=" "/>
|
||||
<Property name="toolTipText" type="java.lang.String" value=""/>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="cancelButton">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="AddEditCategoryDialog.cancelButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="cancelButtonActionPerformed"/>
|
||||
</Events>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="saveButton">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="AddEditCategoryDialog.saveButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="saveButtonActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Form>
|
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.url.analytics.domaincategorization;
|
||||
|
||||
import java.util.Set;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategory;
|
||||
|
||||
/**
|
||||
* Dialog for adding or editing a custom domain suffix category.
|
||||
*/
|
||||
@Messages({
|
||||
"AddEditCategoryDialog_Edit=Edit Entry",
|
||||
"AddEditCategoryDialog_Add=Add Entry"
|
||||
})
|
||||
class AddEditCategoryDialog extends javax.swing.JDialog {
|
||||
|
||||
private boolean changed = false;
|
||||
private final Set<String> currentSuffixes;
|
||||
private final DomainCategory currentDomainCategory;
|
||||
|
||||
// listens for document updates
|
||||
private final DocumentListener updateListener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main constructor if adding a new domain suffix.
|
||||
*
|
||||
* @param parent The parent frame for this dialog.
|
||||
* @param currentSuffixes The current domain suffixes.
|
||||
*/
|
||||
AddEditCategoryDialog(java.awt.Frame parent, Set<String> currentSuffixes) {
|
||||
this(parent, currentSuffixes, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main constructor if editing a domain suffix.
|
||||
*
|
||||
* @param parentThe parent frame for this dialog.
|
||||
* @param currentSuffixes The current domain suffixes.
|
||||
* @param currentDomainCategory The domain category being edited. If null,
|
||||
* it will be assumed that a new domain suffix is being added.
|
||||
*/
|
||||
AddEditCategoryDialog(java.awt.Frame parent, Set<String> currentSuffixes, DomainCategory currentDomainCategory) {
|
||||
super(parent, true);
|
||||
initComponents();
|
||||
this.currentSuffixes = currentSuffixes;
|
||||
this.currentDomainCategory = currentDomainCategory;
|
||||
|
||||
// set title based on whether or not we are editing or adding
|
||||
// also don't allow editing of domain suffix if editing
|
||||
if (currentDomainCategory == null) {
|
||||
setTitle(Bundle.AddEditCategoryDialog_Add());
|
||||
domainSuffixTextField.setEditable(true);
|
||||
domainSuffixTextField.setEnabled(true);
|
||||
onValueUpdate(null, null);
|
||||
} else {
|
||||
setTitle(Bundle.AddEditCategoryDialog_Edit());
|
||||
domainSuffixTextField.setEditable(false);
|
||||
domainSuffixTextField.setEnabled(false);
|
||||
onValueUpdate(currentDomainCategory.getHostSuffix(), currentDomainCategory.getCategory());
|
||||
}
|
||||
|
||||
validationLabel.setText("");
|
||||
|
||||
categoryTextField.getDocument().addDocumentListener(updateListener);
|
||||
domainSuffixTextField.getDocument().addDocumentListener(updateListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for the name in the input field if Ok pressed or
|
||||
* null if not.
|
||||
*
|
||||
* @return The string value for the name in the input field if Ok pressed or
|
||||
* null if not.
|
||||
*/
|
||||
DomainCategory getValue() {
|
||||
return new DomainCategory(domainSuffixTextField.getText(), categoryTextField.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the value has been changed and saved by the user.
|
||||
*
|
||||
* @return Whether or not the value has been changed and saved by the user.
|
||||
*/
|
||||
boolean isChanged() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the text field is updated, this method is called.
|
||||
*
|
||||
* @param suffixStr The current domain suffix string in the input.
|
||||
* @param categoryStr The current category string in the input.
|
||||
*/
|
||||
@Messages({
|
||||
"# {0} - maxSuffixLen",
|
||||
"AddEditCategoryDialog_onValueUpdate_badSuffix=Please provide a domain suffix that is no more than {0} characters that includes at least one period.",
|
||||
"# {0} - maxCategoryLen",
|
||||
"AddEditCategoryDialog_onValueUpdate_badCategory=Please provide a category that is no more than {0} characters.",
|
||||
"AddEditCategoryDialog_onValueUpdate_suffixRepeat=Please provide a unique domain suffix.",
|
||||
"AddEditCategoryDialog_onValueUpdate_sameCategory=Please provide a new category for this domain suffix.",})
|
||||
void onValueUpdate(String suffixStr, String categoryStr) {
|
||||
|
||||
String safeSuffixStr = suffixStr == null ? "" : suffixStr;
|
||||
String normalizedSuffix = WebCategoriesDataModel.getNormalizedSuffix(safeSuffixStr);
|
||||
String safeCategoryStr = categoryStr == null ? "" : categoryStr;
|
||||
String normalizedCategory = WebCategoriesDataModel.getNormalizedCategory(safeCategoryStr);
|
||||
|
||||
// update input text field if it is not the same.
|
||||
if (!safeCategoryStr.equals(categoryTextField.getText())) {
|
||||
categoryTextField.setText(safeCategoryStr);
|
||||
}
|
||||
|
||||
if (!safeSuffixStr.equals(domainSuffixTextField.getText())) {
|
||||
domainSuffixTextField.setText(safeSuffixStr);
|
||||
}
|
||||
|
||||
String validationMessage = null;
|
||||
if (normalizedSuffix.length() == 0
|
||||
|| normalizedSuffix.length() > WebCategoriesDataModel.getMaxDomainSuffixLength()
|
||||
|| normalizedSuffix.indexOf('.') < 0) {
|
||||
|
||||
validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_badSuffix(WebCategoriesDataModel.getMaxCategoryLength());
|
||||
|
||||
} else if (normalizedCategory.length() == 0 || normalizedCategory.length() > WebCategoriesDataModel.getMaxCategoryLength()) {
|
||||
validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_badCategory(WebCategoriesDataModel.getMaxCategoryLength());
|
||||
|
||||
} else if (currentSuffixes.contains(normalizedSuffix)
|
||||
&& (currentDomainCategory == null
|
||||
|| !normalizedSuffix.equals(currentDomainCategory.getHostSuffix()))) {
|
||||
|
||||
validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_suffixRepeat();
|
||||
|
||||
} else if (currentDomainCategory != null
|
||||
&& currentDomainCategory.getCategory() != null
|
||||
&& normalizedCategory.equals(WebCategoriesDataModel.getNormalizedCategory(currentDomainCategory.getCategory()))) {
|
||||
|
||||
validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_sameCategory();
|
||||
}
|
||||
|
||||
saveButton.setEnabled(validationMessage == null);
|
||||
validationLabel.setText(validationMessage == null ? "" : String.format("<html>%s</html>", validationMessage));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
|
||||
private void initComponents() {
|
||||
|
||||
categoryTextField = new javax.swing.JTextField();
|
||||
domainSuffixTextField = new javax.swing.JTextField();
|
||||
javax.swing.JLabel categoryLabel = new javax.swing.JLabel();
|
||||
javax.swing.JLabel domainSuffixLabel = new javax.swing.JLabel();
|
||||
validationLabel = new javax.swing.JLabel();
|
||||
javax.swing.JButton cancelButton = new javax.swing.JButton();
|
||||
saveButton = new javax.swing.JButton();
|
||||
|
||||
setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
categoryLabel.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.categoryLabel.text")); // NOI18N
|
||||
|
||||
domainSuffixLabel.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.domainSuffixLabel.text")); // NOI18N
|
||||
|
||||
validationLabel.setForeground(java.awt.Color.RED);
|
||||
validationLabel.setText(" ");
|
||||
validationLabel.setToolTipText("");
|
||||
|
||||
cancelButton.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.cancelButton.text")); // NOI18N
|
||||
cancelButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
cancelButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
saveButton.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.saveButton.text")); // NOI18N
|
||||
saveButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
saveButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
|
||||
getContentPane().setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||
.addGap(0, 0, Short.MAX_VALUE)
|
||||
.addComponent(saveButton)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(cancelButton))
|
||||
.addComponent(validationLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(domainSuffixLabel)
|
||||
.addComponent(categoryLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(categoryTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 276, Short.MAX_VALUE)
|
||||
.addComponent(domainSuffixTextField))))
|
||||
.addContainerGap())
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(domainSuffixTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(domainSuffixLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(categoryTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(categoryLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(validationLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 46, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(cancelButton)
|
||||
.addComponent(saveButton))
|
||||
.addContainerGap(8, Short.MAX_VALUE))
|
||||
);
|
||||
|
||||
pack();
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
|
||||
private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed
|
||||
this.changed = true;
|
||||
dispose();
|
||||
}//GEN-LAST:event_saveButtonActionPerformed
|
||||
|
||||
private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed
|
||||
this.changed = false;
|
||||
dispose();
|
||||
}//GEN-LAST:event_cancelButtonActionPerformed
|
||||
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JTextField categoryTextField;
|
||||
private javax.swing.JTextField domainSuffixTextField;
|
||||
private javax.swing.JButton saveButton;
|
||||
private javax.swing.JLabel validationLabel;
|
||||
// End of variables declaration//GEN-END:variables
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
WebCategoryOptionsController_title=Custom Web Categories
|
||||
WebCategoryOptionsController_keywords=Custom Web Categories
|
||||
AddEditCategoryDialog.categoryLabel.text=Category:
|
||||
AddEditCategoryDialog.domainSuffixLabel.text=Domain Suffix:
|
||||
AddEditCategoryDialog.saveButton.text=Save
|
||||
AddEditCategoryDialog.cancelButton.text=Cancel
|
||||
WebCategoriesOptionsPanel.panelDescription.text=This module allows you to classify web sites based on domain names.
|
||||
WebCategoriesOptionsPanel.categoriesTitle.text=Categories:
|
||||
WebCategoriesOptionsPanel.newEntryButton.text=New Entry
|
||||
WebCategoriesOptionsPanel.editEntryButton.text=Edit Entry
|
||||
WebCategoriesOptionsPanel.deleteEntryButton.text=Delete Entry
|
||||
WebCategoriesOptionsPanel.importSetButton.text=Import Set
|
||||
WebCategoriesOptionsPanel.exportSetButton.text=Export Set
|
||||
WebCategoriesOptionsPanel.ingestRunningWarning.text=Ingest is currently running. No editing can take place at this time.
|
@ -0,0 +1,37 @@
|
||||
AddEditCategoryDialog_Add=Add Entry
|
||||
AddEditCategoryDialog_Edit=Edit Entry
|
||||
# {0} - maxCategoryLen
|
||||
AddEditCategoryDialog_onValueUpdate_badCategory=Please provide a category that is no more than {0} characters.
|
||||
# {0} - maxSuffixLen
|
||||
AddEditCategoryDialog_onValueUpdate_badSuffix=Please provide a domain suffix that is no more than {0} characters that includes at least one period.
|
||||
AddEditCategoryDialog_onValueUpdate_sameCategory=Please provide a new category for this domain suffix.
|
||||
AddEditCategoryDialog_onValueUpdate_suffixRepeat=Please provide a unique domain suffix.
|
||||
WebCategoriesOptionsPanel_categoryTable_categoryColumnName=Category
|
||||
WebCategoriesOptionsPanel_categoryTable_suffixColumnName=Domain Suffix
|
||||
WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName=Custom Categories Export
|
||||
WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage=A file already exists at the selected path. The categories will not be exported.
|
||||
WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle=File Already Exists
|
||||
WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorMessage=There was an error exporting.
|
||||
WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorTitle=Export Error
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage=There was an error importing this json file.
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle=Import Error
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel=Cancel
|
||||
# {0} - domainSuffix
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage=Domain suffix {0} already exists. What would you like to do?
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite=Overwrite
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip=Skip
|
||||
WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle=Domain Suffix Already Exists
|
||||
WebCategoryOptionsController_title=Custom Web Categories
|
||||
WebCategoryOptionsController_keywords=Custom Web Categories
|
||||
AddEditCategoryDialog.categoryLabel.text=Category:
|
||||
AddEditCategoryDialog.domainSuffixLabel.text=Domain Suffix:
|
||||
AddEditCategoryDialog.saveButton.text=Save
|
||||
AddEditCategoryDialog.cancelButton.text=Cancel
|
||||
WebCategoriesOptionsPanel.panelDescription.text=This module allows you to classify web sites based on domain names.
|
||||
WebCategoriesOptionsPanel.categoriesTitle.text=Categories:
|
||||
WebCategoriesOptionsPanel.newEntryButton.text=New Entry
|
||||
WebCategoriesOptionsPanel.editEntryButton.text=Edit Entry
|
||||
WebCategoriesOptionsPanel.deleteEntryButton.text=Delete Entry
|
||||
WebCategoriesOptionsPanel.importSetButton.text=Import Set
|
||||
WebCategoriesOptionsPanel.exportSetButton.text=Export Set
|
||||
WebCategoriesOptionsPanel.ingestRunningWarning.text=Ingest is currently running. No editing can take place at this time.
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.url.analytics.domaincategorization;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Level;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openide.util.lookup.ServiceProvider;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategorizer;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategorizerException;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategory;
|
||||
|
||||
/**
|
||||
* A DomainCategoryProvider for custom web categories. NOTE: If this class
|
||||
* package or name change, code in DomainCategoryRunner will also need to change
|
||||
* to reflect the changing class name for ordering purposes.
|
||||
*/
|
||||
@ServiceProvider(service = DomainCategorizer.class)
|
||||
public class CustomWebCategorizer implements DomainCategorizer {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(CustomWebCategorizer.class.getName());
|
||||
|
||||
private final WebCategoriesDataModel dataModel;
|
||||
|
||||
/**
|
||||
* Constructor accepting a custom WebCategoriesDataModel.
|
||||
*
|
||||
* @param dataModel The WebCategoriesDataModel to use as a data model.
|
||||
*/
|
||||
CustomWebCategorizer(WebCategoriesDataModel dataModel) {
|
||||
this.dataModel = dataModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* No parameter constructor that uses the singleton instance of the
|
||||
* WebCategoriesDataModel.
|
||||
*
|
||||
*/
|
||||
public CustomWebCategorizer() {
|
||||
this(WebCategoriesDataModel.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DomainCategory getCategory(String domain, String host) throws DomainCategorizerException {
|
||||
if (!dataModel.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
String hostToUse = (StringUtils.isBlank(host)) ? domain : host;
|
||||
if (StringUtils.isBlank(hostToUse)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
hostToUse = hostToUse.toLowerCase();
|
||||
|
||||
try {
|
||||
return dataModel.getMatchingRecord(hostToUse);
|
||||
} catch (SQLException ex) {
|
||||
logger.log(Level.WARNING, "There was an error while retrieving data for: " + hostToUse, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() throws DomainCategorizerException {
|
||||
try {
|
||||
dataModel.initialize();
|
||||
} catch (SQLException ex) {
|
||||
throw new DomainCategorizerException("Unable to initialize.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws SQLException {
|
||||
dataModel.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,535 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.url.analytics.domaincategorization;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openide.modules.InstalledFileLocator;
|
||||
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategory;
|
||||
|
||||
/**
|
||||
* Provides the data model for exporting, importing and CRUD operations on
|
||||
* custom web categories.
|
||||
*/
|
||||
class WebCategoriesDataModel implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* DTO to be used with jackson when converting to and from exported content.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class CustomCategorizationJsonDto {
|
||||
|
||||
private final String category;
|
||||
private final List<String> domains;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param category The category.
|
||||
* @param domains The list of host suffixes in this category.
|
||||
*/
|
||||
@JsonCreator
|
||||
CustomCategorizationJsonDto(
|
||||
@JsonProperty("category") String category,
|
||||
@JsonProperty("domains") List<String> domains) {
|
||||
this.category = category;
|
||||
this.domains = domains == null
|
||||
? Collections.emptyList()
|
||||
: new ArrayList<>(domains);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category.
|
||||
*
|
||||
* @return The category.
|
||||
*/
|
||||
@JsonGetter("category")
|
||||
String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of domain suffixes in this category.
|
||||
*
|
||||
* @return The list of domain suffixes in this category.
|
||||
*/
|
||||
@JsonGetter("domains")
|
||||
List<String> getDomains() {
|
||||
return domains;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MAX_CAT_SIZE = 300;
|
||||
private static final int MAX_DOMAIN_SIZE = 255;
|
||||
|
||||
private static final String ROOT_FOLDER = "DomainCategorization";
|
||||
private static final String FILE_REL_PATH = "custom_list.db";
|
||||
private static final String JDBC_SQLITE_PREFIX = "jdbc:sqlite:";
|
||||
private static final String TABLE_NAME = "domain_suffix";
|
||||
private static final String SUFFIX_COLUMN = "suffix";
|
||||
private static final String CATEGORY_COLUMN = "category";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebCategoriesDataModel.class.getName());
|
||||
private static WebCategoriesDataModel instance;
|
||||
|
||||
/**
|
||||
* Returns the maximum string length of a domain suffix.
|
||||
*
|
||||
* @return The maximum string length of a domain suffix.
|
||||
*/
|
||||
static int getMaxDomainSuffixLength() {
|
||||
return MAX_DOMAIN_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum string length of a category.
|
||||
*
|
||||
* @return The maximum string length of a category.
|
||||
*/
|
||||
static int getMaxCategoryLength() {
|
||||
return MAX_DOMAIN_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default path for where custom domain categorization exists.
|
||||
*
|
||||
* @return The path or null if the path cannot be reconciled.
|
||||
*/
|
||||
private static File getDefaultPath() {
|
||||
File dir = InstalledFileLocator.getDefault().locate(ROOT_FOLDER, WebCategoriesDataModel.class.getPackage().getName(), false);
|
||||
if (dir == null || !dir.exists()) {
|
||||
logger.log(Level.WARNING, String.format("Unable to find file %s with InstalledFileLocator", ROOT_FOLDER));
|
||||
return null;
|
||||
}
|
||||
|
||||
return Paths.get(dir.getAbsolutePath(), FILE_REL_PATH).toFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the normalized category string to be inserted into the
|
||||
* database.
|
||||
*
|
||||
* @param category The category.
|
||||
* @return The normalized string.
|
||||
*/
|
||||
static String getNormalizedCategory(String category) {
|
||||
if (category == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String trimmedCategory = category.trim();
|
||||
|
||||
return trimmedCategory.substring(0, Math.min(trimmedCategory.length(), MAX_CAT_SIZE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the normalized domain suffix string to be inserted into the
|
||||
* database.
|
||||
*
|
||||
* @param domainSuffix The domain suffix.
|
||||
* @return The normalized string.
|
||||
*/
|
||||
static String getNormalizedSuffix(String domainSuffix) {
|
||||
if (domainSuffix == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String sanitized = Stream.of(domainSuffix.split("\\."))
|
||||
.map(s -> {
|
||||
return s
|
||||
// alphanumeric and hyphen
|
||||
.replaceAll("[^0-9a-zA-Z\\-]", "")
|
||||
// no leading or trailing hyphen
|
||||
.replaceAll("^\\-*(.+?)?\\-*$", "$1");
|
||||
})
|
||||
.filter(StringUtils::isNotEmpty)
|
||||
.collect(Collectors.joining("."));
|
||||
|
||||
return sanitized.substring(0, Math.min(sanitized.length(), MAX_DOMAIN_SIZE)).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a singleton instance of this class.
|
||||
*
|
||||
* @return The singleton instance of this class.
|
||||
*/
|
||||
static WebCategoriesDataModel getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new WebCategoriesDataModel();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final File sqlitePath;
|
||||
private Connection dbConn = null;
|
||||
|
||||
/**
|
||||
* Constructor used to create singleton instance.
|
||||
*/
|
||||
private WebCategoriesDataModel() {
|
||||
this(getDefaultPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that accepts a variable path for the custom sqlite database
|
||||
* for custom domain categories.
|
||||
*
|
||||
* @param sqlitePath The path.
|
||||
*/
|
||||
WebCategoriesDataModel(File sqlitePath) {
|
||||
this.sqlitePath = sqlitePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sqlite jdbc connection.
|
||||
*
|
||||
* @throws SQLException
|
||||
*/
|
||||
synchronized void initialize() throws SQLException {
|
||||
String url = JDBC_SQLITE_PREFIX + sqlitePath.getAbsolutePath();
|
||||
if (this.dbConn != null) {
|
||||
this.dbConn.close();
|
||||
this.dbConn = null;
|
||||
}
|
||||
|
||||
this.dbConn = DriverManager.getConnection(url);
|
||||
|
||||
// speed up operations by turning off WAL
|
||||
try (Statement turnOffWal = dbConn.createStatement()) {
|
||||
turnOffWal.execute("PRAGMA journal_mode=OFF");
|
||||
}
|
||||
|
||||
// create table if it doesn't exist
|
||||
try (Statement createDomainsTable = dbConn.createStatement()) {
|
||||
createDomainsTable.execute(
|
||||
" CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (\n"
|
||||
+ " " + SUFFIX_COLUMN + " VARCHAR(" + MAX_DOMAIN_SIZE + ") PRIMARY KEY,\n"
|
||||
+ " " + CATEGORY_COLUMN + " VARCHAR(" + MAX_CAT_SIZE + ")\n"
|
||||
+ " ) WITHOUT ROWID");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if initialized.
|
||||
*
|
||||
* @return True if initialized.
|
||||
*/
|
||||
synchronized boolean isInitialized() {
|
||||
return this.dbConn != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all domain categories present in json file.
|
||||
*
|
||||
* @param jsonInput The json file.
|
||||
* @return The domain categories.
|
||||
* @throws IOException
|
||||
*/
|
||||
List<DomainCategory> getJsonEntries(File jsonInput) throws IOException {
|
||||
if (jsonInput == null) {
|
||||
logger.log(Level.WARNING, "No valid file provided.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
List<CustomCategorizationJsonDto> customCategorizations = mapper.readValue(jsonInput, new TypeReference<List<CustomCategorizationJsonDto>>() {
|
||||
});
|
||||
|
||||
Stream<CustomCategorizationJsonDto> categoryStream = (customCategorizations != null) ? customCategorizations.stream() : Stream.empty();
|
||||
|
||||
return categoryStream
|
||||
.filter(c -> c != null && c.getCategory() != null && c.getDomains() != null)
|
||||
.flatMap(c -> c.getDomains().stream()
|
||||
.map(WebCategoriesDataModel::getNormalizedSuffix)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(d -> new DomainCategory(d, getNormalizedCategory(c.getCategory()))))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports current database to a json file.
|
||||
*
|
||||
* @param jsonOutput The output file.
|
||||
* @throws SQLException
|
||||
* @throws IOException
|
||||
*/
|
||||
synchronized void exportToJson(File jsonOutput) throws SQLException, IOException {
|
||||
if (jsonOutput == null) {
|
||||
logger.log(Level.WARNING, "Null file provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
// retrieve items from the database
|
||||
List<Pair<String, String>> categoryDomains = new ArrayList<>();
|
||||
try (Statement domainSelect = dbConn.createStatement();
|
||||
ResultSet resultSet = domainSelect.executeQuery(
|
||||
"SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME + " ORDER BY " + SUFFIX_COLUMN)) {
|
||||
|
||||
while (resultSet.next()) {
|
||||
categoryDomains.add(Pair.of(resultSet.getString(CATEGORY_COLUMN), resultSet.getString(SUFFIX_COLUMN)));
|
||||
}
|
||||
}
|
||||
|
||||
// aggregate data appropriately into CustomCategorizationJsonDto
|
||||
List<CustomCategorizationJsonDto> categories
|
||||
= categoryDomains.stream()
|
||||
.collect(Collectors.toMap(
|
||||
p -> p.getKey(),
|
||||
p -> new ArrayList<>(Arrays.asList(p.getValue())),
|
||||
(p1, p2) -> {
|
||||
p1.addAll(p2);
|
||||
return p1;
|
||||
}
|
||||
))
|
||||
.entrySet().stream()
|
||||
.map(entry -> new CustomCategorizationJsonDto(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// write to disk
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(jsonOutput, categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a record from the database.
|
||||
*
|
||||
* @param domainSuffix The domain suffix of the item to delete.
|
||||
* @return Whether or not the operation actually deleted something.
|
||||
* @throws SQLException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
synchronized boolean deleteRecord(String domainSuffix) throws SQLException, IllegalArgumentException {
|
||||
if (StringUtils.isBlank(domainSuffix)) {
|
||||
throw new IllegalArgumentException("Expected non-empty domain suffix");
|
||||
}
|
||||
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
try (PreparedStatement suffixDelete = dbConn.prepareStatement(
|
||||
"DELETE FROM " + TABLE_NAME + " WHERE LOWER(" + SUFFIX_COLUMN + ") = LOWER(?)", Statement.RETURN_GENERATED_KEYS);) {
|
||||
|
||||
suffixDelete.setString(1, getNormalizedSuffix(domainSuffix));
|
||||
return suffixDelete.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates the entry for the given domain suffix.
|
||||
*
|
||||
* @param entry The domain suffix and category.
|
||||
* @return True if successfully inserted/updated.
|
||||
* @throws SQLException
|
||||
* @throws IllegalStateException
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
synchronized boolean insertUpdateSuffix(DomainCategory entry) throws SQLException, IllegalStateException, IllegalArgumentException {
|
||||
if (entry == null || StringUtils.isBlank(getNormalizedCategory(entry.getCategory())) || StringUtils.isBlank(getNormalizedSuffix(entry.getHostSuffix()))) {
|
||||
throw new IllegalArgumentException("Expected non-empty, valid category and domain suffix.");
|
||||
}
|
||||
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
try (PreparedStatement insertUpdate = dbConn.prepareStatement(
|
||||
"INSERT OR REPLACE INTO " + TABLE_NAME + "(" + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + ") VALUES (?, ?)",
|
||||
Statement.RETURN_GENERATED_KEYS)) {
|
||||
|
||||
insertUpdate.setString(1, getNormalizedSuffix(entry.getHostSuffix()));
|
||||
insertUpdate.setString(2, getNormalizedCategory(entry.getCategory()));
|
||||
return insertUpdate.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all records in the database.
|
||||
*
|
||||
* @return The list of domain suffixes and their categories.
|
||||
* @throws SQLException
|
||||
*/
|
||||
List<DomainCategory> getRecords() throws SQLException {
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
List<DomainCategory> entries = new ArrayList<>();
|
||||
|
||||
try (Statement domainSelect = dbConn.createStatement();
|
||||
ResultSet resultSet = domainSelect.executeQuery(
|
||||
"SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME + " ORDER BY " + SUFFIX_COLUMN)) {
|
||||
|
||||
while (resultSet.next()) {
|
||||
entries.add(new DomainCategory(
|
||||
resultSet.getString(SUFFIX_COLUMN),
|
||||
resultSet.getString(CATEGORY_COLUMN)));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
|
||||
}
|
||||
|
||||
private static final String GET_DOMAIN_SUFFIX_QUERY
|
||||
= "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN
|
||||
+ " FROM " + TABLE_NAME + " WHERE " + SUFFIX_COLUMN + " = ?";
|
||||
|
||||
/**
|
||||
* Return the matching domain suffix or null if none found.
|
||||
*
|
||||
* @param domainSuffix The domain suffix.
|
||||
* @return The found entry or null.
|
||||
* @throws SQLException
|
||||
*/
|
||||
DomainCategory getRecordBySuffix(String domainSuffix) throws SQLException {
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
try (PreparedStatement domainSelect = dbConn.prepareStatement(GET_DOMAIN_SUFFIX_QUERY)) {
|
||||
domainSelect.setString(1, domainSuffix);
|
||||
|
||||
try (ResultSet resultSet = domainSelect.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
return new DomainCategory(
|
||||
resultSet.getString(SUFFIX_COLUMN),
|
||||
resultSet.getString(CATEGORY_COLUMN));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the suffix and category from the main table and gets the longest matching suffix.
|
||||
private static final String BASE_QUERY_FMT_STR
|
||||
= "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME
|
||||
+ " WHERE suffix IN (%s) ORDER BY LENGTH(" + SUFFIX_COLUMN + ") DESC LIMIT 1";
|
||||
|
||||
/**
|
||||
* Retrieves the longest matching domain suffix and category matching the
|
||||
* list of suffixes or null if no item can be found.
|
||||
*
|
||||
* @param suffixes The list of suffixes.
|
||||
* @return The longest matching entry or null if no entry found.
|
||||
* @throws SQLException
|
||||
*/
|
||||
synchronized DomainCategory getLongestSuffixRecord(List<String> suffixes) throws SQLException {
|
||||
if (suffixes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
String questionMarks = IntStream.range(0, suffixes.size())
|
||||
.mapToObj((num) -> "?")
|
||||
.collect(Collectors.joining(","));
|
||||
|
||||
try (PreparedStatement stmt = dbConn.prepareStatement(String.format(BASE_QUERY_FMT_STR, questionMarks))) {
|
||||
for (int i = 0; i < suffixes.size(); i++) {
|
||||
stmt.setString(i + 1, suffixes.get(i));
|
||||
}
|
||||
|
||||
try (ResultSet resultSet = stmt.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
String suffix = resultSet.getString(SUFFIX_COLUMN);
|
||||
String category = resultSet.getString(CATEGORY_COLUMN);
|
||||
return new DomainCategory(suffix, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the longest matching domain suffix and category matching the
|
||||
* list of suffixes or null if no item can be found.
|
||||
*
|
||||
* @param host The host name.
|
||||
* @return The longest matching entry or null if no entry found.
|
||||
* @throws SQLException
|
||||
*/
|
||||
DomainCategory getMatchingRecord(String host) throws SQLException {
|
||||
return getLongestSuffixRecord(getSuffixes(host));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the possible suffixes that could be tracked. For instance,
|
||||
* if the host was "chatenabled.mail.google.com", the list should be
|
||||
* ["chatenabled.mail.google.com", "mail.google.com", "google.com", "com"].
|
||||
*
|
||||
* @param host The host.
|
||||
* @return The possible suffixes.
|
||||
*/
|
||||
private List<String> getSuffixes(String host) {
|
||||
if (host == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> hostTokens = Arrays.asList(host.split("\\."));
|
||||
List<String> hostSegmentations = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < hostTokens.size(); i++) {
|
||||
String searchString = String.join(".", hostTokens.subList(i, hostTokens.size()));
|
||||
hostSegmentations.add(searchString);
|
||||
}
|
||||
|
||||
return hostSegmentations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws SQLException {
|
||||
dbConn.close();
|
||||
dbConn = null;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.url.analytics.domaincategorization;
|
||||
|
||||
import java.beans.PropertyChangeListener;
|
||||
import javax.swing.JComponent;
|
||||
import org.netbeans.spi.options.OptionsPanelController;
|
||||
import org.openide.util.HelpCtx;
|
||||
import org.openide.util.Lookup;
|
||||
|
||||
/**
|
||||
* The options panel controller that registers and displays the option panel for
|
||||
* custom web categories.
|
||||
*/
|
||||
@OptionsPanelController.TopLevelRegistration(categoryName = "#WebCategoryOptionsController_title",
|
||||
iconBase = "org/sleuthkit/autopsy/images/domain-32.png",
|
||||
position = 21,
|
||||
keywords = "#WebCategoryOptionsController_keywords",
|
||||
keywordsCategory = "Custom Web Categories")
|
||||
public class WebCategoriesOptionsController extends OptionsPanelController {
|
||||
|
||||
private final WebCategoriesDataModel dataModel = WebCategoriesDataModel.getInstance();
|
||||
private final WebCategoriesOptionsPanel panel = new WebCategoriesOptionsPanel(dataModel);
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
panel.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyChanges() {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChanged() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JComponent getComponent(Lookup masterLookup) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpCtx getHelpCtx() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPropertyChangeListener(PropertyChangeListener l) {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePropertyChangeListener(PropertyChangeListener l) {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<Form version="1.8" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
|
||||
<AuxValues>
|
||||
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
|
||||
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
|
||||
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,2,106,0,0,3,37"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
|
||||
<SubComponents>
|
||||
<Component class="javax.swing.JLabel" name="panelDescription">
|
||||
<Properties>
|
||||
<Property name="horizontalAlignment" type="int" value="0"/>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.panelDescription.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
|
||||
<Border info="org.netbeans.modules.form.compat2.border.CompoundBorderInfo">
|
||||
<CompoundBorder>
|
||||
<Border PropertyName="outside" info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo">
|
||||
<EtchetBorder/>
|
||||
</Border>
|
||||
<Border PropertyName="inside" info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo">
|
||||
<EmptyBorder bottom="5" left="5" right="5" top="5"/>
|
||||
</Border>
|
||||
</CompoundBorder>
|
||||
</Border>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="-1" gridY="-1" gridWidth="3" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="10" insetsBottom="10" insetsRight="0" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="categoriesTitle">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.categoriesTitle.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="1" gridWidth="3" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="10" insetsBottom="0" insetsRight="0" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Container class="javax.swing.JPanel" name="categoryTablePanel">
|
||||
<Properties>
|
||||
<Property name="autoscrolls" type="boolean" value="true"/>
|
||||
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[400, 32767]"/>
|
||||
</Property>
|
||||
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[400, 300]"/>
|
||||
</Property>
|
||||
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[400, 600]"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="categoryTable"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="2" gridWidth="3" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="2" insetsLeft="10" insetsBottom="10" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/>
|
||||
</Container>
|
||||
<Component class="javax.swing.JButton" name="newEntryButton">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/add16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.newEntryButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="newEntryButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="10" insetsBottom="5" insetsRight="5" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="editEntryButton">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/edit16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.editEntryButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="editEntryButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="1" gridY="3" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="5" insetsRight="5" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="deleteEntryButton">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/delete16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.deleteEntryButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="deleteEntryButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="2" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="5" insetsRight="5" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="importSetButton">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/import16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.importSetButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="importSetButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="4" gridWidth="1" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="10" insetsBottom="5" insetsRight="5" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="exportSetButton">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/export16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.exportSetButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="exportSetButtonActionPerformed"/>
|
||||
</Events>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="1" gridY="4" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="5" insetsRight="5" anchor="21" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
<Container class="javax.swing.JPanel" name="bottomStrut">
|
||||
<Properties>
|
||||
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[10, 0]"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
|
||||
</AuxValues>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="3" gridY="6" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/>
|
||||
</Container>
|
||||
<Component class="javax.swing.JLabel" name="ingestRunningWarning">
|
||||
<Properties>
|
||||
<Property name="foreground" type="java.awt.Color" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
|
||||
<Connection code="java.awt.Color.RED" type="code"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties" key="WebCategoriesOptionsPanel.ingestRunningWarning.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Constraints>
|
||||
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
|
||||
<GridBagConstraints gridX="0" gridY="5" gridWidth="3" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="10" insetsBottom="10" insetsRight="10" anchor="17" weightX="0.0" weightY="0.0"/>
|
||||
</Constraint>
|
||||
</Constraints>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Form>
|
@ -0,0 +1,576 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2021 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.sleuthkit.autopsy.url.analytics.domaincategorization;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.openide.util.WeakListeners;
|
||||
import org.sleuthkit.autopsy.corecomponents.OptionsPanel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.ColumnModel;
|
||||
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.DefaultCellModel;
|
||||
import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel;
|
||||
import org.sleuthkit.autopsy.ingest.IngestManager;
|
||||
import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel;
|
||||
import org.sleuthkit.autopsy.url.analytics.DomainCategory;
|
||||
|
||||
/**
|
||||
* The options panel displayed for import, export, and CRUD operations on domain
|
||||
* categories.
|
||||
*/
|
||||
@Messages({
|
||||
"WebCategoriesOptionsPanel_categoryTable_suffixColumnName=Domain Suffix",
|
||||
"WebCategoriesOptionsPanel_categoryTable_categoryColumnName=Category",})
|
||||
public class WebCategoriesOptionsPanel extends IngestModuleGlobalSettingsPanel implements OptionsPanel, AutoCloseable {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WebCategoriesOptionsPanel.class.getName());
|
||||
private static final String DEFAULT_EXTENSION = "json";
|
||||
private static final FileNameExtensionFilter DB_FILTER = new FileNameExtensionFilter("JSON", DEFAULT_EXTENSION);
|
||||
|
||||
private final JFileChooser fileChooser = new JFileChooser();
|
||||
private final WebCategoriesDataModel dataModel;
|
||||
|
||||
private final JTablePanel<DomainCategory> categoryTable
|
||||
= JTablePanel.getJTablePanel(Arrays.asList(
|
||||
new ColumnModel<DomainCategory, DefaultCellModel<?>>(
|
||||
Bundle.WebCategoriesOptionsPanel_categoryTable_suffixColumnName(),
|
||||
(domCat) -> new DefaultCellModel<>(domCat.getHostSuffix())
|
||||
.setTooltip(domCat.getHostSuffix()),
|
||||
300
|
||||
),
|
||||
new ColumnModel<>(
|
||||
Bundle.WebCategoriesOptionsPanel_categoryTable_categoryColumnName(),
|
||||
(domCat) -> new DefaultCellModel<>(domCat.getCategory())
|
||||
.setTooltip(domCat.getCategory()),
|
||||
200
|
||||
)
|
||||
)).setKeyFunction((domCat) -> domCat.getHostSuffix());
|
||||
|
||||
private final PropertyChangeListener ingestListener = (evt) -> refreshComponentStates();
|
||||
private final PropertyChangeListener weakIngestListener = WeakListeners.propertyChange(ingestListener, this);
|
||||
private Set<String> domainSuffixes = new HashSet<>();
|
||||
private boolean isRefreshing = false;
|
||||
|
||||
/**
|
||||
* Main constructor.
|
||||
*
|
||||
* @param dataModel The data model that interacts with the database.
|
||||
*/
|
||||
public WebCategoriesOptionsPanel(WebCategoriesDataModel dataModel) {
|
||||
initComponents();
|
||||
this.dataModel = dataModel;
|
||||
|
||||
fileChooser.addChoosableFileFilter(DB_FILTER);
|
||||
fileChooser.setFileFilter(DB_FILTER);
|
||||
categoryTable.setCellListener((evt) -> refreshComponentStates());
|
||||
IngestManager.getInstance().addIngestJobEventListener(weakIngestListener);
|
||||
setDefaultCursor();
|
||||
refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the items selected in the table or null if no selection.
|
||||
*
|
||||
* @return The items selected in the table or null if no selection.
|
||||
*/
|
||||
private List<DomainCategory> getSelected() {
|
||||
return categoryTable.getSelectedItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers swing worker to fetch data and show in table.
|
||||
*/
|
||||
void refresh() {
|
||||
isRefreshing = true;
|
||||
refreshComponentStates();
|
||||
categoryTable.showDefaultLoadingMessage();
|
||||
new DataFetchWorker<Void, List<DomainCategory>>(
|
||||
(noVal) -> this.dataModel.getRecords(),
|
||||
(data) -> onRefreshedData(data),
|
||||
null).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the result of loading the data is returned, this function handles
|
||||
* updating the GUI.
|
||||
*
|
||||
* @param categoriesResult The result of attempting to fetch the data.
|
||||
*/
|
||||
private void onRefreshedData(DataFetchResult<List<DomainCategory>> categoriesResult) {
|
||||
categoryTable.showDataFetchResult(categoriesResult);
|
||||
if (categoriesResult.getResultType() == ResultType.SUCCESS && categoriesResult.getData() != null) {
|
||||
domainSuffixes = categoriesResult.getData().stream()
|
||||
.map((dc) -> dc.getHostSuffix())
|
||||
.collect(Collectors.toSet());
|
||||
} else {
|
||||
domainSuffixes = new HashSet<>();
|
||||
}
|
||||
isRefreshing = false;
|
||||
refreshComponentStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of the components based on whether or not an item is
|
||||
* selected as well as whether or not data is loading or ingest is
|
||||
* happening.
|
||||
*/
|
||||
private void refreshComponentStates() {
|
||||
List<DomainCategory> selectedItems = getSelected();
|
||||
int selectedCount = CollectionUtils.isEmpty(selectedItems) ? 0 : selectedItems.size();
|
||||
boolean isIngestRunning = IngestManager.getInstance().isIngestRunning();
|
||||
boolean operationsPermitted = !isIngestRunning && !isRefreshing;
|
||||
|
||||
deleteEntryButton.setEnabled(selectedCount > 0 && operationsPermitted);
|
||||
editEntryButton.setEnabled(selectedCount == 1 && operationsPermitted);
|
||||
|
||||
newEntryButton.setEnabled(operationsPermitted);
|
||||
exportSetButton.setEnabled(operationsPermitted);
|
||||
importSetButton.setEnabled(operationsPermitted);
|
||||
|
||||
ingestRunningWarning.setVisible(isIngestRunning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the AddEditCategoryDialog to the user and returns the user-inputted
|
||||
* DomainCategory or null if nothing was saved.
|
||||
*
|
||||
* @param original If editing a value, this is the original value being
|
||||
* edited. If adding a new value, this should be null.
|
||||
* @return
|
||||
*/
|
||||
private DomainCategory getAddEditValue(DomainCategory original) {
|
||||
JFrame parent = (this.getRootPane() != null && this.getRootPane().getParent() instanceof JFrame)
|
||||
? (JFrame) this.getRootPane().getParent()
|
||||
: null;
|
||||
|
||||
AddEditCategoryDialog addEditDialog = new AddEditCategoryDialog(parent, domainSuffixes, original);
|
||||
addEditDialog.setResizable(false);
|
||||
addEditDialog.setLocationRelativeTo(parent);
|
||||
addEditDialog.setVisible(true);
|
||||
addEditDialog.toFront();
|
||||
|
||||
if (addEditDialog.isChanged()) {
|
||||
return addEditDialog.getValue();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cursor to waiting.
|
||||
*/
|
||||
private void setWaitingCursor() {
|
||||
SwingUtilities.invokeLater(() -> this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cursor to default.
|
||||
*/
|
||||
private void setDefaultCursor() {
|
||||
SwingUtilities.invokeLater(() -> this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)));
|
||||
}
|
||||
|
||||
/**
|
||||
* An action for updating or altering data in the custom configuration.
|
||||
*/
|
||||
private interface UpdateAction {
|
||||
|
||||
/**
|
||||
* A runnable action to update custom configuration.
|
||||
*
|
||||
* @throws IllegalArgumentException
|
||||
* @throws IOException
|
||||
* @throws SQLException
|
||||
*/
|
||||
void run() throws IllegalArgumentException, IOException, SQLException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an action to update the state of the configuration and runs refresh
|
||||
* when complete.
|
||||
*
|
||||
* @param runnable The runnable action.
|
||||
* @throws IllegalArgumentException
|
||||
* @throws IOException
|
||||
* @throws SQLException
|
||||
*/
|
||||
private void runUpdateAction(UpdateAction runnable) throws IllegalArgumentException, IOException, SQLException {
|
||||
setWaitingCursor();
|
||||
runnable.run();
|
||||
setDefaultCursor();
|
||||
refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
|
||||
private void initComponents() {
|
||||
java.awt.GridBagConstraints gridBagConstraints;
|
||||
|
||||
javax.swing.JLabel panelDescription = new javax.swing.JLabel();
|
||||
javax.swing.JLabel categoriesTitle = new javax.swing.JLabel();
|
||||
javax.swing.JPanel categoryTablePanel = categoryTable;
|
||||
newEntryButton = new javax.swing.JButton();
|
||||
editEntryButton = new javax.swing.JButton();
|
||||
deleteEntryButton = new javax.swing.JButton();
|
||||
importSetButton = new javax.swing.JButton();
|
||||
exportSetButton = new javax.swing.JButton();
|
||||
javax.swing.JPanel bottomStrut = new javax.swing.JPanel();
|
||||
ingestRunningWarning = new javax.swing.JLabel();
|
||||
|
||||
setLayout(new java.awt.GridBagLayout());
|
||||
|
||||
panelDescription.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
|
||||
panelDescription.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.panelDescription.text")); // NOI18N
|
||||
panelDescription.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(), javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5)));
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridwidth = 3;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 0);
|
||||
add(panelDescription, gridBagConstraints);
|
||||
|
||||
categoriesTitle.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.categoriesTitle.text")); // NOI18N
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 1;
|
||||
gridBagConstraints.gridwidth = 3;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 0);
|
||||
add(categoriesTitle, gridBagConstraints);
|
||||
|
||||
categoryTablePanel.setAutoscrolls(true);
|
||||
categoryTablePanel.setMaximumSize(new java.awt.Dimension(400, 32767));
|
||||
categoryTablePanel.setMinimumSize(new java.awt.Dimension(400, 300));
|
||||
categoryTablePanel.setPreferredSize(new java.awt.Dimension(400, 600));
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 2;
|
||||
gridBagConstraints.gridwidth = 3;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.weighty = 1.0;
|
||||
gridBagConstraints.insets = new java.awt.Insets(2, 10, 10, 0);
|
||||
add(categoryTablePanel, gridBagConstraints);
|
||||
|
||||
newEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/add16.png"))); // NOI18N
|
||||
newEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.newEntryButton.text")); // NOI18N
|
||||
newEntryButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
newEntryButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 3;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 5);
|
||||
add(newEntryButton, gridBagConstraints);
|
||||
|
||||
editEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/edit16.png"))); // NOI18N
|
||||
editEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.editEntryButton.text")); // NOI18N
|
||||
editEntryButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
editEntryButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 1;
|
||||
gridBagConstraints.gridy = 3;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5);
|
||||
add(editEntryButton, gridBagConstraints);
|
||||
|
||||
deleteEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/delete16.png"))); // NOI18N
|
||||
deleteEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.deleteEntryButton.text")); // NOI18N
|
||||
deleteEntryButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
deleteEntryButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 2;
|
||||
gridBagConstraints.gridy = 3;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5);
|
||||
add(deleteEntryButton, gridBagConstraints);
|
||||
|
||||
importSetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/import16.png"))); // NOI18N
|
||||
importSetButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.importSetButton.text")); // NOI18N
|
||||
importSetButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
importSetButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 4;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 5);
|
||||
add(importSetButton, gridBagConstraints);
|
||||
|
||||
exportSetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/export16.png"))); // NOI18N
|
||||
exportSetButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.exportSetButton.text")); // NOI18N
|
||||
exportSetButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
exportSetButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 1;
|
||||
gridBagConstraints.gridy = 4;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
|
||||
gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5);
|
||||
add(exportSetButton, gridBagConstraints);
|
||||
|
||||
bottomStrut.setPreferredSize(new java.awt.Dimension(10, 0));
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 3;
|
||||
gridBagConstraints.gridy = 6;
|
||||
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
|
||||
gridBagConstraints.weightx = 1.0;
|
||||
add(bottomStrut, gridBagConstraints);
|
||||
|
||||
ingestRunningWarning.setForeground(java.awt.Color.RED);
|
||||
ingestRunningWarning.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.ingestRunningWarning.text")); // NOI18N
|
||||
gridBagConstraints = new java.awt.GridBagConstraints();
|
||||
gridBagConstraints.gridx = 0;
|
||||
gridBagConstraints.gridy = 5;
|
||||
gridBagConstraints.gridwidth = 3;
|
||||
gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
|
||||
gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10);
|
||||
add(ingestRunningWarning, gridBagConstraints);
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
|
||||
private void deleteEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteEntryButtonActionPerformed
|
||||
List<DomainCategory> selectedItems = getSelected();
|
||||
if (!CollectionUtils.isEmpty(selectedItems)) {
|
||||
setWaitingCursor();
|
||||
for (DomainCategory selected : selectedItems) {
|
||||
if (selected != null && selected.getHostSuffix() != null) {
|
||||
try {
|
||||
dataModel.deleteRecord(selected.getHostSuffix());
|
||||
} catch (IllegalArgumentException | SQLException ex) {
|
||||
logger.log(Level.WARNING, "There was an error while deleting: " + selected.getHostSuffix(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
setDefaultCursor();
|
||||
refresh();
|
||||
}
|
||||
}//GEN-LAST:event_deleteEntryButtonActionPerformed
|
||||
|
||||
private void newEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newEntryButtonActionPerformed
|
||||
DomainCategory newCategory = getAddEditValue(null);
|
||||
if (newCategory != null) {
|
||||
try {
|
||||
runUpdateAction(() -> dataModel.insertUpdateSuffix(newCategory));
|
||||
} catch (IllegalArgumentException | SQLException | IOException ex) {
|
||||
setDefaultCursor();
|
||||
logger.log(Level.WARNING, "There was an error while adding new record: " + newCategory.getHostSuffix(), ex);
|
||||
}
|
||||
}
|
||||
}//GEN-LAST:event_newEntryButtonActionPerformed
|
||||
|
||||
private void editEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editEntryButtonActionPerformed
|
||||
List<DomainCategory> selectedItems = getSelected();
|
||||
if (CollectionUtils.isNotEmpty(selectedItems)) {
|
||||
DomainCategory selected = selectedItems.get(0);
|
||||
if (selected != null && selected.getHostSuffix() != null) {
|
||||
try {
|
||||
DomainCategory newCategory = getAddEditValue(selected);
|
||||
if (newCategory != null) {
|
||||
runUpdateAction(() -> dataModel.insertUpdateSuffix(newCategory));
|
||||
}
|
||||
} catch (IllegalArgumentException | SQLException | IOException ex) {
|
||||
setDefaultCursor();
|
||||
logger.log(Level.WARNING, "There was an error while editing: " + selected.getHostSuffix(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}//GEN-LAST:event_editEntryButtonActionPerformed
|
||||
|
||||
@Messages({
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage=There was an error importing this json file.",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle=Import Error",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle=Domain Suffix Already Exists",
|
||||
"# {0} - domainSuffix",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage=Domain suffix {0} already exists. What would you like to do?",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite=Overwrite",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip=Skip",
|
||||
"WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel=Cancel"})
|
||||
private void importSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importSetButtonActionPerformed
|
||||
fileChooser.setSelectedFile(new File(""));
|
||||
int result = fileChooser.showOpenDialog(this);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
File selectedFile = fileChooser.getSelectedFile();
|
||||
if (selectedFile != null && selectedFile.exists()) {
|
||||
try {
|
||||
runUpdateAction(() -> {
|
||||
List<DomainCategory> categories = dataModel.getJsonEntries(selectedFile);
|
||||
|
||||
for (DomainCategory domcat : categories) {
|
||||
String normalizedCategory = domcat == null ? "" : WebCategoriesDataModel.getNormalizedCategory(domcat.getCategory());
|
||||
String normalizedSuffix = domcat == null ? "" : WebCategoriesDataModel.getNormalizedSuffix(domcat.getHostSuffix());
|
||||
|
||||
if (StringUtils.isBlank(normalizedCategory) || StringUtils.isBlank(normalizedSuffix)) {
|
||||
logger.log(Level.WARNING, String.format("Invalid entry [category: %s, domain suffix: %s]", normalizedCategory, normalizedSuffix));
|
||||
continue;
|
||||
}
|
||||
|
||||
DomainCategory currentCategory = dataModel.getRecordBySuffix(normalizedSuffix);
|
||||
// if a mapping for the domain suffix already exists and the value will change, prompt the user on what to do.
|
||||
if (currentCategory != null) {
|
||||
if (normalizedCategory.equalsIgnoreCase(currentCategory.getCategory())) {
|
||||
// do nothing if import item is same as already present
|
||||
continue;
|
||||
} else {
|
||||
|
||||
String[] options = {
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite(),
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip(),
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel()
|
||||
};
|
||||
|
||||
int optionItem = JOptionPane.showOptionDialog(null,
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage(normalizedSuffix),
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle(),
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
|
||||
|
||||
switch (optionItem) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
continue;
|
||||
case 2:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataModel.insertUpdateSuffix(new DomainCategory(normalizedSuffix, normalizedCategory));
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException | SQLException | IOException ex) {
|
||||
setDefaultCursor();
|
||||
JOptionPane.showMessageDialog(
|
||||
this,
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage(),
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
logger.log(Level.WARNING, "There was an error on import.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}//GEN-LAST:event_importSetButtonActionPerformed
|
||||
|
||||
@Messages({
|
||||
"WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage=A file already exists at the selected path. The categories will not be exported.",
|
||||
"WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle=File Already Exists",
|
||||
"WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorMessage=There was an error exporting.",
|
||||
"WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorTitle=Export Error",
|
||||
"WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName=Custom Categories Export"
|
||||
})
|
||||
private void exportSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSetButtonActionPerformed
|
||||
fileChooser.setSelectedFile(new File(String.format("%s.json", Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName())));
|
||||
int result = fileChooser.showSaveDialog(this);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
File selectedFile = fileChooser.getSelectedFile();
|
||||
if (selectedFile != null) {
|
||||
if (selectedFile.exists()) {
|
||||
JOptionPane.showMessageDialog(
|
||||
this,
|
||||
Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage(),
|
||||
Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setWaitingCursor();
|
||||
dataModel.exportToJson(selectedFile);
|
||||
setDefaultCursor();
|
||||
} catch (SQLException | IOException ex) {
|
||||
setDefaultCursor();
|
||||
JOptionPane.showMessageDialog(
|
||||
this,
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage(),
|
||||
Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
logger.log(Level.WARNING, "There was an error on export.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}//GEN-LAST:event_exportSetButtonActionPerformed
|
||||
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JButton deleteEntryButton;
|
||||
private javax.swing.JButton editEntryButton;
|
||||
private javax.swing.JButton exportSetButton;
|
||||
private javax.swing.JButton importSetButton;
|
||||
private javax.swing.JLabel ingestRunningWarning;
|
||||
private javax.swing.JButton newEntryButton;
|
||||
// End of variables declaration//GEN-END:variables
|
||||
|
||||
@Override
|
||||
public void saveSettings() {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void store() {
|
||||
// NO OP since saves happen whenever there is a change.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
IngestManager.getInstance().removeIngestJobEventListener(weakIngestListener);
|
||||
}
|
||||
}
|
@ -83,6 +83,9 @@ class DomainCategoryRunner extends Extract {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DomainCategoryRunner.class.getName());
|
||||
|
||||
// NOTE: if CustomWebCategorizer ever changes name, this will need to be changed as well.
|
||||
private static final String CUSTOM_CATEGORIZER_PATH = "org.sleuthkit.autopsy.url.analytics.domaincategorization.CustomWebCategorizer";
|
||||
|
||||
/**
|
||||
* Get seconds from epoch from the mapping for the attribute type id.
|
||||
*
|
||||
@ -444,7 +447,16 @@ class DomainCategoryRunner extends Extract {
|
||||
|
||||
List<DomainCategorizer> foundProviders = lookupList.stream()
|
||||
.filter(provider -> provider != null)
|
||||
.sorted((a, b) -> a.getClass().getName().compareToIgnoreCase(b.getClass().getName()))
|
||||
.sorted((a, b) -> {
|
||||
boolean aIsCustom = a.getClass().getName().contains(CUSTOM_CATEGORIZER_PATH);
|
||||
boolean bIsCustom = b.getClass().getName().contains(CUSTOM_CATEGORIZER_PATH);
|
||||
if (aIsCustom != bIsCustom) {
|
||||
// push custom categorizer to top
|
||||
return -Boolean.compare(aIsCustom, bIsCustom);
|
||||
}
|
||||
|
||||
return a.getClass().getName().compareToIgnoreCase(b.getClass().getName());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// add the default categorizer last as a last resort
|
||||
|
1
thirdparty/DomainCategorization/README.txt
vendored
Normal file
1
thirdparty/DomainCategorization/README.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
This is a folder containing information for web domain categorization and custom categories.
|
Loading…
x
Reference in New Issue
Block a user