diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties index 2cb4756460..24a312f733 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties @@ -36,3 +36,4 @@ DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category DataSourceSummaryCountsPanel.jLabel1.text=Results by Type +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED index eb7affe9ee..ba0d3510c7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED @@ -61,9 +61,6 @@ DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category DataSourceSummaryCountsPanel.jLabel1.text=Results by Type -DataSourceSummaryDialog.countsTab.title=Counts -DataSourceSummaryDialog.detailsTab.title=Details -DataSourceSummaryDialog.ingestHistoryTab.title=Ingest History DataSourceSummaryDialog.window.title=Data Sources Summary DataSourceSummaryNode.column.dataSourceName.header=Data Source Name DataSourceSummaryNode.column.files.header=Files @@ -72,4 +69,14 @@ DataSourceSummaryNode.column.status.header=Ingest Status DataSourceSummaryNode.column.tags.header=Tags DataSourceSummaryNode.column.type.header=Type DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source +DataSourceSummaryTabbedPane_countsTab_title=Counts +DataSourceSummaryTabbedPane_detailsTab_title=Details +DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History +DataSourceSummaryTabbedPane_userActivityTab_title=User Activity +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run +DataSourceSummaryUserActivityPanel_tab_title=User Activity +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program ViewSummaryInformationAction.name.text=View Summary Information diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java index dcbf9bb82d..7e66e56682 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019 Basis Technology Corp. + * Copyright 2019 - 2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,8 +18,11 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; +import java.io.File; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -27,13 +30,20 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import java.util.Collections; +import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.DataSource; /** @@ -193,6 +203,31 @@ final class DataSourceInfoUtilities { return getBaseQueryResult(query, handler, errorMessage); } + /** + * Generates a result set handler that will return a map of string to long. + * + * @param keyParam The named parameter in the result set representing the + * key. + * @param valueParam The named parameter in the result set representing the + * value. + * + * @return The result set handler to generate the map of string to long. + */ + private static ResultSetHandler> getStringLongResultSetHandler(String keyParam, String valueParam) { + return (resultSet) -> { + LinkedHashMap toRet = new LinkedHashMap<>(); + while (resultSet.next()) { + try { + toRet.put(resultSet.getString(keyParam), resultSet.getLong(valueParam)); + } catch (SQLException ex) { + logger.log(Level.WARNING, "Failed to get a result pair from the result set.", ex); + } + } + + return toRet; + }; + } + /** * Retrieves counts for each artifact type in a data source. * @@ -215,24 +250,311 @@ final class DataSourceInfoUtilities { + " WHERE bba.data_source_obj_id =" + selectedDataSource.getId() + " GROUP BY bbt.display_name"; - ResultSetHandler> handler = (resultSet) -> { - Map toRet = new HashMap<>(); - while (resultSet.next()) { + String errorMessage = "Unable to get artifact type counts; returning null."; + return getBaseQueryResult(query, getStringLongResultSetHandler(nameParam, valueParam), errorMessage); + } + + /** + * Describes a result of a program run on a datasource. + */ + static class TopProgramsResult { + + private final String programName; + private final String programPath; + private final Long runTimes; + private final Date lastRun; + + /** + * Main constructor. + * + * @param programName The name of the program. + * @param programPath The path of the program. + * @param runTimes The number of runs. + */ + TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { + this.programName = programName; + this.programPath = programPath; + this.runTimes = runTimes; + this.lastRun = lastRun; + } + + /** + * @return The name of the program + */ + String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + String getProgramPath() { + return programPath; + } + + /** + * @return The number of run times or null if not present. + */ + Long getRunTimes() { + return runTimes; + } + + /** + * @return The last time the program was run or null if not present. + */ + public Date getLastRun() { + return lastRun; + } + } + + /** + * A SQL join type. + */ + private enum JoinType { + LEFT, + RIGHT, + INNER, + OUTER + } + + /** + * A blackboard attribute value column. + */ + private enum AttributeColumn { + value_text, + value_int32, + value_int64 + } + + /** + * The suffix joined to a key name for use as an identifier of a query. + */ + private static final String QUERY_SUFFIX = "_query"; + + /** + * Creates a sql statement querying the blackboard attributes table for a + * particular attribute type and returning a specified value. That query + * also joins with the blackboard artifact table. + * + * @param joinType The type of join statement to create. + * @param attributeColumn The blackboard attribute column that should be + * returned. + * @param attrType The attribute type to query for. + * @param keyName The aliased name of the attribute to return. This + * is also used to calculate the alias of the query + * same as getFullKey. + * @param bbaName The blackboard artifact table alias. + * + * @return The generated sql statement. + */ + private static String getAttributeJoin(JoinType joinType, AttributeColumn attributeColumn, ATTRIBUTE_TYPE attrType, String keyName, String bbaName) { + String queryName = keyName + QUERY_SUFFIX; + String innerQueryName = "inner_attribute_" + queryName; + + return "\n" + joinType + " JOIN (\n" + + " SELECT \n" + + " " + innerQueryName + ".artifact_id,\n" + + " " + innerQueryName + "." + attributeColumn + " AS " + keyName + "\n" + + " FROM blackboard_attributes " + innerQueryName + "\n" + + " WHERE " + innerQueryName + ".attribute_type_id = " + attrType.getTypeID() + " -- " + attrType.name() + "\n" + + ") " + queryName + " ON " + queryName + ".artifact_id = " + bbaName + ".artifact_id\n"; + } + + /** + * Given a column key, creates the full name for the column key. + * + * @param key The column key. + * + * @return The full identifier for the column key. + */ + private static String getFullKey(String key) { + return key + QUERY_SUFFIX + "." + key; + } + + /** + * Constructs a SQL 'where' statement from a list of clauses and puts + * parenthesis around each clause. + * + * @param clauses The clauses + * + * @return The generated 'where' statement. + */ + private static String getWhereString(List clauses) { + if (clauses.isEmpty()) { + return ""; + } + + List parenthesized = clauses.stream() + .map(c -> "(" + c + ")") + .collect(Collectors.toList()); + + return "\nWHERE " + String.join("\n AND ", parenthesized) + "\n"; + } + + /** + * Generates a [column] LIKE sql clause. + * + * @param column The column identifier. + * @param likeString The string that will be used as column comparison. + * @param isLike if false, the statement becomes NOT LIKE. + * + * @return The generated statement. + */ + private static String getLikeClause(String column, String likeString, boolean isLike) { + return column + (isLike ? "" : " NOT") + " LIKE '" + likeString + "'"; + } + + /** + * Retrieves a list of the top programs used on the data source. Currently + * determines this based off of which prefetch results return the highest + * count. + * + * @param dataSource The data source. + * @param count The number of programs to return. + * + * @return + */ + static List getTopPrograms(DataSource dataSource, int count) { + if (dataSource == null || count <= 0) { + return Collections.emptyList(); + } + + // ntosboot should be ignored + final String ntosBootIdentifier = "NTOSBOOT"; + // programs in windows directory to be ignored + final String windowsDir = "/WINDOWS%"; + + final String nameParam = "name"; + final String pathParam = "path"; + final String runCountParam = "run_count"; + final String lastRunParam = "last_run"; + + String bbaQuery = "bba"; + + final String query = "SELECT\n" + + " " + getFullKey(nameParam) + " AS " + nameParam + ",\n" + + " " + getFullKey(pathParam) + " AS " + pathParam + ",\n" + + " MAX(" + getFullKey(runCountParam) + ") AS " + runCountParam + ",\n" + + " MAX(" + getFullKey(lastRunParam) + ") AS " + lastRunParam + "\n" + + "FROM blackboard_artifacts " + bbaQuery + "\n" + + getAttributeJoin(JoinType.INNER, AttributeColumn.value_text, ATTRIBUTE_TYPE.TSK_PROG_NAME, nameParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_text, ATTRIBUTE_TYPE.TSK_PATH, pathParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int32, ATTRIBUTE_TYPE.TSK_COUNT, runCountParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int64, ATTRIBUTE_TYPE.TSK_DATETIME, lastRunParam, bbaQuery) + + getWhereString(Arrays.asList( + bbaQuery + ".artifact_type_id = " + ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), + bbaQuery + ".data_source_obj_id = " + dataSource.getId(), + // exclude ntosBootIdentifier from results + getLikeClause(getFullKey(nameParam), ntosBootIdentifier, false), + // exclude windows directory items from results + getFullKey(pathParam) + " IS NULL OR " + getLikeClause(getFullKey(pathParam), windowsDir, false) + )) + + "GROUP BY " + getFullKey(nameParam) + ", " + getFullKey(pathParam) + "\n" + + "ORDER BY \n" + + " MAX(" + getFullKey(runCountParam) + ") DESC,\n" + + " MAX(" + getFullKey(lastRunParam) + ") DESC,\n" + + " " + getFullKey(nameParam) + " ASC"; + + final String errorMessage = "Unable to get top program results; returning null."; + + ResultSetHandler> handler = (resultSet) -> { + List progResults = new ArrayList<>(); + + boolean quitAtCount = false; + + while (resultSet.next() && (!quitAtCount || progResults.size() < count)) { try { - toRet.put(resultSet.getString(nameParam), resultSet.getLong(valueParam)); + long lastRunEpoch = resultSet.getLong(lastRunParam); + Date lastRun = (resultSet.wasNull()) ? null : new Date(lastRunEpoch * 1000); + + Long runCount = resultSet.getLong(runCountParam); + if (resultSet.wasNull()) { + runCount = null; + } + + if (lastRun != null || runCount != null) { + quitAtCount = true; + } + + progResults.add(new TopProgramsResult( + resultSet.getString(nameParam), + resultSet.getString(pathParam), + runCount, + lastRun)); + } catch (SQLException ex) { - logger.log(Level.WARNING, "Failed to get a result pair from the result set.", ex); + logger.log(Level.WARNING, "Failed to get a top program result from the result set.", ex); } } - return toRet; + return progResults; }; - String errorMessage = "Unable to get artifact type counts; returning null."; - return getBaseQueryResult(query, handler, errorMessage); } + /** + * Functions that determine the folder name of a list of path elements. If + * not matched, function returns null. + */ + private static final List, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( + // handle Program Files and Program Files (x86) - if true, return the next folder + (pathList) -> { + if (pathList.size() < 2) { + return null; + } + + String rootParent = pathList.get(0).toUpperCase(); + if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { + return pathList.get(1); + } else { + return null; + } + }, + // if there is a folder named "APPLICATION DATA" or "APPDATA" + (pathList) -> { + for (String pathEl : pathList) { + String uppered = pathEl.toUpperCase(); + if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { + return "AppData"; + } + } + return null; + } + ); + + /** + * Determines a short folder name if any. Otherwise, returns empty string. + * + * @param strPath The string path. + * + * @return The short folder name or empty string if not found. + */ + static String getShortFolderName(String strPath, String applicationName) { + if (strPath == null) { + return ""; + } + + List pathEls = new ArrayList<>(Arrays.asList(applicationName)); + + File file = new File(strPath); + while (file != null && StringUtils.isNotBlank(file.getName())) { + pathEls.add(file.getName()); + file = file.getParentFile(); + } + + Collections.reverse(pathEls); + + for (Function, String> matchEntry : SHORT_FOLDER_MATCHERS) { + String result = matchEntry.apply(pathEls); + if (StringUtils.isNotBlank(result)) { + return result; + } + } + + return ""; + } + /** * Generates a string which is a concatenation of the value received from * the result set. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index e1197fa83a..9cd7549c06 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -49,10 +49,7 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser * datasource. */ @Messages({ - "DataSourceSummaryDialog.window.title=Data Sources Summary", - "DataSourceSummaryDialog.countsTab.title=Counts", - "DataSourceSummaryDialog.detailsTab.title=Details", - "DataSourceSummaryDialog.ingestHistoryTab.title=Ingest History" + "DataSourceSummaryDialog.window.title=Data Sources Summary" }) DataSourceSummaryDialog(Frame owner) { super(owner, Bundle.DataSourceSummaryDialog_window_title(), true); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java index e1d15a6b0d..dddc4ee123 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.casemodule.datasourcesummary; import javax.swing.JTabbedPane; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; import org.sleuthkit.datamodel.DataSource; @@ -27,13 +28,20 @@ import org.sleuthkit.datamodel.DataSource; * DataSourceSummaryCountsPanel, DataSourceSummaryDetailsPanel, and * IngestJobInfoPanel. */ +@Messages({ + "DataSourceSummaryTabbedPane_countsTab_title=Counts", + "DataSourceSummaryTabbedPane_detailsTab_title=Details", + "DataSourceSummaryTabbedPane_userActivityTab_title=User Activity", + "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History" +}) public class DataSourceSummaryTabbedPane extends JTabbedPane { private static final long serialVersionUID = 1L; - - private final DataSourceSummaryCountsPanel countsPanel; - private final DataSourceSummaryDetailsPanel detailsPanel; - private final IngestJobInfoPanel ingestHistoryPanel; + + private final DataSourceSummaryCountsPanel countsPanel = new DataSourceSummaryCountsPanel(); + private final DataSourceSummaryDetailsPanel detailsPanel = new DataSourceSummaryDetailsPanel(); + private final DataSourceSummaryUserActivityPanel userActivityPanel = new DataSourceSummaryUserActivityPanel(); + private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); private DataSource dataSource = null; @@ -41,13 +49,11 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { * Constructs a tabbed pane showing the summary of a data source. */ public DataSourceSummaryTabbedPane() { - countsPanel = new DataSourceSummaryCountsPanel(); - detailsPanel = new DataSourceSummaryDetailsPanel(); - ingestHistoryPanel = new IngestJobInfoPanel(); - - addTab(Bundle.DataSourceSummaryDialog_detailsTab_title(), detailsPanel); - addTab(Bundle.DataSourceSummaryDialog_countsTab_title(), countsPanel); - addTab(Bundle.DataSourceSummaryDialog_ingestHistoryTab_title(), ingestHistoryPanel); + + addTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), detailsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), countsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), userActivityPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel); } /** @@ -69,6 +75,7 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { detailsPanel.setDataSource(dataSource); countsPanel.setDataSource(dataSource); + userActivityPanel.setDataSource(dataSource); ingestHistoryPanel.setDataSource(dataSource); } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form new file mode 100644 index 0000000000..b938286e96 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form @@ -0,0 +1,70 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java new file mode 100644 index 0000000000..c97e2ec18c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java @@ -0,0 +1,291 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.datasourcesummary; + +import java.awt.Component; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.DataSource; + +/** + * A panel to display user activity. + */ +@Messages({ + "DataSourceSummaryUserActivityPanel_tab_title=User Activity", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run" +}) +public class DataSourceSummaryUserActivityPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); + private static final int TOP_PROGS_COUNT = 10; + private static final DefaultTableCellRenderer RIGHT_ALIGNED_RENDERER = new DefaultTableCellRenderer(); + + static { + RIGHT_ALIGNED_RENDERER.setHorizontalAlignment(JLabel.RIGHT); + } + + private DataSource dataSource; + + /** + * Creates new form DataSourceUserActivityPanel + */ + public DataSourceSummaryUserActivityPanel() { + initComponents(); + topProgramsTable.getTableHeader().setReorderingAllowed(false); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + if (dataSource == null || !Case.isCaseOpen()) { + updateTopPrograms(new TopProgramsModel(null)); + } else { + updateTopPrograms(getTopProgramsModel(dataSource)); + } + } + + /** + * Updates the Top Programs Table in the gui. + * + * @param data The data in Object[][] form to be used by the + * DefaultTableModel. + */ + private void updateTopPrograms(TopProgramsModel model) { + topProgramsTable.setModel(model); + topProgramsTable.getColumnModel().getColumn(0).setPreferredWidth(250); + topProgramsTable.getColumnModel().getColumn(0).setCellRenderer(PATH_CELL_RENDERER); + topProgramsTable.getColumnModel().getColumn(1).setPreferredWidth(150); + topProgramsTable.getColumnModel().getColumn(2).setCellRenderer(RIGHT_ALIGNED_RENDERER); + topProgramsTable.getColumnModel().getColumn(2).setPreferredWidth(80); + topProgramsTable.getColumnModel().getColumn(3).setPreferredWidth(150); + topProgramsScrollPane.getVerticalScrollBar().setValue(0); + this.repaint(); + } + + /** + * The counts of top programs run. + * + * @param selectedDataSource The DataSource. + * + * @return The JTable data model of counts of program runs. + */ + private static TopProgramsModel getTopProgramsModel(DataSource selectedDataSource) { + List topProgramList + = DataSourceInfoUtilities.getTopPrograms(selectedDataSource, TOP_PROGS_COUNT); + + if (topProgramList == null) { + return new TopProgramsModel(null); + } else { + return new TopProgramsModel(topProgramList); + } + } + + /** + * A POJO defining the values present in the name cell. Defines the name as + * well as the path for the tooltip. + */ + private static class ProgramNameCellValue { + + private final String programName; + private final String programPath; + + ProgramNameCellValue(String programName, String programPath) { + this.programName = programName; + this.programPath = programPath; + } + + @Override + public String toString() { + // override so that the value in the cell reads as programName + return programName; + } + + /** + * @return The program name. + */ + String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + String getProgramPath() { + return programPath; + } + } + + /** + * Defines a cell renderer for the first cell rendering the name as the text + * and path as the tooltip. + */ + private static TableCellRenderer PATH_CELL_RENDERER = new DefaultTableCellRenderer() { + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) { + JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof ProgramNameCellValue) { + ProgramNameCellValue cellValue = (ProgramNameCellValue) value; + c.setToolTipText(cellValue.getProgramPath()); + } + return c; + } + }; + + /** + * Defines the table model for a JTable of the programs. Accepts a list of + * TopProgramsResult objects as rows data source. + */ + private static class TopProgramsModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + + // column headers for artifact counts table + private static final String[] TOP_PROGS_COLUMN_HEADERS = new String[]{ + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header() + }; + + private final List programResults; + + /** + * Main constructor. + * + * @param programResults The results to display. + */ + TopProgramsModel(List programResults) { + this.programResults = programResults == null ? new ArrayList<>() : Collections.unmodifiableList(programResults); + } + + @Override + public String getColumnName(int column) { + return column < 0 || column >= TOP_PROGS_COLUMN_HEADERS.length ? null : TOP_PROGS_COLUMN_HEADERS[column]; + } + + @Override + public int getRowCount() { + return programResults.size(); + } + + @Override + public int getColumnCount() { + return TOP_PROGS_COLUMN_HEADERS.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= programResults.size()) { + return null; + } + + DataSourceInfoUtilities.TopProgramsResult result = programResults.get(rowIndex); + switch (columnIndex) { + case 0: + return new ProgramNameCellValue(result.getProgramName(), result.getProgramPath()); + case 1: + return DataSourceInfoUtilities.getShortFolderName(result.getProgramPath(), result.getProgramName()); + case 2: + return result.getRunTimes(); + case 3: + return result.getLastRun() == null ? null : DATETIME_FORMAT.format(result.getLastRun()); + default: + return null; + } + } + + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JLabel programsRunLabel = new javax.swing.JLabel(); + topProgramsScrollPane = new javax.swing.JScrollPane(); + topProgramsTable = new javax.swing.JTable(); + + org.openide.awt.Mnemonics.setLocalizedText(programsRunLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.programsRunLabel.text")); // NOI18N + + topProgramsScrollPane.setPreferredSize(new java.awt.Dimension(750, 187)); + topProgramsScrollPane.setViewportView(topProgramsTable); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(programsRunLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 460, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(128, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(programsRunLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane topProgramsScrollPane; + private javax.swing.JTable topProgramsTable; + // End of variables declaration//GEN-END:variables +}