diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java index 818dc85cdd..35d65db932 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java @@ -47,7 +47,7 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.EventUpdateHandler; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.GuiCellModel.DefaultMenuItem; import org.sleuthkit.autopsy.datasourcesummary.uiutils.GuiCellModel.MenuItem; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index 1e4ce97e7d..4a47bcb092 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -6,7 +6,21 @@ AnalysisPanel_keywordHits_tabName=Keyword Hits AnalysisPanel_keywordSearchModuleName=Keyword Search BaseDataSourceSummaryPanel_goToArtifact=View Source Result BaseDataSourceSummaryPanel_goToFile=View Source File in Directory +ContainerPanel_export_acquisitionDetails=Acquisition Details: +ContainerPanel_export_deviceId=Device ID: +ContainerPanel_export_displayName=Display Name: +ContainerPanel_export_filePaths=File Paths: +ContainerPanel_export_imageType=Image Type: +ContainerPanel_export_md5=MD5: +ContainerPanel_export_originalName=Name: +ContainerPanel_export_sectorSize=Sector Size: +ContainerPanel_export_sha1=SHA1: +ContainerPanel_export_sha256=SHA256: +ContainerPanel_export_size=Size: +ContainerPanel_export_timeZone=Time Zone: +ContainerPanel_export_unallocatedSize=Unallocated Space: ContainerPanel_setFieldsForNonImageDataSource_na=N/A +ContainerPanel_tabName=Container CTL_DataSourceSummaryAction=Data Source Summary DataSourceSummaryDialog.closeButton.text=Close ContainerPanel.displayNameLabel.text=Display Name: @@ -73,6 +87,12 @@ GeolocationPanel_mostCommon_tabName=Most Common Cities GeolocationPanel_mostRecent_tabName=Most Recent Cities GeolocationPanel_onNoCrIngest_message=No results will be shown because the GPX Parser was not run. GeolocationPanel_unknownRow_title=Unknown +IngestJobExcelExport_endTimeColumn=End Time +IngestJobExcelExport_ingestStatusTimeColumn=Ingest Status +IngestJobExcelExport_moduleNameTimeColumn=Module Name +IngestJobExcelExport_sheetName=Ingest History +IngestJobExcelExport_startTimeColumn=Start Time +IngestJobExcelExport_versionColumn=Module Version PastCasesPanel_caseColumn_title=Case PastCasesPanel_countColumn_title=Count PastCasesPanel_notableFileTable_tabName=Cases with Common Notable @@ -87,18 +107,19 @@ RecentFilesPanel_attachmentsTable_tabName=Recent Attachments RecentFilesPanel_col_head_date=Date RecentFilesPanel_docsTable_tabName=Recently Opened Documents RecentFilesPanel_downloadsTable_tabName=Recently Downloads -SizeRepresentationUtil_units_bytes=\ bytes -SizeRepresentationUtil_units_gigabytes=\ GB -SizeRepresentationUtil_units_kilobytes=\ kB -SizeRepresentationUtil_units_megabytes=\ MB -SizeRepresentationUtil_units_petabytes=\ PB -SizeRepresentationUtil_units_terabytes=\ TB +SizeRepresentationUtil_units_bytes=bytes +SizeRepresentationUtil_units_gigabytes=GB +SizeRepresentationUtil_units_kilobytes=KB +SizeRepresentationUtil_units_megabytes=MB +SizeRepresentationUtil_units_petabytes=PB +SizeRepresentationUtil_units_terabytes=TB TimelinePanel_earliestLabel_title=Earliest TimelinePanel_latestLabel_title=Latest TimlinePanel_last30DaysChart_artifactEvts_title=Result Events TimlinePanel_last30DaysChart_fileEvts_title=File Events TimlinePanel_last30DaysChart_title=Last 30 Days TypesPanel_artifactsTypesPieChart_title=Artifact Types +TypesPanel_excelTabName=Types TypesPanel_fileMimeTypesChart_audio_title=Audio TypesPanel_fileMimeTypesChart_documents_title=Documents TypesPanel_fileMimeTypesChart_executables_title=Executables diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java index c19fd2198e..a8e5fef64e 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java @@ -19,21 +19,35 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.beans.PropertyChangeEvent; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; +import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import static org.sleuthkit.autopsy.datasourcesummary.ui.BaseDataSourceSummaryPanel.getFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; 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.DefaultUpdateGovernor; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.KeyValueItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.SingleCellExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.TitledExportable; import org.sleuthkit.autopsy.datasourcesummary.uiutils.UpdateGovernor; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Image; @@ -42,39 +56,184 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Panel to display additional details associated with a specific DataSource */ +@Messages({ + "ContainerPanel_tabName=Container" +}) class ContainerPanel extends BaseDataSourceSummaryPanel { /** - * Data payload for the Container panel. + * View model data for data source images. */ - private static class ContainerPanelData { + private static class ImageViewModel { - private final DataSource dataSource; - private final Long unallocatedFilesSize; + private final long unallocatedSize; + private final long size; + private final long sectorSize; + + private final String timeZone; + private final String imageType; + + private final List paths; + private final String md5Hash; + private final String sha1Hash; + private final String sha256Hash; /** * Main constructor. * - * @param dataSource The original datasource. - * @param unallocatedFilesSize The unallocated file size. + * @param unallocatedSize Size in bytes of unallocated space. + * @param size Total size in bytes. + * @param sectorSize Sector size in bytes. + * @param timeZone The time zone. + * @param imageType The type of image. + * @param paths The source paths for the image. + * @param md5Hash The md5 hash or null. + * @param sha1Hash The sha1 hash or null. + * @param sha256Hash The sha256 hash or null. */ - ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize) { - this.dataSource = dataSource; - this.unallocatedFilesSize = unallocatedFilesSize; + ImageViewModel(long unallocatedSize, long size, long sectorSize, + String timeZone, String imageType, List paths, String md5Hash, + String sha1Hash, String sha256Hash) { + this.unallocatedSize = unallocatedSize; + this.size = size; + this.sectorSize = sectorSize; + this.timeZone = timeZone; + this.imageType = imageType; + this.paths = paths == null ? Collections.emptyList() : new ArrayList<>(paths); + this.md5Hash = md5Hash; + this.sha1Hash = sha1Hash; + this.sha256Hash = sha256Hash; } /** - * @return The original datasource. + * @return Size in bytes of unallocated space. */ - DataSource getDataSource() { - return dataSource; + long getUnallocatedSize() { + return unallocatedSize; } /** - * @return The unallocated file size. + * @return Total size in bytes. */ - Long getUnallocatedFilesSize() { - return unallocatedFilesSize; + long getSize() { + return size; + } + + /** + * @return Sector size in bytes. + */ + long getSectorSize() { + return sectorSize; + } + + /** + * @return The time zone. + */ + String getTimeZone() { + return timeZone; + } + + /** + * @return The type of image. + */ + String getImageType() { + return imageType; + } + + /** + * @return The source paths for the image. + */ + List getPaths() { + return paths; + } + + /** + * @return The md5 hash or null. + */ + String getMd5Hash() { + return md5Hash; + } + + /** + * @return The sha1 hash or null. + */ + String getSha1Hash() { + return sha1Hash; + } + + /** + * @return The sha256 hash or null. + */ + String getSha256Hash() { + return sha256Hash; + } + } + + /** + * View model for container data. + */ + private static class ContainerViewModel { + + private final String displayName; + private final String originalName; + private final String deviceIdValue; + private final String acquisitionDetails; + private final ImageViewModel imageViewModel; + + /** + * Main constructor. + * + * @param displayName The display name for this data source. + * @param originalName The original name for this data source. + * @param deviceIdValue The device id value for this data source. + * @param acquisitionDetails The acquisition details for this data + * source or null. + * @param imageViewModel If the data source is an image, the image view + * model for this data source or null if non-image. + */ + ContainerViewModel(String displayName, String originalName, String deviceIdValue, + String acquisitionDetails, ImageViewModel imageViewModel) { + this.displayName = displayName; + this.originalName = originalName; + this.deviceIdValue = deviceIdValue; + this.acquisitionDetails = acquisitionDetails; + this.imageViewModel = imageViewModel; + } + + /** + * @return The display name for this data source. + */ + String getDisplayName() { + return displayName; + } + + /** + * @return The original name for this data source. + */ + String getOriginalName() { + return originalName; + } + + /** + * @return The device id value for this data source. + */ + String getDeviceId() { + return deviceIdValue; + } + + /** + * @return The acquisition details for this data source or null. + */ + String getAcquisitionDetails() { + return acquisitionDetails; + } + + /** + * @return If the data source is an image, the image view model for this + * data source or null if non-image. + */ + ImageViewModel getImageViewModel() { + return imageViewModel; } } @@ -103,6 +262,7 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { private static final Logger logger = Logger.getLogger(ContainerPanel.class.getName()); private final List> dataFetchComponents; + private final DataFetcher containerDataFetcher; /** * Creates a new form ContainerPanel. @@ -117,21 +277,15 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { ContainerPanel(ContainerSummary containerSummary) { super(containerSummary, CONTAINER_UPDATES); + containerDataFetcher = (dataSource) -> getContainerViewModel(containerSummary, dataSource); + dataFetchComponents = Arrays.asList( new DataFetchComponents<>( - (dataSource) -> { - return new ContainerPanelData( - dataSource, - containerSummary.getSizeOfUnallocatedFiles(dataSource) - ); - }, + containerDataFetcher, (result) -> { if (result != null && result.getResultType() == ResultType.SUCCESS) { - ContainerPanelData data = result.getData(); - DataSource dataSource = (data == null) ? null : data.getDataSource(); - Long unallocatedFileSize = (data == null) ? null : data.getUnallocatedFilesSize(); - - updateDetailsPanelData(dataSource, unallocatedFileSize); + ContainerViewModel data = result.getData(); + updateDetailsPanelData(data); } else { if (result == null) { logger.log(Level.WARNING, "No data fetch result was provided to the ContainerPanel."); @@ -139,8 +293,7 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { logger.log(Level.WARNING, "An exception occurred while attempting to fetch data for the ContainerPanel.", result.getException()); } - - updateDetailsPanelData(null, null); + updateDetailsPanelData(null); } } ) @@ -161,33 +314,113 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { } /** - * Update which DataSource this panel should display details about - * - * @param selectedDataSource the DataSource to display details about. + * A means of retrieving data that could potentially throw an exception. */ - private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize) { + private interface Retriever { + + /** + * Retrieves data of a certain type and possibly throws an exception. + * + * @return The data type. + * @throws TskCoreException + * @throws SleuthkitCaseProviderException + * @throws SQLException + */ + O retrieve() throws TskCoreException, SleuthkitCaseProviderException, SQLException; + } + + /** + * Retrieves data of a particular type and handles any exceptions that may + * be thrown by logging. + * + * @param retriever The retrieving function. + * @return The retrieved data. + */ + private static O retrieve(Retriever retriever) { + try { + return retriever.retrieve(); + } catch (TskCoreException | SleuthkitCaseProviderException | SQLException ex) { + logger.log(Level.WARNING, "Error while retrieving data.", ex); + return null; + } + } + + /** + * Generates a container view model object containing data to display about + * the data source. + * + * @param containerSummary The service providing data about the data source. + * @param ds The data source. + * @return The generated view model. + */ + private static ContainerViewModel getContainerViewModel(ContainerSummary containerSummary, DataSource ds) { + if (ds == null) { + return null; + } + + return new ContainerViewModel( + ds.getName(), + ds.getName(), + ds.getDeviceId(), + retrieve(() -> ds.getAcquisitionDetails()), + ds instanceof Image ? getImageViewModel(containerSummary, (Image) ds) : null + ); + } + + /** + * Generates an image view model object containing data to display about the + * image. + * + * @param containerSummary The service providing data about the image. + * @param image The image. + * @return The generated view model. + */ + private static ImageViewModel getImageViewModel(ContainerSummary containerSummary, Image image) { + if (image == null) { + return null; + } + + Long unallocSize = retrieve(() -> containerSummary.getSizeOfUnallocatedFiles(image)); + String imageType = image.getType().getName(); + Long size = image.getSize(); + Long sectorSize = image.getSsize(); + String timeZone = image.getTimeZone(); + List paths = image.getPaths() == null ? Collections.emptyList() : Arrays.asList(image.getPaths()); + String md5 = retrieve(() -> image.getMd5()); + String sha1 = retrieve(() -> image.getSha1()); + String sha256 = retrieve(() -> image.getSha256()); + + return new ImageViewModel(unallocSize, size, sectorSize, timeZone, imageType, paths, md5, sha1, sha256); + } + + /** + * Update the swing components with fetched data. + * + * @param viewModel The data source view model data. + */ + private void updateDetailsPanelData(ContainerViewModel viewModel) { clearTableValues(); - if (selectedDataSource != null) { - displayNameValue.setText(selectedDataSource.getName()); - originalNameValue.setText(selectedDataSource.getName()); - deviceIdValue.setText(selectedDataSource.getDeviceId()); + if (viewModel == null) { + return; + } - try { - acquisitionDetailsTextArea.setText(selectedDataSource.getAcquisitionDetails()); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get acquisition details for selected data source", ex); - } + displayNameValue.setText(viewModel.getDisplayName()); + originalNameValue.setText(viewModel.getOriginalName()); + deviceIdValue.setText(viewModel.getDeviceId()); + acquisitionDetailsTextArea.setText(viewModel.getAcquisitionDetails()); - if (selectedDataSource instanceof Image) { - setFieldsForImage((Image) selectedDataSource, unallocatedFilesSize); - } else { - setFieldsForNonImageDataSource(); - } + if (viewModel.getImageViewModel() != null) { + setFieldsForImage(viewModel.getImageViewModel()); + } else { + setFieldsForNonImageDataSource(); } this.repaint(); } + /** + * Sets image-only fields to N/A. + */ @Messages({ "ContainerPanel_setFieldsForNonImageDataSource_na=N/A" }) @@ -208,54 +441,24 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { } /** - * Sets text fields for an image. This should be called after - * clearTableValues and before updateFieldVisibility to ensure the proper - * rendering. + * Sets fields for images. * - * @param selectedImage The selected image. - * @param unallocatedFilesSize Unallocated file size in bytes. + * @param viewModel The image view model data. */ - private void setFieldsForImage(Image selectedImage, Long unallocatedFilesSize) { - unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(unallocatedFilesSize)); - imageTypeValue.setText(selectedImage.getType().getName()); - sizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSize())); - sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSsize())); - timeZoneValue.setText(selectedImage.getTimeZone()); + private void setFieldsForImage(ImageViewModel viewModel) { + unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getUnallocatedSize())); + imageTypeValue.setText(viewModel.getImageType()); + sizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getSize())); + sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getSectorSize())); + timeZoneValue.setText(viewModel.getTimeZone()); - for (String path : selectedImage.getPaths()) { + for (String path : viewModel.getPaths()) { ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); } - try { - //older databases may have null as the hash values - String md5String = selectedImage.getMd5(); - if (md5String == null) { - md5String = ""; - } - md5HashValue.setText(md5String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); - } - - try { - String sha1String = selectedImage.getSha1(); - if (sha1String == null) { - sha1String = ""; - } - sha1HashValue.setText(sha1String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); - } - - try { - String sha256String = selectedImage.getSha256(); - if (sha256String == null) { - sha256String = ""; - } - sha256HashValue.setText(sha256String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); - } + md5HashValue.setText(viewModel.getMd5Hash()); + sha1HashValue.setText(viewModel.getSha1Hash()); + sha256HashValue.setText(viewModel.getSha256Hash()); } /** @@ -277,9 +480,82 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { ((DefaultTableModel) filePathsTable.getModel()).setRowCount(0); } + /** + * Divides acquisition details into key/value pairs to be displayed in + * separate cells in an excel export. + * + * @param acquisitionDetails The acquisition details. + * @return The list of key value pairs that can be incorporated into the + * excel export. + */ + private static List getAcquisitionDetails(String acquisitionDetails) { + if (StringUtils.isBlank(acquisitionDetails)) { + return Collections.emptyList(); + } else { + return Stream.of(acquisitionDetails.split("\\r?\\n")) + .map((line) -> (StringUtils.isBlank(line)) ? null : new SingleCellExportable(line)) + .filter(item -> item != null) + .collect(Collectors.toList()); + } + } + @Override - List getExports(DataSource ds) { - return Collections.emptyList(); + @Messages({ + "ContainerPanel_export_displayName=Display Name:", + "ContainerPanel_export_originalName=Name:", + "ContainerPanel_export_deviceId=Device ID:", + "ContainerPanel_export_timeZone=Time Zone:", + "ContainerPanel_export_acquisitionDetails=Acquisition Details:", + "ContainerPanel_export_imageType=Image Type:", + "ContainerPanel_export_size=Size:", + "ContainerPanel_export_sectorSize=Sector Size:", + "ContainerPanel_export_md5=MD5:", + "ContainerPanel_export_sha1=SHA1:", + "ContainerPanel_export_sha256=SHA256:", + "ContainerPanel_export_unallocatedSize=Unallocated Space:", + "ContainerPanel_export_filePaths=File Paths:",}) + protected List getExports(DataSource ds) { + ContainerViewModel result = getFetchResult(containerDataFetcher, "Container sheets", ds); + if (ds == null || result == null) { + return Collections.emptyList(); + } + + String NA = Bundle.ContainerPanel_setFieldsForNonImageDataSource_na(); + DefaultCellModel NACell = new DefaultCellModel<>(NA); + + ImageViewModel imageModel = result.getImageViewModel(); + boolean hasImage = imageModel != null; + + DefaultCellModel timeZone = hasImage ? new DefaultCellModel<>(imageModel.getTimeZone()) : NACell; + DefaultCellModel imageType = hasImage ? new DefaultCellModel<>(imageModel.getImageType()) : NACell; + DefaultCellModel size = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getSize()) : NACell; + DefaultCellModel sectorSize = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getSectorSize()) : NACell; + DefaultCellModel md5 = hasImage ? new DefaultCellModel<>(imageModel.getMd5Hash()) : NACell; + DefaultCellModel sha1 = hasImage ? new DefaultCellModel<>(imageModel.getSha1Hash()) : NACell; + DefaultCellModel sha256 = hasImage ? new DefaultCellModel<>(imageModel.getSha256Hash()) : NACell; + DefaultCellModel unallocatedSize = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getUnallocatedSize()) : NACell; + List paths = result.getImageViewModel() == null ? Collections.singletonList(NA) : result.getImageViewModel().getPaths(); + List cellPaths = paths.stream() + .map(SingleCellExportable::new) + .collect(Collectors.toList()); + + return Arrays.asList( + new ExcelSpecialFormatExport(Bundle.ContainerPanel_tabName(), Arrays.asList( + new KeyValueItemExportable(Bundle.ContainerPanel_export_displayName(), new DefaultCellModel<>(result.getDisplayName())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_originalName(), new DefaultCellModel<>(result.getOriginalName())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_deviceId(), new DefaultCellModel<>(result.getDeviceId())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_timeZone(), timeZone), + new TitledExportable(Bundle.ContainerPanel_export_acquisitionDetails(), getAcquisitionDetails(result.getAcquisitionDetails())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_imageType(), imageType), + new KeyValueItemExportable(Bundle.ContainerPanel_export_size(), size), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sectorSize(), sectorSize), + new KeyValueItemExportable(Bundle.ContainerPanel_export_md5(), md5), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sha1(), sha1), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sha256(), sha256), + new KeyValueItemExportable(Bundle.ContainerPanel_export_unallocatedSize(), unallocatedSize), + new TitledExportable(Bundle.ContainerPanel_export_filePaths(), cellPaths) + ))); + } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 102a75f89b..4a77a8a485 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -68,7 +68,6 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { * * @param tabTitle The title of the tab. * @param panel The component to be displayed in the tab. - * @param notifyParentClose Notifies parent to trigger a close. */ DataSourceTab(String tabTitle, BaseDataSourceSummaryPanel panel) { this(tabTitle, panel, panel::setDataSource, panel::getExports, panel::close); @@ -138,10 +137,10 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { private Runnable notifyParentClose = null; private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); - + // create an export panel whose button triggers the export to XLSX action private final ExportPanel exportPanel = new ExportPanel(); - + private final List tabs = Arrays.asList( new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_typesTab_title(), new TypesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), new UserActivityPanel()), @@ -155,7 +154,7 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource, - null, + IngestJobExcelExport::getExports, null), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new ContainerPanel()), new DataSourceTab( @@ -165,10 +164,10 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { null, null) ); - + // the action that does the export private final ExcelExportAction exportAction = new ExcelExportAction(tabs); - + private DataSource dataSource = null; private CardLayout cardLayout; @@ -222,7 +221,7 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { // set this to no datasource initially cardLayout.show(this, NO_DATASOURCE_PANE); - + // set action for when user requests xlsx export exportPanel.setXlsxExportAction(() -> exportAction.accept(getDataSource())); } @@ -270,7 +269,6 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), caseEventsListener); } - /** * 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 diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java new file mode 100644 index 0000000000..3ad757e7be --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java @@ -0,0 +1,253 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.ui; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ColumnModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.IngestModuleInfo; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Class that handles exporting information in IngestJobInfoPanel to excel. + */ +@Messages({ + "IngestJobExcelExport_startTimeColumn=Start Time", + "IngestJobExcelExport_endTimeColumn=End Time", + "IngestJobExcelExport_ingestStatusTimeColumn=Ingest Status", + "IngestJobExcelExport_moduleNameTimeColumn=Module Name", + "IngestJobExcelExport_versionColumn=Module Version", + "IngestJobExcelExport_sheetName=Ingest History" +}) +class IngestJobExcelExport { + + /** + * An entry to display in an excel export. + */ + private static class IngestJobEntry { + + private final Date startTime; + private final Date endTime; + private final String status; + private final String ingestModule; + private final String ingestModuleVersion; + + /** + * Main constructor. + * + * @param startTime The ingest start time. + * @param endTime The ingest stop time. + * @param status The ingest status. + * @param ingestModule The ingest module. + * @param ingestModuleVersion The ingest module version. + */ + IngestJobEntry(Date startTime, Date endTime, String status, String ingestModule, String ingestModuleVersion) { + this.startTime = startTime; + this.endTime = endTime; + this.status = status; + this.ingestModule = ingestModule; + this.ingestModuleVersion = ingestModuleVersion; + } + + /** + * @return The ingest start time. + */ + Date getStartTime() { + return startTime; + } + + /** + * @return The ingest stop time. + */ + Date getEndTime() { + return endTime; + } + + /** + * @return The ingest status. + */ + String getStatus() { + return status; + } + + /** + * @return The ingest module. + */ + String getIngestModule() { + return ingestModule; + } + + /** + * @return The ingest module version. + */ + String getIngestModuleVersion() { + return ingestModuleVersion; + } + } + + private static final Logger logger = Logger.getLogger(IngestJobExcelExport.class.getName()); + private static final String DATETIME_FORMAT_STR = "yyyy/MM/dd HH:mm:ss"; + private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat(DATETIME_FORMAT_STR, Locale.getDefault()); + + // columns in the excel export table to be created. + private static final List>> COLUMNS = Arrays.asList( + new ColumnModel<>( + Bundle.IngestJobExcelExport_startTimeColumn(), + (entry) -> getDateCell(entry.getStartTime())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_endTimeColumn(), + (entry) -> getDateCell(entry.getEndTime())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_ingestStatusTimeColumn(), + (entry) -> new DefaultCellModel<>(entry.getStatus())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_moduleNameTimeColumn(), + (entry) -> new DefaultCellModel<>(entry.getIngestModule())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_versionColumn(), + (entry) -> new DefaultCellModel<>(entry.getIngestModuleVersion())) + ); + + /** + * Retrieves data for a date cell. + * + * @param date The date. + * @return The data cell to be used in the excel export. + */ + private static DefaultCellModel getDateCell(Date date) { + Function dateParser = (dt) -> dt == null ? "" : DATETIME_FORMAT.format(dt); + return new DefaultCellModel<>(date, dateParser, DATETIME_FORMAT_STR); + } + + /** + * Retrieves all the ingest job modules and versions for a job. + * + * @param job The ingest job. + * @return All of the corresponding entries sorted by module name. + */ + private static List getEntries(IngestJobInfo job) { + List infoList = job.getIngestModuleInfo(); + if (infoList == null) { + return Collections.emptyList(); + } else { + Date startTime = job.getStartDateTime(); + Date endTime = job.getEndDateTime(); + String status = job.getStatus().getDisplayName(); + + return infoList.stream() + .filter(info -> info != null) + .map(info -> new IngestJobEntry(startTime, endTime, status, info.getDisplayName(), info.getVersion())) + .sorted((a, b) -> { + boolean aIsNull = a == null || a.getIngestModule() == null; + boolean bIsNull = b == null || b.getIngestModule() == null; + if (aIsNull || bIsNull) { + return Boolean.compare(aIsNull, bIsNull); + } else { + return a.getIngestModule().compareTo(b.getIngestModule()); + } + }) + .collect(Collectors.toList()); + } + } + + /** + * For output, show ingest job details in first row present. Otherwise, set + * to null. + * + * @param list The list of entries for an ingest job. + * @return The stream of entries to be displayed. + */ + private static Stream showFirstRowOnly(List list) { + return IntStream.range(0, list.size()) + .mapToObj(idx -> { + IngestJobEntry entry = list.get(idx); + if (entry == null || idx == 0) { + return entry; + } else { + return new IngestJobEntry(null, null, null, entry.getIngestModule(), entry.getIngestModuleVersion()); + } + }); + + } + + /** + * Returns a list of sheets to be exported for the Ingest History tab. + * + * @param dataSource The data source. + * @return The list of sheets to be included in an export. + */ + static List getExports(DataSource dataSource) { + if (dataSource == null) { + return Collections.emptyList(); + } + + List info = null; + try { + info = Case.getCurrentCaseThrows().getSleuthkitCase().getIngestJobs(); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "There was an error fetching ingest jobs", ex); + } + + if (info == null) { + info = Collections.emptyList(); + } + + List toDisplay = info.stream() + .filter(job -> job != null && dataSource.getId() == job.getObjectId()) + .sorted((a, b) -> { + // sort ingest jobs by time. + boolean aIsNull = a.getStartDateTime() == null; + boolean bIsNull = b.getStartDateTime() == null; + if (aIsNull || bIsNull) { + return Boolean.compare(aIsNull, bIsNull); + } else { + return a.getStartDateTime().compareTo(b.getStartDateTime()); + } + }) + .map((job) -> getEntries(job)) + .filter(lst -> lst != null) + .flatMap((lst) -> showFirstRowOnly(lst)) + .filter(item -> item != null) + .collect(Collectors.toList()); + + return Arrays.asList(new ExcelTableExport<>(Bundle.IngestJobExcelExport_sheetName(), COLUMNS, toDisplay)); + } + + private IngestJobExcelExport() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java index 34c622563d..74b9be06a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java @@ -22,6 +22,7 @@ import java.text.DecimalFormat; import java.util.Arrays; import java.util.List; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; /** * This class provides utilities for representing storage size in most relevant @@ -32,14 +33,64 @@ public final class SizeRepresentationUtil { private static final int SIZE_CONVERSION_CONSTANT = 1000; private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); - private static List UNITS = Arrays.asList( - Bundle.SizeRepresentationUtil_units_bytes(), - Bundle.SizeRepresentationUtil_units_kilobytes(), - Bundle.SizeRepresentationUtil_units_megabytes(), - Bundle.SizeRepresentationUtil_units_gigabytes(), - Bundle.SizeRepresentationUtil_units_terabytes(), - Bundle.SizeRepresentationUtil_units_petabytes() - ); + /** + * A size unit corresponding to orders of magnitude of bytes (kilobyte, gigabytes, etc.). + */ + @NbBundle.Messages({ + "SizeRepresentationUtil_units_bytes=bytes", + "SizeRepresentationUtil_units_kilobytes=KB", + "SizeRepresentationUtil_units_megabytes=MB", + "SizeRepresentationUtil_units_gigabytes=GB", + "SizeRepresentationUtil_units_terabytes=TB", + "SizeRepresentationUtil_units_petabytes=PB" + }) + enum SizeUnit { + BYTES(Bundle.SizeRepresentationUtil_units_bytes(), "#", 0), + KB(Bundle.SizeRepresentationUtil_units_kilobytes(), "#,##0.00,", 1), + MB(Bundle.SizeRepresentationUtil_units_megabytes(), "#,##0.00,,", 2), + GB(Bundle.SizeRepresentationUtil_units_gigabytes(), "#,##0.00,,,", 3), + TB(Bundle.SizeRepresentationUtil_units_terabytes(), "#,##0.00,,,,", 4), + PB(Bundle.SizeRepresentationUtil_units_petabytes(), "#,##0.00,,,,,", 5); + + private final String suffix; + private final String excelFormatString; + private final long divisor; + + /** + * Main constructor. + * @param suffix The string suffix to use for size unit. + * @param excelFormatString The excel format string to use for this size unit. + * @param power The power of 1000 of bytes for this size unit. + */ + SizeUnit(String suffix, String excelFormatString, int power) { + this.suffix = suffix; + + // based on https://www.mrexcel.com/board/threads/how-do-i-format-cells-to-show-gb-mb-kb.140135/ + this.excelFormatString = String.format("%s \"%s\"", excelFormatString, suffix); + this.divisor = (long) Math.pow(SIZE_CONVERSION_CONSTANT, power); + } + + /** + * @return The string suffix to use for size unit. + */ + public String getSuffix() { + return suffix; + } + + /** + * @return The excel format string to use for this size unit. + */ + public String getExcelFormatString() { + return excelFormatString; + } + + /** + * @return The divisor to convert from bytes to this unit. + */ + public long getDivisor() { + return divisor; + } + } /** * Get a long size in bytes as a string formated to be read by users. @@ -47,49 +98,59 @@ public final class SizeRepresentationUtil { * @param size Long value representing a size in bytes. * * @return Return a string formated with a user friendly version of the size - * as a string, returns empty String when provided empty size. + * as a string, returns empty String when provided empty size. */ - public static String getSizeString(Long size) { + static String getSizeString(Long size) { return getSizeString(size, APPROXIMATE_SIZE_FORMAT, true); } + /** + * Determines the relevant size unit that should be used for a particular size. + * @param size The size in bytes. + * @return The relevant size unit. + */ + static SizeUnit getSizeUnit(Long size) { + if (size == null) { + return SizeUnit.values()[0]; + } + + for (int unitsIndex = 0; unitsIndex < SizeUnit.values().length; unitsIndex++) { + SizeUnit unit = SizeUnit.values()[unitsIndex]; + long result = size / unit.getDivisor(); + if (result < SIZE_CONVERSION_CONSTANT) { + return unit; + } + } + + return SizeUnit.values()[SizeUnit.values().length - 1]; + } + /** * Get a long size in bytes as a string formated to be read by users. * - * @param size Long value representing a size in byte.s - * @param format The means of formatting the number. + * @param size Long value representing a size in byte.s + * @param format The means of formatting the number. * @param showFullSize Optionally show the number of bytes in the - * datasource. + * datasource. * * @return Return a string formated with a user friendly version of the size - * as a string, returns empty String when provided empty size. + * as a string, returns empty String when provided empty size. */ - @NbBundle.Messages({ - "SizeRepresentationUtil_units_bytes= bytes", - "SizeRepresentationUtil_units_kilobytes= kB", - "SizeRepresentationUtil_units_megabytes= MB", - "SizeRepresentationUtil_units_gigabytes= GB", - "SizeRepresentationUtil_units_terabytes= TB", - "SizeRepresentationUtil_units_petabytes= PB" - }) - public static String getSizeString(Long size, DecimalFormat format, boolean showFullSize) { + static String getSizeString(Long size, DecimalFormat format, boolean showFullSize) { if (size == null) { return ""; } - double approximateSize = size; - int unitsIndex = 0; - for (; unitsIndex < UNITS.size(); unitsIndex++) { - if (approximateSize < SIZE_CONVERSION_CONSTANT) { - break; - } else { - approximateSize /= SIZE_CONVERSION_CONSTANT; - } + + SizeUnit sizeUnit = getSizeUnit(size); + if (sizeUnit == null) { + sizeUnit = SizeUnit.BYTES; } - - String fullSize = size + UNITS.get(0); - String closestUnitSize = format.format(approximateSize) + UNITS.get(unitsIndex); - - if (unitsIndex == 0) { + + String closestUnitSize = String.format("%s %s", + format.format(((double) size) / sizeUnit.getDivisor()), sizeUnit.getSuffix()); + + String fullSize = String.format("%d %s", size, SizeUnit.BYTES.getSuffix()); + if (sizeUnit.equals(SizeUnit.BYTES)) { return fullSize; } else if (showFullSize) { return String.format("%s (%s)", closestUnitSize, fullSize); @@ -97,6 +158,24 @@ public final class SizeRepresentationUtil { return closestUnitSize; } } + + /** + * Returns a default cell model using size units. + * @param bytes The number of bytes. + * @return The default cell model. + */ + static DefaultCellModel getBytesCell(Long bytes) { + if (bytes == null) { + return new DefaultCellModel<>(""); + } else { + SizeUnit unit = SizeRepresentationUtil.getSizeUnit(bytes); + if (unit == null) { + unit = SizeUnit.BYTES; + } + + return new DefaultCellModel(bytes, SizeRepresentationUtil::getSizeString, unit.getExcelFormatString()); + } + } private SizeRepresentationUtil() { } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java index ae5598cb92..97fc63fc51 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -31,7 +31,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.FileTypeUtils.FileTypeCategory; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TypesSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.MimeTypeSummary; @@ -40,13 +39,16 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.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.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel.PieChartItem; -import org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -72,7 +74,8 @@ import org.sleuthkit.datamodel.TskCoreException; "TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed", "TypesPanel_usageLabel_title=Usage", "TypesPanel_osLabel_title=OS", - "TypesPanel_sizeLabel_title=Size"}) + "TypesPanel_sizeLabel_title=Size", + "TypesPanel_excelTabName=Types"}) class TypesPanel extends BaseDataSourceSummaryPanel { /** @@ -167,10 +170,9 @@ class TypesPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; private static final DecimalFormat INTEGER_SIZE_FORMAT = new DecimalFormat("#"); - private static final DecimalFormat COMMA_FORMATTER = new DecimalFormat("#,###"); - private static final String FILE_TYPE_FACTORY = FileTypeIdModuleFactory.class.getCanonicalName(); - private static final String FILE_TYPE_MODULE_NAME = FileTypeIdModuleFactory.getModuleName(); - private static final Logger logger = Logger.getLogger(TypesPanel.class.getName()); + private static final String COMMA_FORMAT_STR = "#,###"; + + private static final DecimalFormat COMMA_FORMATTER = new DecimalFormat(COMMA_FORMAT_STR); private static final Color IMAGES_COLOR = new Color(156, 39, 176); private static final Color VIDEOS_COLOR = Color.YELLOW; @@ -191,6 +193,15 @@ class TypesPanel extends BaseDataSourceSummaryPanel { new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_unknown_title(), new HashSet<>(Arrays.asList("application/octet-stream")), UNKNOWN_COLOR) ); + private final DataFetcher usageFetcher; + private final DataFetcher osFetcher; + private final DataFetcher sizeFetcher; + + private final DataFetcher allocatedFetcher; + private final DataFetcher unallocatedFetcher; + private final DataFetcher slackFetcher; + private final DataFetcher directoriesFetcher; + private final LoadableLabel usageLabel = new LoadableLabel(Bundle.TypesPanel_usageLabel_title()); private final LoadableLabel osLabel = new LoadableLabel(Bundle.TypesPanel_osLabel_title()); private final LoadableLabel sizeLabel = new LoadableLabel(Bundle.TypesPanel_sizeLabel_title()); @@ -246,42 +257,34 @@ class TypesPanel extends BaseDataSourceSummaryPanel { super(mimeTypeData, typeData, containerData); + this.usageFetcher = containerData::getDataSourceType; + this.osFetcher = containerData::getOperatingSystems; + + this.sizeFetcher = (dataSource) -> dataSource == null ? null : dataSource.getSize(); + + this.allocatedFetcher = (dataSource) -> typeData.getCountOfAllocatedFiles(dataSource); + this.unallocatedFetcher = (dataSource) -> typeData.getCountOfUnallocatedFiles(dataSource); + this.slackFetcher = (dataSource) -> typeData.getCountOfSlackFiles(dataSource); + this.directoriesFetcher = (dataSource) -> typeData.getCountOfDirectories(dataSource); + this.dataFetchComponents = Arrays.asList( - // usage label worker - new DataFetchWorker.DataFetchComponents<>( - containerData::getDataSourceType, - (result) -> usageLabel.showDataFetchResult(result)), - // os label worker - new DataFetchWorker.DataFetchComponents<>( - containerData::getOperatingSystems, - (result) -> osLabel.showDataFetchResult(result)), - // size label worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> { - Long size = dataSource == null ? null : dataSource.getSize(); - return SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false); - }, - sizeLabel::showDataFetchResult), - // file types worker + new DataFetchWorker.DataFetchComponents<>(usageFetcher, usageLabel::showDataFetchResult), + new DataFetchWorker.DataFetchComponents<>(osFetcher, osLabel::showDataFetchResult), + new DataFetchWorker.DataFetchComponents<>(sizeFetcher, + (sizeResult) -> sizeLabel.showDataFetchResult( + DataFetchResult.getSubResult(sizeResult, + size -> SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false)))), new DataFetchWorker.DataFetchComponents<>( (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource), this::showMimeTypeCategories), - // allocated files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfAllocatedFiles(dataSource)), - allocatedLabel::showDataFetchResult), - // unallocated files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfUnallocatedFiles(dataSource)), - unallocatedLabel::showDataFetchResult), - // slack files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfSlackFiles(dataSource)), - slackLabel::showDataFetchResult), - // directories worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfDirectories(dataSource)), - directoriesLabel::showDataFetchResult) + new DataFetchWorker.DataFetchComponents<>(allocatedFetcher, + countRes -> allocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(unallocatedFetcher, + countRes -> unallocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(slackFetcher, + countRes -> slackLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(directoriesFetcher, + countRes -> directoriesLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))) ); initComponents(); @@ -406,9 +409,53 @@ class TypesPanel extends BaseDataSourceSummaryPanel { return longVal == null ? "0" : COMMA_FORMATTER.format(longVal); } + /** + * Returns a key value pair to be exported in a sheet. + * + * @param fetcher The means of fetching the data. + * @param key The key to use. + * @param dataSource The data source containing the data. + * @return The key value pair to be exported. + */ + private static KeyValueItemExportable getStrExportable(DataFetcher fetcher, String key, DataSource dataSource) { + String result = getFetchResult(fetcher, "Types", dataSource); + return (result == null) ? null : new KeyValueItemExportable(key, new DefaultCellModel<>(result)); + } + + /** + * Returns a key value pair to be exported in a sheet formatting the long + * with commas separated by orders of 1000. + * + * @param fetcher The means of fetching the data. + * @param key The string key for this key value pair. + * @param dataSource The data source. + * @return The key value pair. + */ + private static KeyValueItemExportable getCountExportable(DataFetcher fetcher, String key, DataSource dataSource) { + Long count = getFetchResult(fetcher, "Types", dataSource); + return (count == null) ? null : new KeyValueItemExportable(key, + new DefaultCellModel(count, COMMA_FORMATTER::format, COMMA_FORMAT_STR)); + } + @Override List getExports(DataSource dataSource) { - return Collections.emptyList(); + if (dataSource == null) { + return Collections.emptyList(); + } + + 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))), + getCountExportable(allocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_allocatedRow_title(), dataSource), + getCountExportable(unallocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_unallocatedRow_title(), dataSource), + getCountExportable(slackFetcher, Bundle.TypesPanel_filesByCategoryTable_slackRow_title(), dataSource), + getCountExportable(directoriesFetcher, Bundle.TypesPanel_filesByCategoryTable_directoryRow_title(), dataSource)) + .filter(sheet -> sheet != null) + .collect(Collectors.toList()) + )); } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java index 38afc21e2f..60d7e27112 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import javax.swing.JLabel; +import org.apache.poi.ss.usermodel.HorizontalAlignment; /** * Basic interface for a cell model. @@ -29,20 +30,23 @@ public interface CellModel { * Describes the horizontal alignment. */ public enum HorizontalAlign { - LEFT(JLabel.LEFT), - CENTER(JLabel.CENTER), - RIGHT(JLabel.RIGHT); + LEFT(JLabel.LEFT, HorizontalAlignment.LEFT), + CENTER(JLabel.CENTER, HorizontalAlignment.CENTER), + RIGHT(JLabel.RIGHT, HorizontalAlignment.RIGHT); private final int jlabelAlignment; + private final HorizontalAlignment poiAlignment; /** * Constructor for a HorizontalAlign enum. * * @param jlabelAlignment The corresponding JLabel horizontal alignment * number. + * @param poiAlignment Horizontal alignment for Apache POI. */ - HorizontalAlign(int jlabelAlignment) { + HorizontalAlign(int jlabelAlignment, HorizontalAlignment poiAlignment) { this.jlabelAlignment = jlabelAlignment; + this.poiAlignment = poiAlignment; } /** @@ -52,6 +56,13 @@ public interface CellModel { int getJLabelAlignment() { return this.jlabelAlignment; } + + /** + * @return Horizontal alignment for Apache POI. + */ + HorizontalAlignment getPoiAlignment() { + return poiAlignment; + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java index a409794387..06165a3023 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java @@ -24,21 +24,21 @@ import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelCellModel; /** * The default cell model. */ public class DefaultCellModel implements GuiCellModel, ExcelCellModel { - private final T data; - private final Function stringConverter; + final T data; + final Function stringConverter; String tooltip; CellModel.HorizontalAlign horizontalAlignment; Insets insets; List popupMenu; Supplier> menuItemSupplier; - private final String excelFormatString; + final String excelFormatString; /** * Main constructor. @@ -76,6 +76,7 @@ public class DefaultCellModel implements GuiCellModel, ExcelCellModel { this.data = data; this.stringConverter = stringConverter; this.excelFormatString = excelFormatString; + this.tooltip = getText(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java new file mode 100644 index 0000000000..0ca52f12e4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java @@ -0,0 +1,32 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +/** + * Basic interface for a cell model. + */ +public interface ExcelCellModel extends CellModel { + + /** + * @return The format string to be used with Apache POI during excel + * export or null if none necessary. + */ + String getExcelFormatString(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java index cf4869ce47..fab6558c4a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java @@ -21,13 +21,23 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModel.HorizontalAlign; /** * Class for handling Excel exporting. @@ -59,6 +69,87 @@ public class ExcelExport { } } + /** + * A cell style key that can be used with the WorksheetEnv to generate a + * cell style to be used in a POI excel document. + */ + static class CellStyleKey { + + private final String formatString; + private final CellStyle cellStyle; + private final HorizontalAlign alignment; + + /** + * Main constructor. + * + * @param formatString The format string or null if no special + * formatting. + * @param cellStyle The base cell style or null if default is to be + * used. + * @param alignment The horizontal alignment or null if default is to be + * used. + */ + CellStyleKey(String formatString, CellStyle cellStyle, HorizontalAlign alignment) { + this.formatString = formatString; + this.cellStyle = cellStyle; + this.alignment = alignment; + } + + /** + * @return The format string or null if no special formatting. + */ + String getFormatString() { + return formatString; + } + + /** + * @return The base cell style or null if default is to be used. + */ + CellStyle getCellStyle() { + return cellStyle; + } + + /** + * @return The horizontal alignment or null if default is to be used. + */ + HorizontalAlign getAlignment() { + return alignment; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + Objects.hashCode(this.formatString); + hash = 29 * hash + Objects.hashCode(this.cellStyle); + hash = 29 * hash + Objects.hashCode(this.alignment); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CellStyleKey other = (CellStyleKey) obj; + if (!Objects.equals(this.formatString, other.formatString)) { + return false; + } + if (!Objects.equals(this.cellStyle, other.cellStyle)) { + return false; + } + if (this.alignment != other.alignment) { + return false; + } + return true; + } + } + /** * Class detailing aspects of the worksheet. */ @@ -66,18 +157,47 @@ public class ExcelExport { private final CellStyle headerStyle; private final Workbook parentWorkbook; + private final CellStyle defaultStyle; + + // maps a data format string / original cell style combination to a created cell style + private final Map cellStyleCache = new HashMap<>(); /** * Main constructor. * * @param headerStyle The cell style to use for headers. + * @param defaultStyle The cell style to use as a default. * @param parentWorkbook The parent workbook. */ - WorksheetEnv(CellStyle headerStyle, Workbook parentWorkbook) { + WorksheetEnv(CellStyle headerStyle, CellStyle defaultStyle, Workbook parentWorkbook) { this.headerStyle = headerStyle; + this.defaultStyle = defaultStyle; this.parentWorkbook = parentWorkbook; } + /** + * Returns a cell style signified by the given cell style key. If the + * key is already present, a cached version is returned. + * + * @param cellStyleKey The key. + * @return The cell style representing this key. + */ + public CellStyle getCellStyle(CellStyleKey cellStyleKey) { + return cellStyleCache.computeIfAbsent(cellStyleKey, (pair) -> { + CellStyle computed = this.parentWorkbook.createCellStyle(); + computed.cloneStyleFrom(cellStyleKey.getCellStyle() == null ? defaultStyle : cellStyleKey.getCellStyle()); + + if (cellStyleKey.getAlignment() != null) { + computed.setAlignment(cellStyleKey.getAlignment().getPoiAlignment()); + } + + if (cellStyleKey.getFormatString() != null) { + computed.setDataFormat(this.parentWorkbook.getCreationHelper().createDataFormat().getFormat(cellStyleKey.getFormatString())); + } + return computed; + }); + } + /** * Returns the cell style to use for headers. * @@ -87,6 +207,15 @@ public class ExcelExport { return headerStyle; } + /** + * Returns the cell style for default items. + * + * @return The cell style for default items. + */ + public CellStyle getDefaultCellStyle() { + return defaultStyle; + } + /** * Returns the parent workbook. * @@ -125,6 +254,7 @@ public class ExcelExport { /** * Retrieves a singleton instance of this class. + * * @return The instance. */ public static ExcelExport getInstance() { @@ -141,10 +271,11 @@ public class ExcelExport { /** * Writes the exports to a workbook. + * * @param exports The sheets to export. * @param path The path to the output file. * @throws IOException - * @throws ExcelExportException + * @throws ExcelExportException */ @Messages({ "# {0} - sheetNumber", @@ -160,10 +291,15 @@ public class ExcelExport { //headerFont.setFontHeightInPoints((short) 14); // Create a CellStyle with the font + HorizontalAlignment alignment = HorizontalAlignment.LEFT; CellStyle headerCellStyle = workbook.createCellStyle(); headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(alignment); - WorksheetEnv env = new WorksheetEnv(headerCellStyle, workbook); + CellStyle defaultCellStyle = workbook.createCellStyle(); + defaultCellStyle.setAlignment(alignment); + + WorksheetEnv env = new WorksheetEnv(headerCellStyle, defaultCellStyle, workbook); if (exports != null) { for (int i = 0; i < exports.size(); i++) { @@ -190,4 +326,46 @@ public class ExcelExport { // Closing the workbook workbook.close(); } + + /** + * Creates an excel cell given the model. + * + * @param env The work sheet environment including the workbook. + * @param row The row in the excel document. + * @param colNum The column number (not zero-indexed). + * @param cellModel The model for the cell. + * @param cellStyle The style to use. + * @return The created cell. + */ + static Cell createCell(WorksheetEnv env, Row row, int colNum, ExcelCellModel cellModel, Optional cellStyle) { + CellStyle cellStyleToUse = cellStyle.orElse(env.getDefaultCellStyle()); + + if (cellModel.getExcelFormatString() != null || cellModel.getHorizontalAlignment() != null) { + cellStyleToUse = env.getCellStyle(new CellStyleKey(cellModel.getExcelFormatString(), cellStyleToUse, cellModel.getHorizontalAlignment())); + } + + Object cellData = cellModel.getData(); + Cell cell = row.createCell(colNum); + if (cellData instanceof Calendar) { + cell.setCellValue((Calendar) cellData); + } else if (cellData instanceof Date) { + cell.setCellValue((Date) cellData); + } else if (cellData instanceof Double) { + cell.setCellValue((Double) cellData); + } else if (cellData instanceof String) { + cell.setCellValue((String) cellData); + } else if (cellData instanceof Short) { + cell.setCellValue((Short) cellData); + } else if (cellData instanceof Integer) { + cell.setCellValue((Integer) cellData); + } else if (cellData instanceof Long) { + cell.setCellValue((Long) cellData); + } else if (cellData instanceof Float) { + cell.setCellValue((Float) cellData); + } else { + cell.setCellValue(cellModel.getText()); + } + cell.setCellStyle(cellStyleToUse); + return cell; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java new file mode 100644 index 0000000000..f9c46fa5e2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java @@ -0,0 +1,265 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; + +/** + * An excel export that has special row-by-row formatting. + */ +public class ExcelSpecialFormatExport implements ExcelExport.ExcelSheetExport { + + /** + * The dimensions consumed by an item in an ExcelSpecialFormatExport list of + * items to be rendered. + */ + public static class ItemDimensions { + + private final int rowStart; + private final int rowEnd; + private final int colStart; + private final int colEnd; + + /** + * Main constructor. + * + * @param rowStart The starting excel row of the item. + * @param colStart The starting excel column of the item. + * @param rowEnd The last excel row of the the item. + * @param colEnd The last excel column of the item. + */ + public ItemDimensions(int rowStart, int colStart, int rowEnd, int colEnd) { + this.rowStart = rowStart; + this.colStart = colStart; + this.rowEnd = rowEnd; + this.colEnd = colEnd; + } + + /** + * @return The starting excel row of the item. + */ + public int getRowStart() { + return rowStart; + } + + /** + * @return The last excel row of the the item. + */ + public int getRowEnd() { + return rowEnd; + } + + /** + * @return The starting excel column of the item. + */ + public int getColStart() { + return colStart; + } + + /** + * @return The last excel column of the item. + */ + public int getColEnd() { + return colEnd; + } + } + + /** + * An item to be exported in a specially formatted excel export. + */ + public interface ExcelItemExportable { + + /** + * Writes the item to the sheet in the special format export sheet. + * + * @param sheet The sheet. + * @param rowStart The starting row to start writing. + * @param colStart The starting column to start writing. + * @param env The excel export context. + * @return The dimensions of what has been written. + * @throws ExcelExportException + */ + ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException; + } + + /** + * Writes a string to a single cell in a specially formatted excel export. + */ + public static class SingleCellExportable implements ExcelItemExportable { + + private final ExcelCellModel item; + + /** + * Main constructor. + * + * @param key The text to be written. + */ + public SingleCellExportable(String key) { + this(new DefaultCellModel<>(key)); + } + + /** + * Main constructor. + * + * @param item The cell model to be written. + */ + public SingleCellExportable(ExcelCellModel item) { + this.item = item; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + Row row = sheet.createRow(rowStart); + ExcelExport.createCell(env, row, colStart, item, Optional.empty()); + return new ItemDimensions(rowStart, colStart, rowStart, colStart); + } + } + + /** + * Writes a row consisting of first column as a key and second column as a + * value. + */ + public static class KeyValueItemExportable implements ExcelItemExportable { + + private final ExcelCellModel key; + private final ExcelCellModel value; + + /** + * Main constructor. + * + * @param key The string key to be exported. + * @param value The cell model to be exported. + */ + public KeyValueItemExportable(String key, ExcelCellModel value) { + this(new DefaultCellModel<>(key), value); + } + + /** + * Main constructor. + * + * @param key The cell key to be exported. + * @param value The cell model to be exported. + */ + public KeyValueItemExportable(ExcelCellModel key, ExcelCellModel value) { + this.key = key; + this.value = value; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + Row row = sheet.createRow(rowStart); + ExcelExport.createCell(env, row, colStart, key, Optional.of(env.getHeaderStyle())); + ExcelExport.createCell(env, row, colStart + 1, value, Optional.empty()); + return new ItemDimensions(rowStart, colStart, rowStart, colStart + 1); + } + } + + /** + * A special format excel export item that shows a title and a list of items + * indented one column. + * + * i.e. + *
+     * title
+     *      item 1
+     *      item 2
+     * 
+ */ + public static class TitledExportable implements ExcelItemExportable { + + private static final int DEFAULT_INDENT = 1; + + private final String title; + private final List children; + + /** + * Main constructor. + * + * @param title The title for the export. + * @param children The children to be indented and enumerated. + */ + public TitledExportable(String title, List children) { + this.title = title; + this.children = children; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + ExcelExport.createCell(env, sheet.createRow(rowStart), colStart, new DefaultCellModel<>(title), Optional.of(env.getHeaderStyle())); + int curRow = rowStart + 1; + int maxCol = colStart; + for (ExcelItemExportable export : children) { + if (export == null) { + continue; + } + + ItemDimensions thisItemDim = export.write(sheet, curRow, colStart + DEFAULT_INDENT, env); + curRow = thisItemDim.getRowEnd() + 1; + maxCol = Math.max(thisItemDim.getColEnd(), maxCol); + } + + return new ItemDimensions(rowStart, colStart, curRow - 1, maxCol); + } + } + + private final String sheetName; + private final List exports; + + /** + * Main constructor. + * + * @param sheetName The name of the sheet. + * @param exports The row-by-row items to be exported. + */ + public ExcelSpecialFormatExport(String sheetName, List exports) { + this.sheetName = sheetName; + this.exports = exports == null ? Collections.emptyList() : exports; + } + + @Override + public String getSheetName() { + return sheetName; + } + + @Override + public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExportException { + int rowStart = 0; + int maxCol = 0; + for (ExcelItemExportable export : exports) { + if (export == null) { + continue; + } + + ItemDimensions dimensions = export.write(sheet, rowStart, 0, env); + rowStart = dimensions.getRowEnd() + 1; + maxCol = Math.max(maxCol, dimensions.getColEnd()); + } + + // Resize all columns to fit the content size + for (int i = 0; i <= maxCol; i++) { + sheet.autoSizeColumn(i); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java index 43b8320c49..5592344e2c 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java @@ -18,40 +18,26 @@ */ package org.sleuthkit.autopsy.datasourcesummary.uiutils; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +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; /** * An excel sheet export of table data. */ -public class ExcelTableExport implements ExcelExport.ExcelSheetExport { - - /** - * Basic interface for a cell model. - */ - public interface ExcelCellModel extends CellModel { - - /** - * @return The format string to be used with Apache POI during excel - * export or null if none necessary. - */ - String getExcelFormatString(); - } +public class ExcelTableExport implements ExcelSheetExport, ExcelItemExportable { private final String sheetName; private final List> columns; private final List data; + private final int columnIndent; /** * Main constructor. @@ -62,9 +48,23 @@ public class ExcelTableExport implements ExcelExpor * @param data The data to export. */ public ExcelTableExport(String sheetName, List> columns, List data) { + this(sheetName, columns, data, 0); + } + + /** + * Main constructor. + * + * @param sheetName The name of the sheet. NOTE: There can be no duplicates + * in a workbook. + * @param columns The columns of the table. + * @param data The data to export. + * @param columnIndent The column indent. + */ + public ExcelTableExport(String sheetName, List> columns, List data, int columnIndent) { this.sheetName = sheetName; this.columns = columns; this.data = data; + this.columnIndent = columnIndent; } @Override @@ -74,11 +74,20 @@ public class ExcelTableExport implements ExcelExpor @Override public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv style) throws ExcelExport.ExcelExportException { - renderSheet(sheet, style, columns, data); + renderSheet(sheet, style, 0, columnIndent, columns, data); + // Resize all columns to fit the content size for (int i = 0; i < columns.size(); i++) { sheet.autoSizeColumn(i); } + + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + int columnStart = columnIndent + colStart; + int rowsWritten = renderSheet(sheet, env, rowStart, columnStart, columns, data); + return new ItemDimensions(rowStart, columnStart, rowStart + rowsWritten - 1, this.columns == null ? columnStart : columnStart + this.columns.size()); } /** @@ -86,88 +95,44 @@ public class ExcelTableExport implements ExcelExpor * * @param sheet The sheet. * @param worksheetEnv The worksheet environment and preferences. + * @param rowStart The row to start in. + * @param colStart The column to start in. * @param columns The columns. * @param data The data. * @throws ExcelExportException + * @return The number of rows (including the header) written. */ - private static void renderSheet( - Sheet sheet, ExcelExport.WorksheetEnv worksheetEnv, List> columns, List data) + private static int renderSheet( + Sheet sheet, + ExcelExport.WorksheetEnv worksheetEnv, + int rowStart, + int colStart, + List> columns, List data) throws ExcelExport.ExcelExportException { List safeData = data == null ? Collections.emptyList() : data; // Create a header row - Row headerRow = sheet.createRow(0); + Row headerRow = sheet.createRow(rowStart); // Create header cells for (int i = 0; i < columns.size(); i++) { - Cell cell = headerRow.createCell(i); + Cell cell = headerRow.createCell(i + colStart); 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) - Map cellStyles = new HashMap<>(); + for (int rowNum = 0; rowNum < safeData.size(); rowNum++) { T rowData = safeData.get(rowNum); - Row row = sheet.createRow(rowNum + 1); + Row row = sheet.createRow(rowNum + rowStart + 1); for (int colNum = 0; colNum < columns.size(); colNum++) { ColumnModel colModel = columns.get(colNum); ExcelCellModel cellModel = colModel.getCellRenderer().apply(rowData); - String formatString = cellModel.getExcelFormatString(); - Optional cellStyle = (formatString == null) - ? Optional.empty() - : Optional.of(cellStyles.computeIfAbsent(formatString, k -> createCellStyle(worksheetEnv.getParentWorkbook(), formatString))); - createCell(row, colNum, cellModel, cellStyle); + ExcelExport.createCell(worksheetEnv, row, colNum + colStart, cellModel, Optional.empty()); } } - } - /** - * Create a cell style in the workbook with the given format string. - * - * @param workbook The workbook. - * @param formatString The format string. - * @return The cell style. - */ - private static CellStyle createCellStyle(Workbook workbook, String formatString) { - CellStyle cellStyle = workbook.createCellStyle(); - cellStyle.setDataFormat(workbook.getCreationHelper().createDataFormat().getFormat(formatString)); - return cellStyle; + return safeData.size() + 1; } - - /** - * Creates an excel cell given the model. - * - * @param row The row in the excel document. - * @param colNum The column number (not zero-indexed). - * @param cellModel The model for the cell. - * @param cellStyle The style to use. - * @return The created cell. - */ - private static Cell createCell(Row row, int colNum, ExcelCellModel cellModel, Optional cellStyle) { - Object cellData = cellModel.getData(); - Cell cell = row.createCell(colNum); - if (cellData instanceof Calendar) { - cell.setCellValue((Calendar) cellData); - } else if (cellData instanceof Date) { - cell.setCellValue((Date) cellData); - } else if (cellData instanceof Double) { - cell.setCellValue((Double) cellData); - } else if (cellData instanceof String) { - cell.setCellValue((String) cellData); - } else if (cellData instanceof Short) { - cell.setCellValue((Short) cellData); - } else if (cellData instanceof Integer) { - cell.setCellValue((Integer) cellData); - } else if (cellData instanceof Long) { - cell.setCellValue((Long) cellData); - } else if (cellData instanceof Float) { - cell.setCellValue((Float) cellData); - } else { - cell.setCellValue(cellModel.getText()); - } - cellStyle.ifPresent(cs -> cell.setCellStyle(cs)); - return cell; - } - }