diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties deleted file mode 100644 index b320647823fdf858ad9b13268643d9535525c1f7..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties +++ /dev/null @@ -1,33 +0,0 @@ -OpenIDE-Module-Display-Category=External Viewers -OpenIDE-Module-Long-Description=\ - Displays user activity as an interactive timeline chart with year, month and day granularity. \n\ - Events for a selected day are viewable in the built-in result and content viewers. -OpenIDE-Module-Name=Timeline -CTL_MakeTimeline="Make Timeline (Beta)" -CTL_TimelineView=Generate Timeline -OpenIDE-Module-Short-Description=Displays user activity timeline -TimelineProgressDialog.jLabel1.text=Creating timeline . . . -TimelineFrame.title=Timeline -Timeline.frameName.text={0} - Autopsy Timeline (Beta) -Timeline.resultsPanel.title=Timeline Results -Timeline.getName=Make Timeline (Beta) -Timeline.runJavaFxThread.progress.creating=Creating timeline . . . -Timeline.runJavaFxThread.progress.genBodyFile=Generating Bodyfile -Timeline.runJavaFxThread.progress.genMacTime=Generating Mactime -Timeline.runJavaFxThread.progress.parseMacTime=Parsing Mactime -Timeline.zoomOutButton.text=Zoom Out -Timeline.goToButton.text=Go To\: -Timeline.yearBarChart.x.years=Years -Timeline.yearBarChart.y.numEvents=Number of Events -Timeline.MonthsBarChart.x.monthYY=Month ({0}) -Timeline.MonthsBarChart.y.numEvents=Number of Events -Timeline.eventsByMoBarChart.x.dayOfMo=Day of Month -Timeline.eventsByMoBarChart.y.numEvents=Number of Events -Timeline.node.emptyRoot=Empty Root -Timeline.resultPanel.loading=Loading... -Timeline.node.root=Root -Timeline.propChg.confDlg.timelineOOD.msg=Timeline is out of date. Would you like to regenerate it? -Timeline.propChg.confDlg.timelineOOD.details=Select an option -Timeline.initTimeline.confDlg.genBeforeIngest.msg=You are trying to generate a timeline before ingest has been completed. The timeline may be incomplete. Do you want to continue? -Timeline.initTimeline.confDlg.genBeforeIngest.deails=Timeline -TimelineProgressDialog.setName.text=Make Timeline (Beta) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties deleted file mode 100644 index b905bd12053d786f21c70d3ed2c1a08983688293..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties +++ /dev/null @@ -1,33 +0,0 @@ -OpenIDE-Module-Display-Category=\u5916\u90E8\u30D3\u30E5\u30FC\u30A2 -OpenIDE-Module-Long-Description=\ - \u30E6\u30FC\u30B6\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3\u3092\u5E74\u3001\u6708\u3001\u65E5\u306E\u5358\u4F4D\u3067\u3001\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6\u306A\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C1\u30E3\u30FC\u30C8\u3068\u3057\u3066\u8868\u793A\u3057\u307E\u3059\u3002\n\ - \u9078\u629E\u3057\u305F\u65E5\u306E\u30A4\u30D9\u30F3\u30C8\u306F\u5185\u8535\u306E\u7D50\u679C\u304A\u3088\u3073\u30B3\u30F3\u30C6\u30F3\u30C4\u30D3\u30E5\u30FC\u30A2\u3067\u78BA\u8A8D\u3067\u304D\u307E\u3059\u3002 -OpenIDE-Module-Name=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -CTL_MakeTimeline="\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09" -CTL_TimelineView=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210 -OpenIDE-Module-Short-Description=\u30E6\u30FC\u30B6\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u8868\u793A -TimelineProgressDialog.jLabel1.text=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u4E2D\u2026 -TimelineFrame.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -Timeline.frameName.text={0} - Autopsy\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\uFF08\u30D9\u30FC\u30BF\uFF09 -Timeline.resultsPanel.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u7D50\u679C -Timeline.getName=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09 -Timeline.runJavaFxThread.progress.creating=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u4E2D\u2026 -Timeline.runJavaFxThread.progress.genBodyFile=Bodyfile\u3092\u4F5C\u6210\u4E2D -Timeline.runJavaFxThread.progress.genMacTime=MAC\u30BF\u30A4\u30E0\u3092\u4F5C\u6210\u4E2D -Timeline.runJavaFxThread.progress.parseMacTime=MAC\u30BF\u30A4\u30E0\u3092\u30D1\u30FC\u30B9\u4E2D -Timeline.zoomOutButton.text=\u7E2E\u5C0F -Timeline.goToButton.text=\u4E0B\u8A18\u3078\u79FB\u52D5\uFF1A -Timeline.yearBarChart.x.years=\u5E74 -Timeline.yearBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570 -Timeline.MonthsBarChart.x.monthYY=\u6708 ({0}) -Timeline.MonthsBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570 -Timeline.eventsByMoBarChart.x.dayOfMo=\u65E5 -Timeline.eventsByMoBarChart.y.numEvents=\u30A4\u30D9\u30F3\u30C8\u6570 -Timeline.node.emptyRoot=\u7A7A\u306E\u30EB\u30FC\u30C8 -Timeline.resultPanel.loading=\u30ED\u30FC\u30C9\u4E2D\u2026 -Timeline.node.root=\u30EB\u30FC\u30C8 -Timeline.propChg.confDlg.timelineOOD.msg=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u6700\u65B0\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u518D\u5EA6\u4F5C\u6210\u3057\u307E\u3059\u304B\uFF1F -Timeline.propChg.confDlg.timelineOOD.details=\u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u9078\u629E\u3057\u3066\u4E0B\u3055\u3044 -Timeline.initTimeline.confDlg.genBeforeIngest.msg=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u304C\u5B8C\u4E86\u3059\u308B\u524D\u306B\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307E\u3059\u3002\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u4E0D\u5B8C\u5168\u306B\u306A\u308B\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\u7D9A\u884C\u3057\u307E\u3059\u304B\uFF1F -Timeline.initTimeline.confDlg.genBeforeIngest.deails=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -TimelineProgressDialog.setName.text=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\uFF08\u30D9\u30FC\u30BF\uFF09 \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java b/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java deleted file mode 100644 index 0e8cb2111060b7fa3663a541fb6cd0c5df30b587..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/Timeline.java +++ /dev/null @@ -1,1237 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.timeline; - -import java.awt.Component; -import java.awt.Cursor; -import java.awt.Dimension; -import java.awt.EventQueue; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.io.BufferedWriter; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Scanner; -import java.util.Stack; -import java.util.TimeZone; -import java.util.logging.Level; -import java.util.prefs.Preferences; -import javafx.application.Platform; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.embed.swing.JFXPanel; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Pos; -import javafx.scene.Group; -import javafx.scene.Scene; -import javafx.scene.chart.BarChart; -import javafx.scene.chart.CategoryAxis; -import javafx.scene.chart.NumberAxis; -import javafx.scene.control.Button; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javax.swing.JFrame; -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import org.netbeans.api.progress.ProgressHandle; -import org.netbeans.api.progress.ProgressHandleFactory; -import org.openide.awt.ActionID; -import org.openide.awt.ActionReference; -import org.openide.awt.ActionReferences; -import org.openide.awt.ActionRegistration; -import org.openide.modules.InstalledFileLocator; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.util.HelpCtx; -import org.openide.util.NbBundle; -import org.openide.util.NbPreferences; -import org.openide.util.actions.CallableSystemAction; -import org.openide.util.actions.Presenter; -import org.openide.util.lookup.Lookups; -import org.openide.windows.WindowManager; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.core.Installer; -import org.sleuthkit.autopsy.corecomponents.DataContentPanel; -import org.sleuthkit.autopsy.corecomponents.DataResultPanel; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.PlatformUtil; -import org.sleuthkit.autopsy.datamodel.FilterNodeLeaf; -import org.sleuthkit.autopsy.datamodel.DirectoryNode; -import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; -import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; -import org.sleuthkit.autopsy.datamodel.FileNode; -import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.autopsy.coreutils.ExecUtil; -import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; - -@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline") -@ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false) -@ActionReferences(value = { - @ActionReference(path = "Menu/Tools", position = 100)}) -//@NbBundle.Messages(value = "CTL_TimelineView=Generate Timeline") -/** - * The Timeline Action entry point. Collects data and pushes data to javafx - * widgets - * - */ -public class Timeline extends CallableSystemAction implements Presenter.Toolbar, PropertyChangeListener { - - private static final Logger logger = Logger.getLogger(Timeline.class.getName()); - private final java.io.File macRoot = InstalledFileLocator.getDefault().locate("mactime", Timeline.class.getPackage().getName(), false); //NON-NLS - private TimelineFrame mainFrame; //frame for holding all the elements - private Group fxGroupCharts; //Orders the charts - private Scene fxSceneCharts; //Displays the charts - private HBox fxHBoxCharts; //Holds the navigation buttons in horiztonal fashion. - private VBox fxVBox; //Holds the JavaFX Elements in vertical fashion. - private JFXPanel fxPanelCharts; //FX panel to hold the group - private BarChart<String, Number> fxChartEvents; //Yearly/Monthly events - Bar chart - private ScrollPane fxScrollEvents; //Scroll Panes for dealing with oversized an oversized chart - private static final int FRAME_HEIGHT = 700; //Sizing constants - private static final int FRAME_WIDTH = 1200; - private Button fxZoomOutButton; //Navigation buttons - private ComboBox<String> fxDropdownSelectYears; //Dropdown box for selecting years. Useful when the charts' scale means some years are unclickable, despite having events. - private final Stack<BarChart<String, Number>> fxStackPrevCharts = new Stack<BarChart<String, Number>>(); //Stack for storing drill-up information. - private BarChart<String, Number> fxChartTopLevel; //the topmost chart, used for resetting to default view. - private BarChart<String, Number> fxMonthView; //the month chart - private DataResultPanel dataResultPanel; - private DataContentPanel dataContentPanel; - private ProgressHandle progress; - private java.io.File moduleDir; - private String mactimeFileName; - private List<YearEpoch> data; - private boolean listeningToAddImage = false; - private long lastObjectId = -1; - private TimelineProgressDialog progressDialog; - private EventHandler<MouseEvent> fxMouseEnteredListener; - private EventHandler<MouseEvent> fxMouseExitedListener; - private SleuthkitCase skCase; - private boolean fxInited = false; - private int monthCounter = 0; - - public Timeline() { - super(); - - fxInited = Installer.isJavaFxInited(); - // TimeZone.setDefault(TimeZone.getTimeZone("UTC")); //sets the default timezone to UTC unless otherwise stated - } - - //Swing components and JavafX components don't play super well together - //Swing components need to be initialized first, in the swing specific thread - //Next, the javafx components may be initialized. - private void customize() { - - //listeners - fxMouseEnteredListener = new EventHandler<MouseEvent>() { - @Override - public void handle(MouseEvent e) { - fxPanelCharts.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - } - }; - fxMouseExitedListener = new EventHandler<MouseEvent>() { - @Override - public void handle(MouseEvent e) { - fxPanelCharts.setCursor(null); - } - }; - - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - //Making the main frame * - - mainFrame = new TimelineFrame(); - mainFrame.setFrameName( - NbBundle.getMessage(this.getClass(), "Timeline.frameName.text", Case.getCurrentCase().getName())); - - //use the same icon on jframe as main application - mainFrame.setIconImage(WindowManager.getDefault().getMainWindow().getIconImage()); - mainFrame.setFrameSize(new Dimension(FRAME_WIDTH, FRAME_HEIGHT)); //(Width, Height) - - - dataContentPanel = DataContentPanel.createInstance(); - //dataContentPanel.setAlignmentX(Component.RIGHT_ALIGNMENT); - //dataContentPanel.setPreferredSize(new Dimension(FRAME_WIDTH, (int) (FRAME_HEIGHT * 0.4))); - - dataResultPanel = DataResultPanel.createInstance( - NbBundle.getMessage(this.getClass(), "Timeline.resultsPanel.title"), - "", Node.EMPTY, 0, dataContentPanel); - dataResultPanel.setContentViewer(dataContentPanel); - //dataResultPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - //dataResultPanel.setPreferredSize(new Dimension((int)(FRAME_WIDTH * 0.5), (int) (FRAME_HEIGHT * 0.5))); - logger.log(Level.INFO, "Successfully created viewers"); //NON-NLS - - mainFrame.setBottomLeftPanel(dataResultPanel); - mainFrame.setBottomRightPanel(dataContentPanel); - - runJavaFxThread(); - } - }); - - - } - - private void runJavaFxThread() { - //JavaFX thread - //JavaFX components MUST be run in the JavaFX thread, otherwise massive amounts of exceptions will be thrown and caught. Liable to freeze up and crash. - //Components can be declared whenever, but initialization and manipulation must take place here. - Platform.runLater(new Runnable() { - @Override - public void run() { - try { - // start the progress bar - progress = ProgressHandleFactory.createHandle( - NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.creating")); - progress.start(); - - fxChartEvents = null; //important to reset old data - fxPanelCharts = new JFXPanel(); - fxGroupCharts = new Group(); - fxSceneCharts = new Scene(fxGroupCharts, FRAME_WIDTH, FRAME_HEIGHT * 0.6); //Width, Height - fxVBox = new VBox(5); - fxVBox.setAlignment(Pos.BOTTOM_CENTER); - fxHBoxCharts = new HBox(10); - fxHBoxCharts.setAlignment(Pos.BOTTOM_CENTER); - - //Initializing default values for the scroll pane - fxScrollEvents = new ScrollPane(); - fxScrollEvents.setPrefSize(FRAME_WIDTH, FRAME_HEIGHT * 0.6); //Width, Height - fxScrollEvents.setContent(null); //Needs some content, otherwise it crashes - - // set up moduleDir - moduleDir = new java.io.File(Case.getCurrentCase().getModulesOutputDirAbsPath() + java.io.File.separator + "timeline"); - if (!moduleDir.exists()) { - moduleDir.mkdir(); - } - - int currentProgress = 0; - java.io.File mactimeFile = new java.io.File(moduleDir, mactimeFileName); - if (!mactimeFile.exists()) { - progressDialog.setProgressTotal(3); //total 3 units - logger.log(Level.INFO, "Creating body file"); //NON-NLS - progressDialog.updateProgressBar( - NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.genBodyFile")); - String bodyFilePath = makeBodyFile(); - progressDialog.updateProgressBar(++currentProgress); - logger.log(Level.INFO, "Creating mactime file: " + mactimeFile.getAbsolutePath()); //NON-NLS - progressDialog.updateProgressBar( - NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.genMacTime")); - makeMacTime(bodyFilePath); - progressDialog.updateProgressBar(++currentProgress); - data = null; - } else { - progressDialog.setProgressTotal(1); //total 1 units - logger.log(Level.INFO, "Mactime file already exists; parsing that: " + mactimeFile.getAbsolutePath()); //NON-NLS - } - - - progressDialog.updateProgressBar( - NbBundle.getMessage(this.getClass(), "Timeline.runJavaFxThread.progress.parseMacTime")); - if (data == null) { - logger.log(Level.INFO, "Parsing mactime file: " + mactimeFile.getAbsolutePath()); //NON-NLS - data = parseMacTime(mactimeFile); //The sum total of the mactime parsing. YearEpochs contain everything you need to make a timeline. - } - progressDialog.updateProgressBar(++currentProgress); - - //Making a dropdown box to select years. - List<String> lsi = new ArrayList<String>(); //List is in the format of {Year : Number of Events}, used for selecting from the dropdown. - for (YearEpoch ye : data) { - lsi.add(ye.year + " : " + ye.getNumFiles()); - } - ObservableList<String> listSelect = FXCollections.observableArrayList(lsi); - fxDropdownSelectYears = new ComboBox<String>(listSelect); - - //Buttons for navigating up and down the timeline - fxZoomOutButton = new Button(NbBundle.getMessage(this.getClass(), "Timeline.zoomOutButton.text")); - fxZoomOutButton.setOnAction(new EventHandler<ActionEvent>() { - @Override - public void handle(ActionEvent e) { - BarChart<String, Number> bc; - if (fxStackPrevCharts.size() == 0) { - bc = fxChartTopLevel; - } else { - bc = fxStackPrevCharts.pop(); - } - fxChartEvents = bc; - fxScrollEvents.setContent(fxChartEvents); - } - }); - - fxDropdownSelectYears.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() { - @Override - public void changed(ObservableValue<? extends String> ov, String t, String t1) { - if (fxDropdownSelectYears.getValue() != null) { - mainFrame.setTopComponentCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - try { - fxChartEvents = createMonthsWithDrill(findYear(data, Integer.valueOf(fxDropdownSelectYears.getValue().split(" ")[0]))); - fxScrollEvents.setContent(fxChartEvents); - } finally { - mainFrame.setTopComponentCursor(null); - } - } - } - }); - - //Adding things to the V and H boxes. - //hBox_Charts stores the pseudo menu bar at the top of the timeline. |Zoom Out|View Year: [Select Year]|►| - fxHBoxCharts.getChildren().addAll(fxZoomOutButton, new Label( - NbBundle.getMessage(this.getClass(), "Timeline.goToButton.text")), fxDropdownSelectYears); - fxVBox.getChildren().addAll(fxHBoxCharts, fxScrollEvents); //FxBox_V holds things in a visual stack. - fxGroupCharts.getChildren().add(fxVBox); //Adding the FxBox to the group. Groups make things easier to manipulate without having to update a hundred things every change. - fxPanelCharts.setScene(fxSceneCharts); - - - fxPanelCharts.setAlignmentX(Component.LEFT_ALIGNMENT); - - fxChartTopLevel = createYearChartWithDrill(data); - fxChartEvents = fxChartTopLevel; - fxScrollEvents.setContent(fxChartEvents); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - mainFrame.setTopPanel(fxPanelCharts); - dataResultPanel.open(); - //mainFrame.pack(); - mainFrame.setVisible(true); - } - }); - } finally { - // stop the progress bar - progress.finish(); - - // close the progressDialog - progressDialog.doClose(0); - } - } - }); - } - - /** - * Creates a BarChart with datapoints for all the years from the parsed - * mactime file. - * - * @param allYears The list of years that have barData from the mactime file - * @return BarChart scaled to the year level - */ - private BarChart<String, Number> createYearChartWithDrill(final List<YearEpoch> allYears) { - final CategoryAxis xAxis = new CategoryAxis(); //Axes are very specific types. Categorys are strings. - final NumberAxis yAxis = new NumberAxis(); - final Label l = new Label(""); - l.setStyle("-fx-font: 24 arial;"); //NON-NLS - l.setTextFill(Color.AZURE); - xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.yearBarChart.x.years")); - yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.yearBarChart.y.numEvents")); - //Charts are made up of individual pieces of Chart.Data. In this case, a piece of barData is a single bar on the graph. - //Data is packaged into a series, which can be assigned custom colors or styling - //After the series are created, 1 or more series are packaged into a single chart. - ObservableList<BarChart.Series<String, Number>> bcData = FXCollections.observableArrayList(); - BarChart.Series<String, Number> se = new BarChart.Series<String, Number>(); - if (allYears != null) { - for (final YearEpoch ye : allYears) { - se.getData().add(new BarChart.Data<String, Number>(String.valueOf(ye.year), ye.getNumFiles())); - } - } - bcData.add(se); - - - //Note: - // BarChart.Data wraps the Java Nodes class. BUT, until a BarChart.Data gets added to an actual series, it's node is null, and you can perform no operations on it. - // When the Data is added to a series(or a chart? I am unclear on where), a node is automaticaly generated for it, after which you can perform any of the operations it offers. - // In addtion, you are free to set the node to whatever you want. It wraps the most generic Node class. - // But it is for this reason that the chart generating functions have two forloops. I do not believe they can be condensed into a single loop due to the nodes being null until - // an undetermined point in time. - BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, bcData); - for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) { //.get(0) refers to the BarChart.Series class to work on. There is only one series in this graph, so get(0) is safe. - barData.getNode().setScaleX(.5); - - final javafx.scene.Node barNode = barData.getNode(); - //hover listener - barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener); - barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener); - - //click listener - barNode.addEventHandler(MouseEvent.MOUSE_CLICKED, - new EventHandler<MouseEvent>() { - @Override - public void handle(MouseEvent e) { - if (e.getButton().equals(MouseButton.PRIMARY)) { - if (e.getClickCount() == 1) { - Platform.runLater(new Runnable() { - @Override - public void run() { - BarChart<String, Number> b = - createMonthsWithDrill(findYear(allYears, Integer.valueOf(barData.getXValue()))); - fxChartEvents = b; - fxScrollEvents.setContent(fxChartEvents); - } - }); - - } - } - } - }); - } - - bc.autosize(); //Get an auto height - bc.setPrefWidth(FRAME_WIDTH); //but override the width - bc.setLegendVisible(false); //The legend adds too much extra chart space, it's not necessary. - return bc; - } - - /* - * Displays a chart with events from one year only, separated into 1-month chunks. - * Always 12 per year, empty months are represented by no bar. - */ - private BarChart<String, Number> createMonthsWithDrill(final YearEpoch ye) { - final CategoryAxis xAxis = new CategoryAxis(); - final NumberAxis yAxis = new NumberAxis(); - xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.MonthsBarChart.x.monthYY", ye.year)); - yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.MonthsBarChart.y.numEvents")); - ObservableList<BarChart.Series<String, Number>> bcData = FXCollections.observableArrayList(); - - BarChart.Series<String, Number> se = new BarChart.Series<String, Number>(); - for (int monthNum = 0; monthNum < 12; ++monthNum) { - String monthName = new DateFormatSymbols().getMonths()[monthNum]; - MonthEpoch month = ye.getMonth(monthNum); - int numEvents = month == null ? 0 : month.getNumFiles(); - se.getData().add(new BarChart.Data<String, Number>(monthName, numEvents)); //Adding new barData at {X-pos, Y-Pos} - } - bcData.add(se); - final BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, bcData); - - for (int i = 0; i < 12; i++) { - for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) { - //Note: - // All the charts of this package have a problem where when the chart gets below a certain pixel ratio, the barData stops drawing. The axes and the labels remain, - // But the actual chart barData is invisible, unclickable, and unrendered. To partially compensate for that, barData.getNode() can be manually scaled up to increase visibility. - // Sometimes I've had it jacked up to as much as x2400 just to see a sliver of information. - // But that doesn't work all the time. Adding it to a scrollpane and letting the user scroll up and down to view the chart is the other workaround. Both of these fixes suck. - final javafx.scene.Node barNode = barData.getNode(); - barNode.setScaleX(.5); - - //hover listener - barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener); - barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener); - - //clicks - barNode.addEventHandler(MouseEvent.MOUSE_PRESSED, - new EventHandler<MouseEvent>() { - @Override - public void handle(MouseEvent e) { - if (e.getButton().equals(MouseButton.PRIMARY)) { - if (e.getClickCount() == 1) { - Platform.runLater(new Runnable() { - @Override - public void run() { - fxChartEvents = createEventsByMonth(findMonth(ye.months, monthStringToInt(barData.getXValue())), ye); - fxScrollEvents.setContent(fxChartEvents); - } - }); - } - } - } - }); - } - } - - bc.autosize(); - bc.setPrefWidth(FRAME_WIDTH); - bc.setLegendVisible(false); - fxMonthView= bc; - return bc; - } - - - /* - * Displays a chart with events from one month only. - * Up to 31 days per month, as low as 28 as determined by the specific MonthEpoch - */ - @SuppressWarnings("unchecked") - private BarChart<String, Number> createEventsByMonth(final MonthEpoch me, final YearEpoch ye) { - final CategoryAxis xAxis = new CategoryAxis(); - final NumberAxis yAxis = new NumberAxis(); - xAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.eventsByMoBarChart.x.dayOfMo")); - yAxis.setLabel(NbBundle.getMessage(this.getClass(), "Timeline.eventsByMoBarChart.y.numEvents")); - ObservableList<BarChart.Data<String, Number>> bcData = makeObservableListByMonthAllDays(me, ye.getYear()); - BarChart.Series<String, Number> series = new BarChart.Series<String, Number>(bcData); - series.setName(me.getMonthName() + " " + ye.getYear()); - - - ObservableList<BarChart.Series<String, Number>> ol = - FXCollections.<BarChart.Series<String, Number>>observableArrayList(series); - - final BarChart<String, Number> bc = new BarChart<String, Number>(xAxis, yAxis, ol); - for (final BarChart.Data<String, Number> barData : bc.getData().get(0).getData()) { - //data.getNode().setScaleX(2); - - final javafx.scene.Node barNode = barData.getNode(); - - //hover listener - barNode.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, fxMouseEnteredListener); - barNode.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, fxMouseExitedListener); - - barNode.addEventHandler(MouseEvent.MOUSE_PRESSED, - new EventHandler<MouseEvent>() { - MonthEpoch myme = me; - - @Override - public void handle(MouseEvent e) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - //reset the view and free the current nodes before loading new ones - final FileRootNode d = new FileRootNode( - NbBundle.getMessage(this.getClass(), "Timeline.node.emptyRoot"), new ArrayList<Long>()); - dataResultPanel.setNode(d); - dataResultPanel.setPath(NbBundle.getMessage(this.getClass(), "Timeline.resultPanel.loading")); - } - }); - final int day = (Integer.valueOf((barData.getXValue()).split("-")[1])); - final DayEpoch de = myme.getDay(day); - final List<Long> afs; - if (de != null) { - afs = de.getEvents(); - } else { - logger.log(Level.SEVERE, "There were no events for the clicked-on day: " + day); //NON-NLS - return; - } - - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - final FileRootNode d = new FileRootNode( - NbBundle.getMessage(this.getClass(), "Timeline.node.root"), afs); - dataResultPanel.setNode(d); - //set result viewer title path with the current date - String dateString = ye.getYear() + "-" + (1 + me.getMonthInt()) + "-" + +de.dayNum; - dataResultPanel.setPath(dateString); - } - }); - - - } - }); - } - bc.autosize(); - bc.setPrefWidth(FRAME_WIDTH); - monthCounter++; - if (monthCounter==12) - { - fxStackPrevCharts.push(fxMonthView); - monthCounter=0; - } - return bc; - } - - private static ObservableList<BarChart.Data<String, Number>> makeObservableListByMonthAllDays(final MonthEpoch me, int year) { - ObservableList<BarChart.Data<String, Number>> bcData = FXCollections.observableArrayList(); - int totalDays = me.getTotalNumDays(year); - for (int i = 1; i <= totalDays; ++i) { - DayEpoch day = me.getDay(i); - int numFiles = day == null ? 0 : day.getNumFiles(); - BarChart.Data<String, Number> d = new BarChart.Data<String, Number>(me.month + 1 + "-" + i, numFiles); - d.setExtraValue(me); - bcData.add(d); - } - return bcData; - } - - /* - * Section for Utility functions - */ - /** - * - * @param mon The month to convert. Must be minimum 4 characters long - * "February" and "Febr" are acceptable. - * @return The integer value of the month. February = 1, July = 6 - */ - private static int monthStringToInt(String mon) { - try { - Date date = new SimpleDateFormat("MMMM", Locale.ENGLISH).parse(mon); - Calendar cal = Calendar.getInstance(); - cal.setTime(date); - return cal.get(Calendar.MONTH); - } catch (ParseException ex) { - logger.log(Level.WARNING, "Unable to convert string " + mon + " to integer", ex); //NON-NLS - return -1; - } - } - - /** - * Used for finding the proper month in a list of available months - * - * @param lst The list of months to search through. It is assumed that the - * desired match is in this list. - * @param match The month, in integer format, to retrieve. - * @return The month epoch as specified by match. - */ - private static MonthEpoch findMonth(List<MonthEpoch> lst, int match) { - for (MonthEpoch e : lst) { - if (e.month == match) { - return e; - } - } - return null; - } - - /** - * Used for finding the proper year in a list of available years - * - * @param lst The list of years to search through. It is assumed that the - * desired match is in this list. - * @param match The year to retrieve. - * @return The year epoch as specified by match. - */ - private static YearEpoch findYear(List<YearEpoch> lst, int match) { - for (YearEpoch e : lst) { - if (e.year == match) { - return e; - } - } - return null; - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - String prop = evt.getPropertyName(); - if (prop.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - if (mainFrame != null && !mainFrame.isVisible()) { - // change the lastObjectId to trigger a reparse of mactime barData - ++lastObjectId; - return; - } - - int answer = JOptionPane.showConfirmDialog(mainFrame, - NbBundle.getMessage(this.getClass(), - "Timeline.propChg.confDlg.timelineOOD.msg"), - NbBundle.getMessage(this.getClass(), - "Timeline.propChg.confDlg.timelineOOD.details"), - JOptionPane.YES_NO_OPTION); - if (answer != JOptionPane.YES_OPTION) { - return; - } - - clearMactimeData(); - - // call performAction as if the user selected 'Make Timeline' from the menu - performAction(); - } else if (prop.equals(Case.Events.CURRENT_CASE.toString())) { - if (mainFrame != null && mainFrame.isVisible()) { - mainFrame.dispose(); - mainFrame = null; - } - - data = null; - } - } - - private void clearMactimeData() { - // get rid of the old barData - data = null; - - // get rid of the mactime file - java.io.File mactimeFile = new java.io.File(moduleDir, mactimeFileName); - mactimeFile.delete(); - - // close the jframe - if (mainFrame != null) { - mainFrame.setVisible(false); - mainFrame.dispose(); - mainFrame = null; - } - - // remove ourself as change listener on Case - Case.removePropertyChangeListener(this); - listeningToAddImage = false; - - } - - /* - * The backbone of the timeline functionality, years are split into months, months into days, and days contain the events of that given day. - * All of those are Epochs. - */ - abstract class Epoch { - - abstract public int getNumFiles(); - } - - private class YearEpoch extends Epoch { - - private int year; - private List<MonthEpoch> months = new ArrayList<>(); - - YearEpoch(int year) { - this.year = year; - } - - public int getYear() { - return year; - } - - @Override - public int getNumFiles() { - int size = 0; - for (MonthEpoch me : months) { - size += me.getNumFiles(); - } - return size; - } - - public MonthEpoch getMonth(int monthNum) { - MonthEpoch month = null; - for (MonthEpoch me : months) { - if (me.getMonthInt() == monthNum) { - month = me; - break; - } - } - return month; - } - - public void add(long fileId, int month, int day) { - // see if this month is in the list - MonthEpoch monthEpoch = null; - for (MonthEpoch me : months) { - if (me.getMonthInt() == month) { - monthEpoch = me; - break; - } - } - - if (monthEpoch == null) { - monthEpoch = new MonthEpoch(month); - months.add(monthEpoch); - } - - // add the file the the MonthEpoch object - monthEpoch.add(fileId, day); - } - } - - private class MonthEpoch extends Epoch { - - private int month; //Zero-indexed: June = 5, August = 7, etc - private List<DayEpoch> days = new ArrayList<>(); //List of DayEpochs in this month, max 31 - - MonthEpoch(int month) { - this.month = month; - } - - public int getMonthInt() { - return month; - } - - public int getTotalNumDays(int year) { - Calendar cal = Calendar.getInstance(); - cal.set(year, month, 1); - return cal.getActualMaximum(Calendar.DAY_OF_MONTH); - } - - @Override - public int getNumFiles() { - int numFiles = 0; - for (DayEpoch de : days) { - numFiles += de.getNumFiles(); - } - return numFiles; - } - - public DayEpoch getDay(int dayNum) { - DayEpoch de = null; - for (DayEpoch d : days) { - if (d.dayNum == dayNum) { - de = d; - break; - } - } - return de; - } - - public void add(long fileId, int day) { - DayEpoch dayEpoch = null; - for (DayEpoch de : days) { - if (de.getDayInt() == day) { - dayEpoch = de; - break; - } - } - - if (dayEpoch == null) { - dayEpoch = new DayEpoch(day); - days.add(dayEpoch); - } - - dayEpoch.add(fileId); - } - - /** - * Returns the month's name in String format, e.g., September, July, - */ - String getMonthName() { - return new DateFormatSymbols().getMonths()[month]; - } - - /** - * @return the list of days in this month - */ - List<DayEpoch> getDays() { - return this.days; - } - } - - private class DayEpoch extends Epoch { - - private final List<Long> fileIds = new ArrayList<>(); - int dayNum = 0; //Day of the month this Epoch represents, 1 indexed: 28=28. - - DayEpoch(int dayOfMonth) { - this.dayNum = dayOfMonth; - } - - public int getDayInt() { - return dayNum; - } - - @Override - public int getNumFiles() { - return fileIds.size(); - } - - public void add(long fileId) { - fileIds.add(fileId); - } - - List<Long> getEvents() { - return this.fileIds; - } - } - - // The node factories used to make lists of files to send to the result viewer - // using the lazy loading (rather than background) loading option to facilitate - // loading a huge number of nodes for the given day - private class FileNodeChildFactory extends Children.Keys<Long> { - - private List<Long> fileIds; - - FileNodeChildFactory(List<Long> fileIds) { - super(true); - this.fileIds = fileIds; - } - - @Override - protected void addNotify() { - super.addNotify(); - setKeys(fileIds); - } - - @Override - protected void removeNotify() { - super.removeNotify(); - setKeys(new ArrayList<Long>()); - } - - @Override - protected Node[] createNodes(Long t) { - return new Node[]{createNodeForKey(t)}; - } - - // @Override - // protected boolean createKeys(List<Long> list) { - // list.addAll(fileIds); - // return true; - // } - //@Override - protected Node createNodeForKey(Long fileId) { - AbstractFile af = null; - try { - af = skCase.getAbstractFileById(fileId); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting file by id and creating a node in Timeline: " + fileId, ex); //NON-NLS - //no node will be shown for this object - return null; - } - - Node wrapped; - if (af.isDir()) { - wrapped = new DirectoryNode(af, false); - } else { - wrapped = new FileNode(af, false); - } - return new FilterNodeLeaf(wrapped); - } - } - - private class FileRootNode extends DisplayableItemNode { - - FileRootNode(String NAME, List<Long> fileIds) { - //super(Children.create(new FileNodeChildFactory(fileIds), true)); - super(new FileNodeChildFactory(fileIds), Lookups.singleton(fileIds)); - super.setName(NAME); - super.setDisplayName(NAME); - } - - @Override - public boolean isLeafTypeNode() { - return false; - } - - @Override - public <T> T accept(DisplayableItemNodeVisitor<T> v) { - return null; - } - } - - /** - * Parse the output of mactime to break the results in to day-sized chunks (in GMT) - * @param f handle to mactime csv output - * @return - */ - private List<YearEpoch> parseMacTime(java.io.File f) { - List<YearEpoch> years = new ArrayList<>(); - Scanner scan; - try { - scan = new Scanner(new FileInputStream(f)); - } catch (FileNotFoundException ex) { - logger.log(Level.SEVERE, "Error: could not find mactime file.", ex); //NON-NLS - return years; - } - scan.useDelimiter(","); - scan.nextLine(); // skip the header line - - int prevYear = -1; - YearEpoch ye = null; - - while (scan.hasNextLine()) { - String[] s = scan.nextLine().split(","); //1999-02-08T11:08:08Z, 78706, m..b, rrwxrwxrwx, 0, 0, 8355, /img... - - // break the date into year,month,day,hour,minute, and second: Note that the ISO times are in GMT - String delims = "[T:Z\\-]+"; //split by the delimiters NON-NLS - String[] date = s[0].split(delims); //{1999,02,08,11,08,08,...} - - int year = Integer.valueOf(date[0]); - int month = Integer.valueOf(date[1]) - 1; //Months are zero indexed: 1 = February, 6 = July, 11 = December - int day = Integer.valueOf(date[2]); //Days are 1 indexed - int hour=Integer.valueOf(date[3]); - int minute=Integer.valueOf(date[4]); - int second=Integer.valueOf(date[5]); - - Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //set calendar to GMT due to ISO format - calendar.set(year, month, day, hour, minute, second); - day=calendar.get(Calendar.DAY_OF_MONTH); // this is needed or else timezone change wont work. probably incorrect optimization by compiler - - //conversion to GMT - - if (!ContentUtils.shouldDisplayTimesInLocalTime()) { - calendar.setTimeZone(TimeZone.getTimeZone("GMT")); - } - else{ - calendar.setTimeZone(TimeZone.getDefault());// local timezone OF the user. should be what the user SETS at startup - } - - day=calendar.get(Calendar.DAY_OF_MONTH);//get the day which may be affected by timezone change - long ObjId = Long.valueOf(s[4]); - - // when the year changes, create and add a new YearEpoch object to the list - if (year != prevYear) { - ye = new YearEpoch(year); - years.add(ye); - prevYear = year; - } - - // save the object id along with the day - if (ye != null) { - ye.add(ObjId, month, day); - } - } - - scan.close(); - - return years; - } - - /** - * Crate a body file and return its path or null if error - * - * @return absolute path string or null if error - */ - private String makeBodyFile() { - // Setup timestamp - DateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss"); - Date date = new Date(); - String datenotime = dateFormat.format(date); - - final Case currentCase = Case.getCurrentCase(); - - // Get report path - String bodyFilePath = moduleDir.getAbsolutePath() - + java.io.File.separator + currentCase.getName() + "-" + datenotime + ".txt"; //NON-NLS - // Run query to get all files - final String filesAndDirs = "name != '.' " //NON-NLS - + "AND name != '..'"; //NON-NLS - List<Long> fileIds = null; - try { - fileIds = skCase.findAllFileIdsWhere(filesAndDirs); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error querying image files to make a body file: " + bodyFilePath, ex); //NON-NLS - return null; - } - - // Loop files and write info to report - FileWriter fileWriter = null; - try { - fileWriter = new FileWriter(bodyFilePath, true); - } catch (IOException ex) { - logger.log(Level.SEVERE, "Error creating output stream to write body file to: " + bodyFilePath, ex); //NON-NLS - return null; - } - - BufferedWriter out = null; - try { - out = new BufferedWriter(fileWriter); - for (long fileId : fileIds) { - AbstractFile file = skCase.getAbstractFileById(fileId); - // try { - // MD5|name|inode|mode_as_string|ObjId|GID|size|atime|mtime|ctime|crtime - if (file.getMd5Hash() != null) { - out.write(file.getMd5Hash()); - } - out.write("|"); - String path = null; - try { - path = file.getUniquePath(); - } catch (TskCoreException e) { - logger.log(Level.SEVERE, "Failed to get the unique path of: " + file + " and writing body file.", e); //NON-NLS - return null; - } - - out.write(path); - - out.write("|"); - out.write(Long.toString(file.getMetaAddr())); - out.write("|"); - String modeString = file.getModesAsString(); - if (modeString != null) { - out.write(modeString); - } - out.write("|"); - out.write(Long.toString(file.getId())); - out.write("|"); - out.write(Long.toString(file.getGid())); - out.write("|"); - out.write(Long.toString(file.getSize())); - out.write("|"); - out.write(Long.toString(file.getAtime())); - out.write("|"); - out.write(Long.toString(file.getMtime())); - out.write("|"); - out.write(Long.toString(file.getCtime())); - out.write("|"); - out.write(Long.toString(file.getCrtime())); - out.write("\n"); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error querying file by id", ex); //NON-NLS - return null; - - } catch (IOException ex) { - logger.log(Level.WARNING, "Error while trying to write data to the body file.", ex); //NON-NLS - return null; - } finally { - if (out != null) { - try { - out.flush(); - out.close(); - } catch (IOException ex1) { - logger.log(Level.WARNING, "Could not flush and/or close body file.", ex1); //NON-NLS - } - } - } - - - return bodyFilePath; - } - - /** - * Run mactime on the given body file. Generates CSV file with ISO dates (in GMT) - * @param pathToBodyFile - * @return Path to output file. - */ - private String makeMacTime(String pathToBodyFile) { - String cmdpath = ""; - String macpath = ""; - String[] mactimeArgs; - final String machome = macRoot.getAbsolutePath(); - pathToBodyFile = PlatformUtil.getOSFilePath(pathToBodyFile); - if (PlatformUtil.isWindowsOS()) { - macpath = machome + java.io.File.separator + "mactime.exe"; //NON-NLS - cmdpath = PlatformUtil.getOSFilePath(macpath); - mactimeArgs = new String[]{"-b", pathToBodyFile, "-d", "-y"}; //NON-NLS - } else { - cmdpath = "perl"; //NON-NLS - macpath = machome + java.io.File.separator + "mactime.pl"; //NON-NLS - mactimeArgs = new String[]{macpath, "-b", pathToBodyFile, "-d", "-y"}; //NON-NLS - } - - String macfile = moduleDir.getAbsolutePath() + java.io.File.separator + mactimeFileName; - - - String output = ""; - ExecUtil execUtil = new ExecUtil(); - Writer writer = null; - try { - //JavaSystemCaller.Exec.execute("\"" + command + "\""); - writer = new FileWriter(macfile); - execUtil.execute(writer, cmdpath, mactimeArgs); - } catch (InterruptedException ie) { - logger.log(Level.WARNING, "Mactime process was interrupted by user", ie); //NON-NLS - return null; - } catch (IOException ioe) { - logger.log(Level.SEVERE, "Could not create mactime file, encountered error ", ioe); //NON-NLS - return null; - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException ex) { - logger.log(Level.SEVERE, "Could not clsoe writer after creating mactime file, encountered error ", ex); //NON-NLS - } - } - execUtil.stop(); - } - - return macfile; - } - - @Override - public boolean isEnabled() { - return Case.isCaseOpen() && this.fxInited; - } - - @Override - public void performAction() { - initTimeline(); - } - - private void initTimeline() { - if (!Case.existsCurrentCase()) { - return; - } - - final Case currentCase = Case.getCurrentCase(); - skCase = currentCase.getSleuthkitCase(); - - try { - if (currentCase.hasData() == false) { - logger.log(Level.INFO, "Error creating timeline, there are no data sources. "); //NON-NLS - } else { - - if (IngestManager.getInstance().isIngestRunning()) { - int answer = JOptionPane.showConfirmDialog(new JFrame(), - NbBundle.getMessage(this.getClass(), - "Timeline.initTimeline.confDlg.genBeforeIngest.msg"), - NbBundle.getMessage(this.getClass(), - "Timeline.initTimeline.confDlg.genBeforeIngest.deails"), - JOptionPane.YES_NO_OPTION); - if (answer != JOptionPane.YES_OPTION) { - return; - } - } - - logger.log(Level.INFO, "Beginning generation of timeline"); //NON-NLS - - // if the timeline window is already open, bring to front and do nothing - if (mainFrame != null && mainFrame.isVisible()) { - mainFrame.toFront(); - return; - } - - // listen for case changes (specifically images being added). - if (Case.isCaseOpen() && !listeningToAddImage) { - Case.addPropertyChangeListener(this); - listeningToAddImage = true; - } - - // create the modal progressDialog - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressDialog = new TimelineProgressDialog(WindowManager.getDefault().getMainWindow(), true); - progressDialog.setVisible(true); - } - }); - - // initialize mactimeFileName - mactimeFileName = currentCase.getName() + "-MACTIME.txt"; //NON-NLS - - // see if barData has been added to the database since the last - // time timeline ran - long objId = skCase.getLastObjectId(); - if (objId != lastObjectId && lastObjectId != -1) { - clearMactimeData(); - } - lastObjectId = objId; - - customize(); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error when generating timeline, ", ex); //NON-NLS - } catch (Exception ex) { - logger.log(Level.SEVERE, "Unexpected error when generating timeline, ", ex); //NON-NLS - } - } - - @Override - public String getName() { - return NbBundle.getMessage(this.getClass(), "Timeline.getName"); - } - - @Override - public HelpCtx getHelpCtx() { - return HelpCtx.DEFAULT_HELP; - } - - @Override - public boolean asynchronous() { - return false; - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java b/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java deleted file mode 100644 index 76256770d498a12a59e6220744337e3b3e50ef78..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.sleuthkit.autopsy.timeline; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Cursor; -import java.awt.Dimension; -import java.awt.Image; - -/** - * - * Ready timeline frame with layout to hold dynamic components - */ - class TimelineFrame extends javax.swing.JFrame { - - /** - * Creates new form TimelineFrame - */ - public TimelineFrame() { - initComponents(); - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents - private void initComponents() { - - splitYPane = new javax.swing.JSplitPane(); - topPane = new javax.swing.JPanel(); - splitXPane = new javax.swing.JSplitPane(); - leftPane = new javax.swing.JPanel(); - rightPane = new javax.swing.JPanel(); - - setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); - setTitle(org.openide.util.NbBundle.getMessage(TimelineFrame.class, "TimelineFrame.title")); // NOI18N - setPreferredSize(new java.awt.Dimension(1200, 700)); - - splitYPane.setDividerLocation(420); - splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); - splitYPane.setResizeWeight(0.5); - splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400)); - - topPane.setPreferredSize(new java.awt.Dimension(1200, 400)); - topPane.setLayout(new java.awt.BorderLayout()); - splitYPane.setLeftComponent(topPane); - - splitXPane.setDividerLocation(600); - splitXPane.setResizeWeight(0.5); - splitXPane.setPreferredSize(new java.awt.Dimension(1200, 300)); - splitXPane.setRequestFocusEnabled(false); - - leftPane.setPreferredSize(new java.awt.Dimension(700, 300)); - leftPane.setLayout(new java.awt.BorderLayout()); - splitXPane.setLeftComponent(leftPane); - - rightPane.setPreferredSize(new java.awt.Dimension(500, 300)); - rightPane.setLayout(new java.awt.BorderLayout()); - splitXPane.setRightComponent(rightPane); - - splitYPane.setRightComponent(splitXPane); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(splitYPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(splitYPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - - pack(); - }// </editor-fold>//GEN-END:initComponents - - void setTopPanel(Component component) { - this.topPane.add(component, BorderLayout.CENTER); - } - - void setBottomLeftPanel(Component component) { - this.leftPane.add(component, BorderLayout.CENTER); - } - - - void setBottomRightPanel(Component component) { - this.rightPane.add(component, BorderLayout.CENTER); - } - - void setFrameName(String name) { - this.setTitle(name); - } - - void setFrameIcon(Image iconImage) { - this.setIconImage(iconImage); - } - - void setFrameSize(Dimension size) { - this.setSize(size); - } - - void setTopComponentCursor(Cursor cursor) { - this.topPane.setCursor(cursor); - } - - - /** - * @param args the command line arguments - */ - public static void main(String args[]) { - /* Set the Nimbus look and feel */ - //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) "> - /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel. - * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html - */ - try { - for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { //NON-NLS - javax.swing.UIManager.setLookAndFeel(info.getClassName()); - break; - } - } - } catch (ClassNotFoundException ex) { - java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); - } catch (InstantiationException ex) { - java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); - } catch (IllegalAccessException ex) { - java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); - } catch (javax.swing.UnsupportedLookAndFeelException ex) { - java.util.logging.Logger.getLogger(TimelineFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); - } - //</editor-fold> - - /* Create and display the form */ - java.awt.EventQueue.invokeLater(new Runnable() { - public void run() { - new TimelineFrame().setVisible(true); - } - }); - } - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JPanel leftPane; - private javax.swing.JPanel rightPane; - private javax.swing.JSplitPane splitXPane; - private javax.swing.JSplitPane splitYPane; - private javax.swing.JPanel topPane; - // End of variables declaration//GEN-END:variables -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java b/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java deleted file mode 100644 index 3b4844d415c54872c5712b260bf1491607847df8..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.timeline; - -import java.awt.EventQueue; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import javax.swing.AbstractAction; -import javax.swing.ActionMap; -import javax.swing.InputMap; -import javax.swing.JComponent; -import javax.swing.KeyStroke; - -import org.openide.util.NbBundle; -import org.openide.windows.WindowManager; - -/** - * Dialog with progress bar that pops up when timeline is being generated - */ - class TimelineProgressDialog extends javax.swing.JDialog { - - /** - * A return status code - returned if Cancel button has been pressed - */ - public static final int RET_CANCEL = 0; - /** - * A return status code - returned if OK button has been pressed - */ - public static final int RET_OK = 1; - - /** - * Creates new form TimelineProgressDialog - */ - public TimelineProgressDialog(java.awt.Frame parent, boolean modal) { - super(parent, modal); - initComponents(); - - setLocationRelativeTo(null); - //set icon the same as main app - setIconImage(WindowManager.getDefault().getMainWindow().getIconImage()); - - //progressBar.setIndeterminate(true); - - setName(NbBundle.getMessage(this.getClass(), "TimelineProgressDialog.setName.text")); - - // Close the dialog when Esc is pressed - String cancelName = "cancel"; //NON-NLS - InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); - inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName); - ActionMap actionMap = getRootPane().getActionMap(); - actionMap.put(cancelName, new AbstractAction() { - public void actionPerformed(ActionEvent e) { - doClose(RET_CANCEL); - } - }); - } - - /** - * @return the return status of this dialog - one of RET_OK or RET_CANCEL - */ - public int getReturnStatus() { - return returnStatus; - } - - void updateProgressBar(final int progress) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - progressBar.setValue(progress); - } - }); - - } - - void updateProgressBar(final String message) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - progressBar.setString(message); - } - }); - - } - - void setProgressTotal(final int total) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - //progressBar.setIndeterminate(false); - progressBar.setMaximum(total); - //progressBar.setValue(0); - progressBar.setStringPainted(true); - progressBar.setVisible(true); - } - }); - - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents - private void initComponents() { - - jLabel1 = new javax.swing.JLabel(); - progressBar = new javax.swing.JProgressBar(); - - addWindowListener(new java.awt.event.WindowAdapter() { - public void windowClosing(java.awt.event.WindowEvent evt) { - closeDialog(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(TimelineProgressDialog.class, "TimelineProgressDialog.jLabel1.text")); // NOI18N - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(progressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 504, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addComponent(jLabel1) - .addGap(0, 0, Short.MAX_VALUE))) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(jLabel1) - .addGap(7, 7, 7) - .addComponent(progressBar, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(16, Short.MAX_VALUE)) - ); - - pack(); - }// </editor-fold>//GEN-END:initComponents - - /** - * Closes the dialog - */ - private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog - doClose(RET_CANCEL); - }//GEN-LAST:event_closeDialog - - void doClose(final int retStatus) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - returnStatus = retStatus; - setVisible(false); - dispose(); - } - }); - - } - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JLabel jLabel1; - private javax.swing.JProgressBar progressBar; - // End of variables declaration//GEN-END:variables - private int returnStatus = RET_CANCEL; -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/layer.xml b/Core/src/org/sleuthkit/autopsy/timeline/layer.xml deleted file mode 100644 index 1a06f5740a7e08b991ea4abc0b91a64af5ec7319..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/layer.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd"> -<filesystem> - - - <!-- ====================================================== - Windows2 - ====================================================== --> - <folder name="Actions"> - <folder name="Tools"> - <file name="org-sleuthkit-autopsy-timeline-Simile2.instance_hidden"/> - <file name="org-sleuthkit-autopsy-timeline-Timeline.instance"/> - </folder> - <folder name="Window"> - <file name="org-sleuthkit-autopsy-timeline-Timeline2TopComponent.instance_hidden"/> - <file name="org-sleuthkit-autopsy-timeline-TimelineTopComponent.instance_hidden"/> - </folder> - </folder> - <folder name="Windows2"> - <folder name="Components"> - - </folder> - <folder name="Modes"> - <file name="timeline.wsmode" url="timelineWsmode.xml"/> - </folder> - </folder> - -</filesystem> diff --git a/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml b/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml deleted file mode 100644 index cde9642f4a3f62393862eda5fc569fa88eb0ae0f..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/timelineWsmode.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE mode PUBLIC - "-//NetBeans//DTD Mode Properties 2.0//EN" - "http://www.netbeans.org/dtds/mode-properties2_0.dtd"> - -<mode version="2.3"> - <name unique="timeline" /> - <!--<kind type="view" /> --> <!-- modal --> - <kind type="editor" /> <!-- non-modal --> - <state type="separated" /> - <constraints> - <path orientation="horizontal" number="100" weight="0.5"/> - </constraints> - <bounds x="7" y="909" width="400" height="250" /> - <frame state="0"/> - <empty-behavior permanent="true"/> -</mode> \ No newline at end of file diff --git a/Timeline/build.xml b/Timeline/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..b99c79655da4a275300f09e759b499e420aa6cd4 --- /dev/null +++ b/Timeline/build.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- You may freely edit this file. See harness/README in the NetBeans platform --> +<!-- for some information on what you could do (e.g. targets to override). --> +<!-- If you delete this file and reopen the project it will be recreated. --> +<project name="org.sleuthkit.autopsy.timeline" default="netbeans" basedir="."> + <description>Builds, tests, and runs the project org.sleuthkit.autopsy.timeline.</description> + <import file="nbproject/build-impl.xml"/> + +</project> diff --git a/Timeline/manifest.mf b/Timeline/manifest.mf new file mode 100644 index 0000000000000000000000000000000000000000..89925c271ec5176a7bda9cd4a0c0438a7eccc317 --- /dev/null +++ b/Timeline/manifest.mf @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +OpenIDE-Module: org.sleuthkit.autopsy.timeline +OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/timeline/Bundle.properties +OpenIDE-Module-Requires: org.openide.windows.WindowManager +OpenIDE-Module-Specification-Version: 1.0 + diff --git a/Timeline/nbproject/build-impl.xml b/Timeline/nbproject/build-impl.xml new file mode 100644 index 0000000000000000000000000000000000000000..4babba24f71fcaac06c8eb32def4c87a1ebe5956 --- /dev/null +++ b/Timeline/nbproject/build-impl.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +*** GENERATED FROM project.xml - DO NOT EDIT *** +*** EDIT ../build.xml INSTEAD *** +--> +<project name="org.sleuthkit.autopsy.timeline-impl" basedir=".."> + <fail message="Please build using Ant 1.7.1 or higher."> + <condition> + <not> + <antversion atleast="1.7.1"/> + </not> + </condition> + </fail> + <property file="nbproject/private/suite-private.properties"/> + <property file="nbproject/suite.properties"/> + <fail unless="suite.dir">You must set 'suite.dir' to point to your containing module suite</fail> + <property file="${suite.dir}/nbproject/private/platform-private.properties"/> + <property file="${suite.dir}/nbproject/platform.properties"/> + <macrodef name="property" uri="http://www.netbeans.org/ns/nb-module-project/2"> + <attribute name="name"/> + <attribute name="value"/> + <sequential> + <property name="@{name}" value="${@{value}}"/> + </sequential> + </macrodef> + <macrodef name="evalprops" uri="http://www.netbeans.org/ns/nb-module-project/2"> + <attribute name="property"/> + <attribute name="value"/> + <sequential> + <property name="@{property}" value="@{value}"/> + </sequential> + </macrodef> + <property file="${user.properties.file}"/> + <nbmproject2:property name="harness.dir" value="nbplatform.${nbplatform.active}.harness.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <nbmproject2:property name="nbplatform.active.dir" value="nbplatform.${nbplatform.active}.netbeans.dest.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <nbmproject2:evalprops property="cluster.path.evaluated" value="${cluster.path}" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <fail message="Path to 'platform' cluster missing in $${cluster.path} property or using corrupt Netbeans Platform (missing harness)."> + <condition> + <not> + <contains string="${cluster.path.evaluated}" substring="platform"/> + </not> + </condition> + </fail> + <import file="${harness.dir}/build.xml"/> +</project> diff --git a/Timeline/nbproject/platform.properties b/Timeline/nbproject/platform.properties new file mode 100644 index 0000000000000000000000000000000000000000..867137ed43359526671e623e503fb0e1eb2a8b73 --- /dev/null +++ b/Timeline/nbproject/platform.properties @@ -0,0 +1,7 @@ +cluster.path=\ + ${nbplatform.active.dir}/autopsy:\ + ${nbplatform.active.dir}/harness:\ + ${nbplatform.active.dir}/java:\ + ${nbplatform.active.dir}/platform +nbjdk.active=default +nbplatform.active=Autopsy_Dev diff --git a/Timeline/nbproject/project.properties b/Timeline/nbproject/project.properties new file mode 100644 index 0000000000000000000000000000000000000000..949be310a18406a37f930b6ab662468b441f8cba --- /dev/null +++ b/Timeline/nbproject/project.properties @@ -0,0 +1,11 @@ +file.reference.controlsfx-8.0.6_20.jar=release/modules/ext/controlsfx-8.0.6_20.jar +file.reference.jfxtras-common-8.0-r2-20140616.080113-115.jar=release\\modules\\ext\\jfxtras-common-8.0-r2-20140616.080113-115.jar +file.reference.jfxtras-controls-8.0-r2-20140616.080125-115.jar=release\\modules\\ext\\jfxtras-controls-8.0-r2-20140616.080125-115.jar +file.reference.jfxtras-fxml-8.0-r2-20140616.080139-115.jar=release\\modules\\ext\\jfxtras-fxml-8.0-r2-20140616.080139-115.jar +file.reference.joda-time-2.3.jar=release/modules/ext/joda-time-2.3.jar +file.reference.sqlite-jdbc-3.7.8-SNAPSHOT.jar=release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar +javac.source=1.8 +javac.compilerargs=-Xlint -Xlint:-serial +license.file=LICENSE-2.0.txt +project.license=advanced_timeline + diff --git a/Timeline/nbproject/project.xml b/Timeline/nbproject/project.xml new file mode 100644 index 0000000000000000000000000000000000000000..e7df2f5673d64306022504c0c4f94920e215d242 --- /dev/null +++ b/Timeline/nbproject/project.xml @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://www.netbeans.org/ns/project/1"> + <type>org.netbeans.modules.apisupport.project</type> + <configuration> + <data xmlns="http://www.netbeans.org/ns/nb-module-project/3"> + <code-name-base>org.sleuthkit.autopsy.timeline</code-name-base> + <suite-component/> + <module-dependencies> + <dependency> + <code-name-base>org.netbeans.api.progress</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>1</release-version> + <specification-version>1.32.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.netbeans.modules.settings</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>1</release-version> + <specification-version>1.38.2</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.awt</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.55.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.explorer</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>6.50.3</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.filesystems</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>8.5.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.modules</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.35.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.nodes</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.33.2</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.util</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>8.29.3</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.util.lookup</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>8.19.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.windows</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>6.60.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.sleuthkit.autopsy.core</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>10</release-version> + <specification-version>10.0.11</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.sleuthkit.autopsy.corelibs</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>3</release-version> + <specification-version>1.1</specification-version> + </run-dependency> + </dependency> + </module-dependencies> + <public-packages/> + <class-path-extension> + <runtime-relative-path>ext/joda-time-2.3.jar</runtime-relative-path> + <binary-origin>release/modules/ext/joda-time-2.3.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/controlsfx-8.0.6_20.jar</runtime-relative-path> + <binary-origin>release/modules/ext/controlsfx-8.0.6_20.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/jfxtras-common-8.0-r2-20140616.080113-115.jar</runtime-relative-path> + <binary-origin>release\modules\ext\jfxtras-common-8.0-r2-20140616.080113-115.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar</runtime-relative-path> + <binary-origin>release\modules\ext\jfxtras-controls-8.0-r2-20140616.080125-115.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</runtime-relative-path> + <binary-origin>release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar</runtime-relative-path> + <binary-origin>release\modules\ext\jfxtras-fxml-8.0-r2-20140616.080139-115.jar</binary-origin> + </class-path-extension> + </data> + </configuration> +</project> diff --git a/Timeline/nbproject/suite.properties b/Timeline/nbproject/suite.properties new file mode 100644 index 0000000000000000000000000000000000000000..29d7cc9bd6fdd81453543cdf1bcf1dab301e3a92 --- /dev/null +++ b/Timeline/nbproject/suite.properties @@ -0,0 +1 @@ +suite.dir=${basedir}/.. diff --git a/Timeline/release/modules/ext/controlsfx-8.0.6.jar b/Timeline/release/modules/ext/controlsfx-8.0.6.jar new file mode 100644 index 0000000000000000000000000000000000000000..2f2d1badd0facb374d5c2519ce3525ff8693c222 Binary files /dev/null and b/Timeline/release/modules/ext/controlsfx-8.0.6.jar differ diff --git a/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar b/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar new file mode 100644 index 0000000000000000000000000000000000000000..39a73c0b9acb47fb4ab23fc5922313885d0e8e91 Binary files /dev/null and b/Timeline/release/modules/ext/controlsfx-8.0.6_20.jar differ diff --git a/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar b/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar new file mode 100644 index 0000000000000000000000000000000000000000..05825d15b8b7b69fd07c2b475127f86e67861b05 Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-all-8.0-r2-20140616.080024-115.jar differ diff --git a/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar b/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar new file mode 100644 index 0000000000000000000000000000000000000000..5330e96922ef275d4e2d7288c546671b0a721f3b Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-common-8.0-r2-20140616.080113-115.jar differ diff --git a/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar b/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar new file mode 100644 index 0000000000000000000000000000000000000000..50e52486fcd051e73a92a5881b586326cb76f668 Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-controls-8.0-r2-20140616.080125-115.jar differ diff --git a/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar b/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar new file mode 100644 index 0000000000000000000000000000000000000000..8925347141da65a7032584adde0e9ba44289b175 Binary files /dev/null and b/Timeline/release/modules/ext/jfxtras-fxml-8.0-r2-20140616.080139-115.jar differ diff --git a/Timeline/release/modules/ext/joda-time-2.3.jar b/Timeline/release/modules/ext/joda-time-2.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..9dce4f92a61489984d74ab393daa111a24f47535 Binary files /dev/null and b/Timeline/release/modules/ext/joda-time-2.3.jar differ diff --git a/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt b/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7 --- /dev/null +++ b/Timeline/release/modules/ext/joda-time-2.3/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt b/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt new file mode 100644 index 0000000000000000000000000000000000000000..dffbcf31cacf6e01cd738cfd6a6ffea1905bace2 --- /dev/null +++ b/Timeline/release/modules/ext/joda-time-2.3/NOTICE.txt @@ -0,0 +1,5 @@ +============================================================================= += NOTICE file corresponding to section 4d of the Apache License Version 2.0 = +============================================================================= +This product includes software developed by +Joda.org (http://www.joda.org/). diff --git a/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt b/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc2249f11aa6161fb601082e8f5556bc2f28acdf --- /dev/null +++ b/Timeline/release/modules/ext/joda-time-2.3/RELEASE-NOTES.txt @@ -0,0 +1,130 @@ +Joda-Time version 2.3 +--------------------- + +Joda-Time is a date and time handling library that seeks to replace the JDK +Date and Calendar classes. + +This release contains enhancements, bug fixes and a time zone update. +The release runs on JDK 5 or later. + +Joda-Time is licensed under the business-friendly Apache License Version 2. +This is the same license as all of Apache, plus other open source projects such as Spring. +The intent is to make the code available to the Java community with the minimum +of restrictions. If the license causes you problems please contact the mailing list. + +** Please also check out our related projects ** +** http://www.joda.org/joda-time/related.html ** + + +Enhancements since 2.2 +---------------------- +- Interval/MutableInterval .isEqual() [#20] + Add method to compare intervals ignoring the chronology + https://github.com/JodaOrg/joda-time/issues/20 + +- Chronology classes now define equals methods [#36] + Previously, the Chronology classes relied on caching in factory methods + to guarantee instances were singletons + Now, there are dedicated, normal, equals methods + This will aid weird cases where deserialization or similar avoids the caches + It will make no difference to most users + +- Maximum size for pattern cache [#49] + Sets a maximum size for the cache to avoid memory issues + +- Add LocalDateTime.toDate(TimeZone) [#48] + Provides an alternate way to create a java.util.Date that avoids some synchronization + +- Home page moved + http://www.joda.org/joda-time + + +Compatibility with 2.2 +---------------------- +Build system - Yes + +Binary compatible - Yes + +Source compatible - Yes + +Serialization compatible - Yes + +Data compatible - Yes, except + - DateTimeZone data updated to version 2013d + +Semantic compatible - Yes, except + - DateTimeZone is now limited to offsets from -23:59:59.999 to +23:59:59.999 + + - BasicChronology now defines an equals method + This which would affect you if you subclassed it (unlikely) + + - GJChronology now has a minimum cutover instant of 0001-01-01 (Gregorian) + Its unlikely you have it set earlier than this + If you did your code was broken anyway + + +Deprecations since 2.2 +---------------------- +- DateMidnight [#41] + This class is flawed in concept + The time of midnight occasionally does not occur in some time-zones + This is a result of a daylight savings time from 00:00 to 01:00 + DateMidnight is essentially a DateTime with a time locked to midnight + Such a concept is more generally a poor one to use, given LocalDate + Replace DateMidnight with LocalDate + Or replace it with DateTime, perhaps using the withTimeAtStartOfDay() method + + +Bug fixes since 2.2 +------------------- +- ZoneInfoCompiler and DateTimeZoneBuilder multi-threading [#18] + A thread local variable was previously only initialised in one thread causing NPE + https://github.com/JodaOrg/joda-time/issues/18 + +- Short time-zone name parsing failed to match the longest name + This affected two short names where one is a short form of the second such as "UT" and "UTC" + +- Days.daysBetween fails for MonthDay [#22] + Incorrect calculation around leap years + +- DateTimeZone failed to validate offsets [#43] + Previously, there was little validation, resulting in the ability to create large offsets + Those offsets could fail in other parts of the library + Now, it is limited to -23:59:59.999 to +23:59:59.999 + +- DateTimeZone.forOffsetHoursMinutes failed to allow offsets from -00:01 to -00:59 [#42] + The forOffsetHoursMinutes() method could not create an offset from -00:01 to -00:59 + This was due to an inappropriate design + A backwards compatible change to the input handling has been made + forOffsetHoursMinutes(0, -15) now creates -00:15 + +- DateTimeFormatter.parseInto [#21] + Fix parseInto() where it obtains the default year for parsing from the ReadWritableInstant + Previously, the wrong year could be obtained at the start or end of the year in non UTC zones + Now obtains the year from the ReadWritableInstant using the chronology of the ReadWritableInstant + +- Better thread-safety in ISODateTimeFormat [#45] + +- Fix GJChronology.plus/minus across cutover and year zero [#28] + When subtracting a number of years from a date in the GJChronology there are two considerations + The cutover date might be crossed, and year zero might be crossed (there is no year zero in GJ) + Previously, each were handled separately, but not together. Now it is fixed + As part of this change, the minimum cutover instant was set to 0001-01-01 (Gregorian) + + +Scala +-------- +Joda-Time uses annotations from Joda-Convert. +In the Java programming language, this dependency is optional, however in Scala it is not. +Scala users must manually add the Joda-Convert v1.2 dependency. + + +Feedback +-------- +Feedback is best received using GitHub issues and Pull Requests. +https://github.com/JodaOrg/joda-time/ + +Feedback is also welcomed via the joda-interest mailing list. + +The Joda team +http://joda-time.sourceforge.net diff --git a/Timeline/release/modules/ext/joda-time-2.3/pom.xml b/Timeline/release/modules/ext/joda-time-2.3/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb2e4c095bd471679eb31b919e0cc95a6b6d999d --- /dev/null +++ b/Timeline/release/modules/ext/joda-time-2.3/pom.xml @@ -0,0 +1,699 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + <groupId>joda-time</groupId> + <artifactId>joda-time</artifactId> + <packaging>jar</packaging> + <name>Joda-Time</name> + <version>2.3</version> + <description>Date and time library to replace JDK date handling</description> + <url>http://www.joda.org/joda-time/</url> + + <!-- ==================================================================== --> + <issueManagement> + <system>GitHub</system> + <url>https://github.com/JodaOrg/joda-time/issues</url> + </issueManagement> + <inceptionYear>2002</inceptionYear> + <mailingLists> + <mailingList> + <name>Joda Interest list</name> + <subscribe>https://lists.sourceforge.net/lists/listinfo/joda-interest</subscribe> + <unsubscribe>https://lists.sourceforge.net/lists/listinfo/joda-interest</unsubscribe> + <archive>http://sourceforge.net/mailarchive/forum.php?forum_name=joda-interest</archive> + </mailingList> + </mailingLists> + + <!-- ==================================================================== --> + <developers> + <developer> + <id>scolebourne</id> + <name>Stephen Colebourne</name> + <email></email> + <roles> + <role>Project Lead</role> + </roles> + <timezone>0</timezone> + <url>https://github.com/jodastephen</url> + </developer> + <developer> + <id>broneill</id> + <name>Brian S O'Neill</name> + <email></email> + <roles> + <role>Senior Developer</role> + </roles> + <url>https://github.com/broneill</url> + </developer> + </developers> + <contributors> + <contributor> + <name>Guy Allard</name> + </contributor> + <contributor> + <name>Oren Benjamin</name> + <url>https://github.com/oby1</url> + </contributor> + <contributor> + <name>Fredrik Borgh</name> + </contributor> + <contributor> + <name>Dave Brosius</name> + <url>https://github.com/mebigfatguy</url> + </contributor> + <contributor> + <name>Luc Claes</name> + <url>https://github.com/lucclaes</url> + </contributor> + <contributor> + <name>Dan Cojocar</name> + <url>https://github.com/dancojocar</url> + </contributor> + <contributor> + <name>Christopher Elkins</name> + <url>https://github.com/celkins</url> + </contributor> + <contributor> + <name>Jeroen van Erp</name> + </contributor> + <contributor> + <name>Gwyn Evans</name> + </contributor> + <contributor> + <name>John Fletcher</name> + </contributor> + <contributor> + <name>Sean Geoghegan</name> + </contributor> + <contributor> + <name>mjunginger</name> + <url>https://github.com/mjunginger</url> + </contributor> + <contributor> + <name>Ashish Katyal</name> + </contributor> + <contributor> + <name>Martin Kneissl</name> + <url>https://github.com/mkneissl</url> + </contributor> + <contributor> + <name>Vidar Larsen</name> + <url>https://github.com/vlarsen</url> + </contributor> + <contributor> + <name>Kasper Laudrup</name> + </contributor> + <contributor> + <name>Jeff Lavallee</name> + <url>https://github.com/jlavallee</url> + </contributor> + <contributor> + <name>Antonio Leitao</name> + </contributor> + <contributor> + <name>Kostas Maistrelis</name> + </contributor> + <contributor> + <name>Al Major</name> + </contributor> + <contributor> + <name>Blair Martin</name> + </contributor> + <contributor> + <name>Julen Parra</name> + </contributor> + <contributor> + <name>Michael Plump</name> + </contributor> + <contributor> + <name>Ryan Propper</name> + </contributor> + <contributor> + <name>Mike Schrag</name> + </contributor> + <contributor> + <name>Hajime Senuma</name> + <url>https://github.com/hajimes</url> + </contributor> + <contributor> + <name>Kandarp Shah</name> + </contributor> + <contributor> + <name>Francois Staes</name> + </contributor> + <contributor> + <name>Ricardo Trindade</name> + </contributor> + <contributor> + <name>Bram Van Dam</name> + <url>https://github.com/codematters</url> + </contributor> + <contributor> + <name>Maxim Zhao</name> + </contributor> + </contributors> + + <!-- ==================================================================== --> + <licenses> + <license> + <name>Apache 2</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> + <distribution>repo</distribution> + </license> + </licenses> + <scm> + <connection>scm:git:https://github.com/JodaOrg/joda-time.git</connection> + <developerConnection>scm:git:git@github.com:JodaOrg/joda-time.git</developerConnection> + <url>https://github.com/JodaOrg/joda-time</url> + </scm> + <organization> + <name>Joda.org</name> + <url>http://www.joda.org</url> + </organization> + + <!-- ==================================================================== --> + <build> + <resources> + <resource> + <targetPath>META-INF</targetPath> + <directory>${project.basedir}</directory> + <includes> + <include>LICENSE.txt</include> + <include>NOTICE.txt</include> + </includes> + </resource> + <resource> + <directory>${project.basedir}/src/main/java</directory> + <includes> + <include>**/*.properties</include> + </includes> + </resource> + </resources> + <!-- define build --> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.2.1</version> + <executions> + <execution> + <phase>compile</phase> + <goals> + <goal>java</goal> + </goals> + </execution> + </executions> + <configuration> + <mainClass>org.joda.time.tz.ZoneInfoCompiler</mainClass> + <classpathScope>compile</classpathScope> + <verbose>true</verbose> + <systemProperties> + <systemProperty> + <key>org.joda.time.DateTimeZone.Provider</key> + <value>org.joda.time.tz.UTCProvider</value> + </systemProperty> + </systemProperties> + <arguments> + <argument>-src</argument> + <argument>${project.build.sourceDirectory}/org/joda/time/tz/src</argument> + <argument>-dst</argument> + <argument>${project.build.outputDirectory}/org/joda/time/tz/data</argument> + <argument>africa</argument> + <argument>antarctica</argument> + <argument>asia</argument> + <argument>australasia</argument> + <argument>europe</argument> + <argument>northamerica</argument> + <argument>southamerica</argument> + <argument>pacificnew</argument> + <argument>etcetera</argument> + <argument>backward</argument> + <argument>systemv</argument> + </arguments> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <includes> + <include>**/TestAllPackages.java</include> + </includes> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <configuration> + <archive> + <manifestFile>src/conf/MANIFEST.MF</manifestFile> + <manifest> + <addDefaultImplementationEntries>true</addDefaultImplementationEntries> + <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> + </manifest> + </archive> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <groups> + <group> + <title>User packages</title> + <packages>org.joda.time:org.joda.time.format:org.joda.time.chrono</packages> + </group> + <group> + <title>Implementation packages</title> + <packages>org.joda.time.base:org.joda.time.convert:org.joda.time.field:org.joda.time.tz</packages> + </group> + </groups> + </configuration> + <executions> + <execution> + <id>attach-javadocs</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <executions> + <execution> + <id>attach-sources</id> + <phase>package</phase> + <goals> + <goal>jar-no-fork</goal> + </goals> + </execution> + </executions> + <!-- work around maven bug where properties files added twice --> + <configuration> + <excludes> + <exclude>**/*.properties</exclude> + </excludes> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <attach>false</attach> + <descriptors> + <descriptor>src/main/assembly/dist.xml</descriptor> + </descriptors> + <tarLongFileMode>gnu</tarLongFileMode> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>deploy</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-site-plugin</artifactId> + <configuration> + <skipDeploy>true</skipDeploy> + </configuration> + </plugin> + <plugin> + <groupId>com.github.github</groupId> + <artifactId>site-maven-plugin</artifactId> + <version>0.8</version> + <executions> + <execution> + <id>github-site</id> + <goals> + <goal>site</goal> + </goals> + <phase>site-deploy</phase> + </execution> + </executions> + <configuration> + <message>Create website for ${project.artifactId} v${project.version}</message> + <path>${project.artifactId}</path> + <merge>true</merge> + <server>github</server> + <repositoryOwner>JodaOrg</repositoryOwner> + <repositoryName>jodaorg.github.io</repositoryName> + <branch>refs/heads/master</branch> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>clirr-maven-plugin</artifactId> + <version>2.3</version> + <configuration> + <comparisonVersion>2.2</comparisonVersion> + <minSeverity>info</minSeverity> + <logResults>true</logResults> + </configuration> + </plugin> + </plugins> + <!-- Manage plugin versions --> + <pluginManagement> + <plugins> + <!-- Maven build and reporting plugins (alphabetical) --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>${maven-assembly-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>${maven-checkstyle-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-changes-plugin</artifactId> + <version>${maven-changes-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-clean-plugin</artifactId> + <version>${maven-clean-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>${maven-compiler-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>${maven-deploy-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>${maven-dependency-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>${maven-gpg-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + <version>${maven-install-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>${maven-jar-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>${maven-javadoc-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jxr-plugin</artifactId> + <version>${maven-jxr-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-plugin-plugin</artifactId> + <version>${maven-plugin-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-pmd-plugin</artifactId> + <version>${maven-pmd-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>${maven-project-info-reports-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-repository-plugin</artifactId> + <version>${maven-repository-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>${maven-resources-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-site-plugin</artifactId> + <version>${maven-site-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>${maven-source-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-report-plugin</artifactId> + <version>${maven-surefire-report-plugin.version}</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-toolchains-plugin</artifactId> + <version>${maven-toolchains-plugin.version}</version> + </plugin> + <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.--> + <plugin> + <groupId>org.eclipse.m2e</groupId> + <artifactId>lifecycle-mapping</artifactId> + <version>1.0.0</version> + <configuration> + <lifecycleMappingMetadata> + <pluginExecutions> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.codehaus.mojo</groupId> + <artifactId> + exec-maven-plugin + </artifactId> + <versionRange>[1.2.1,)</versionRange> + <goals> + <goal>java</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + </pluginExecutions> + </lifecycleMappingMetadata> + </configuration> + </plugin> + </plugins> + </pluginManagement> + </build> + + <!-- ==================================================================== --> + <prerequisites> + <maven>3.0.4</maven> + </prerequisites> + <dependencies> + <dependency> + <groupId>org.joda</groupId> + <artifactId>joda-convert</artifactId> + <version>1.2</version> + <scope>compile</scope> + <optional>true</optional><!-- mandatory in Scala --> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>3.8.2</version> + <scope>test</scope> + </dependency> + </dependencies> + + <!-- ==================================================================== --> + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-project-info-reports-plugin</artifactId> + <version>${maven-project-info-plugin.version}</version> + <reportSets> + <reportSet> + <reports> + <report>dependencies</report> + <report>dependency-info</report> + <report>issue-tracking</report> + <report>license</report> + <report>mailing-list</report> + <report>project-team</report> + <report>scm</report> + <report>summary</report> + </reports> + </reportSet> + </reportSets> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>${maven-javadoc-plugin.version}</version> + <reportSets> + <reportSet> + <reports> + <report>javadoc</report> + </reports> + </reportSet> + </reportSets> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-report-plugin</artifactId> + <version>${maven-surefire-report-plugin.version}</version> + <configuration> + <showSuccess>true</showSuccess> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jxr-plugin</artifactId> + <version>${maven-jxr-plugin.version}</version> + <reportSets> + <reportSet> + <reports> + <report>jxr</report> + </reports> + </reportSet> + </reportSets> + </plugin> + </plugins> + </reporting> + + <!-- ==================================================================== --> + <distributionManagement> + <repository> + <id>sonatype-joda-staging</id> + <name>Sonatype OSS staging repository</name> + <url>http://oss.sonatype.org/service/local/staging/deploy/maven2/</url> + <layout>default</layout> + </repository> + <snapshotRepository> + <uniqueVersion>false</uniqueVersion> + <id>sonatype-joda-snapshot</id> + <name>Sonatype OSS snapshot repository</name> + <url>http://oss.sonatype.org/content/repositories/joda-snapshots</url> + <layout>default</layout> + </snapshotRepository> + <downloadUrl>http://oss.sonatype.org/content/repositories/joda-releases</downloadUrl> + </distributionManagement> + + <!-- ==================================================================== --> + <profiles> + <profile> + <id>repo-sign-artifacts</id> + <activation> + <property> + <name>oss.repo</name> + <value>true</value> + </property> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-toolchains-plugin</artifactId> + <executions> + <execution> + <phase>validate</phase> + <goals> + <goal>toolchain</goal> + </goals> + </execution> + </executions> + <configuration> + <toolchains> + <jdk> + <version>1.5</version> + <vendor>sun</vendor> + </jdk> + </toolchains> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>verify</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <!-- ==================================================================== --> + <properties> + <!-- Plugin version numbers --> + <maven-assembly-plugin.version>2.4</maven-assembly-plugin.version> + <maven-changes-plugin.version>2.9</maven-changes-plugin.version> + <maven-checkstyle-plugin.version>2.10</maven-checkstyle-plugin.version> + <maven-clean-plugin.version>2.5</maven-clean-plugin.version> + <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version> + <maven-deploy-plugin.version>2.7</maven-deploy-plugin.version> + <maven-dependency-plugin.version>2.8</maven-dependency-plugin.version> + <maven-gpg-plugin.version>1.4</maven-gpg-plugin.version> + <maven-install-plugin.version>2.4</maven-install-plugin.version> + <maven-jar-plugin.version>2.4</maven-jar-plugin.version> + <maven-javadoc-plugin.version>2.9.1</maven-javadoc-plugin.version> + <maven-jxr-plugin.version>2.3</maven-jxr-plugin.version> + <maven-plugin-plugin.version>3.2</maven-plugin-plugin.version> + <maven-pmd-plugin.version>3.0.1</maven-pmd-plugin.version> + <maven-project-info-reports-plugin.version>2.7</maven-project-info-reports-plugin.version> + <maven-repository-plugin.version>2.3.1</maven-repository-plugin.version> + <maven-resources-plugin.version>2.6</maven-resources-plugin.version> + <maven-site-plugin.version>3.3</maven-site-plugin.version> + <maven-source-plugin.version>2.2.1</maven-source-plugin.version> + <maven-surefire-plugin.version>2.15</maven-surefire-plugin.version> + <maven-surefire-report-plugin.version>2.15</maven-surefire-report-plugin.version> + <maven-toolchains-plugin.version>1.0</maven-toolchains-plugin.version> + <!-- Properties for maven-compiler-plugin --> + <maven.compiler.compilerVersion>1.5</maven.compiler.compilerVersion> + <maven.compiler.source>1.5</maven.compiler.source> + <maven.compiler.target>1.5</maven.compiler.target> + <maven.compiler.fork>true</maven.compiler.fork> + <maven.compiler.verbose>true</maven.compiler.verbose> + <maven.compiler.optimize>true</maven.compiler.optimize> + <maven.compiler.debug>true</maven.compiler.debug> + <maven.compiler.debuglevel>lines,source</maven.compiler.debuglevel> + <!-- Properties for maven-javadoc-plugin --> + <author>false</author> + <notimestamp>true</notimestamp> + <!-- Properties for maven-checkstyle-plugin --> + <checkstyle.config.location>${project.basedir}/src/main/checkstyle/checkstyle.xml</checkstyle.config.location> + <!-- Other properties --> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + </properties> +</project> diff --git a/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar b/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..bcea83745ab32246e315dd0c53209dc6a0cd39b9 Binary files /dev/null and b/Timeline/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar differ diff --git a/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar b/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..0ec70e33be9c8175ec567f86b54a8a13d6f297f8 Binary files /dev/null and b/Timeline/release/modules/ext/sqlite-jdbc-3.8.0-SNAPSHOT.jar differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties new file mode 100644 index 0000000000000000000000000000000000000000..a2a287e3220501c96467e63b8803cacb164f7b9a --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/Bundle.properties @@ -0,0 +1,37 @@ +OpenIDE-Module-Name=Timeline +CTL_MakeTimeline="Timeline" +Timeline.frameName.text={0} - Autopsy Timeline +Timeline.resultsPanel.title=Timeline Results +Timeline.runJavaFxThread.progress.creating=Creating timeline . . . +Timeline.zoomOutButton.text=Zoom Out +Timeline.goToButton.text=Go To\: +Timeline.yearBarChart.x.years=Years +Timeline.yearBarChart.y.numEvents=Number of Events +Timeline.MonthsBarChart.x.monthYY=Month ({0}) +Timeline.MonthsBarChart.y.numEvents=Number of Events +Timeline.eventsByMoBarChart.x.dayOfMo=Day of Month +Timeline.eventsByMoBarChart.y.numEvents=Number of Events +Timeline.node.emptyRoot=Empty Root +Timeline.resultPanel.loading=Loading... +Timeline.node.root=Root +Timeline.propChg.confDlg.timelineOOD.msg=The event data is out of date. Would you like to regenerate it? +Timeline.propChg.confDlg.timelineOOD.details=Select an option +Timeline.initTimeline.confDlg.genBeforeIngest.msg=You are trying to generate a timeline before ingest has been completed. The timeline may be incomplete. Do you want to continue? +Timeline.initTimeline.confDlg.genBeforeIngest.details=Timeline +TimelineFrame.title=Timeline +TimelinePanel.jButton1.text=6m +TimelinePanel.jButton13.text=all +TimelinePanel.jButton10.text=1h +TimelinePanel.jButton9.text=12h +TimelinePanel.jButton11.text=5y +TimelinePanel.jButton12.text=10y +TimelinePanel.jButton6.text=1w +TimelinePanel.jButton5.text=1y +TimelinePanel.jButton8.text=1d +TimelinePanel.jButton7.text=3d +TimelinePanel.jButton2.text=1m +TimelinePanel.jButton3.text=3m +TimelinePanel.jButton4.text=2w +ProgressWindow.progressHeader.text=\ +AggregateEvent.differentTypes="aggregate events are not compatible they have different types" +AggregateEvent.differentDescriptions="aggregate events are not compatible they have different descriptions" diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java b/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java new file mode 100644 index 0000000000000000000000000000000000000000..685d50d3d56b8231946f85f3a4fbb7feb226c97f --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java @@ -0,0 +1,145 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import org.openide.util.Exceptions; + +/** + * This class support both programmer productivity by abstracting frequently + * used code to load FXML-defined GUI components, and code performance by + * implementing a caching FXMLLoader as described at + * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. + * + * TODO: this code is duplicated in the Image Analyze module, we should move it + * into a centralized places like a JavaFX utils class/package/module in + * Autopsy- jm + */ +public class FXMLConstructor { + + private static final CachingClassLoader CACHING_CLASS_LOADER = new CachingClassLoader((FXMLLoader.getDefaultClassLoader())); + + static public void construct(Node n, String fxmlFileName) { + FXMLLoader fxmlLoader = new FXMLLoader(n.getClass().getResource(fxmlFileName)); + fxmlLoader.setRoot(n); + fxmlLoader.setController(n); + fxmlLoader.setClassLoader(CACHING_CLASS_LOADER); + + try { + fxmlLoader.load(); + } catch (Exception exception) { + try { + fxmlLoader.setClassLoader(FXMLLoader.getDefaultClassLoader()); + fxmlLoader.load(); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + } + } + } + + /** + * The default FXMLLoader does not cache information about previously loaded + * FXML files. See + * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. + * for more details. As a partial workaround, we cache information on + * previously loaded classes. This does not solve all performance issues, + * but is a big improvement. + */ + static public class CachingClassLoader extends ClassLoader { + + private final Map<String, Class<?>> classes = new HashMap<>(); + + private final ClassLoader parent; + + public CachingClassLoader(ClassLoader parent) { + this.parent = parent; + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + Class<?> c = findClass(name); + if (c == null) { + throw new ClassNotFoundException(name); + } + return c; + } + + @Override + protected Class<?> findClass(String className) throws ClassNotFoundException { +// System.out.print("try to load " + className); + if (classes.containsKey(className)) { + Class<?> result = classes.get(className); + return result; + } else { + try { + Class<?> result = parent.loadClass(className); +// System.out.println(" -> success!"); + classes.put(className, result); + return result; + } catch (ClassNotFoundException ignore) { +// System.out.println(); + classes.put(className, null); + return null; + } + } + } + + // ========= delegating methods ============= + @Override + public URL getResource(String name) { + return parent.getResource(name); + } + + @Override + public Enumeration<URL> getResources(String name) throws IOException { + return parent.getResources(name); + } + + @Override + public String toString() { + return parent.toString(); + } + + @Override + public void setDefaultAssertionStatus(boolean enabled) { + parent.setDefaultAssertionStatus(enabled); + } + + @Override + public void setPackageAssertionStatus(String packageName, boolean enabled) { + parent.setPackageAssertionStatus(packageName, enabled); + } + + @Override + public void setClassAssertionStatus(String className, boolean enabled) { + parent.setClassAssertionStatus(className, enabled); + } + + @Override + public void clearAssertionStatus() { + parent.clearAssertionStatus(); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java b/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java new file mode 100644 index 0000000000000000000000000000000000000000..5a0a55ba4f03eaa8d935b485c674d1c4eeef69b5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/LoggedTask.java @@ -0,0 +1,79 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javafx.concurrent.Task; +import org.openide.util.Cancellable; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * + */ +public abstract class LoggedTask<T> extends Task<T> implements Cancellable { + + private static final Logger LOGGER = Logger.getLogger(LoggedTask.class.getName()); + + private final Boolean logStateChanges; + + public LoggedTask(String taskName, Boolean logStateChanges) { + updateTitle(taskName); + this.logStateChanges = logStateChanges; + } + + @Override + protected void cancelled() { + super.cancelled(); + if (logStateChanges) { + LOGGER.log(Level.WARNING, "{0} cancelled!", getTitle()); + } + } + + @Override + protected void failed() { + super.failed(); + LOGGER.log(Level.SEVERE, getTitle() + " failed!", getException()); + + } + + @Override + protected void scheduled() { + super.scheduled(); + if (logStateChanges) { + LOGGER.log(Level.INFO, "{0} scheduled", getTitle()); + } + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + get(); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } catch (ExecutionException ex) { + Exceptions.printStackTrace(ex); + } + if (logStateChanges) { + LOGGER.log(Level.INFO, "{0} succeeded", getTitle()); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java b/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java new file mode 100644 index 0000000000000000000000000000000000000000..fd1f450370a1f2064d0d8734c12bc62b5b270abd --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java @@ -0,0 +1,96 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.util.logging.Level; +import javax.swing.JOptionPane; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionReferences; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.actions.CallableSystemAction; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.Installer; +import org.sleuthkit.autopsy.coreutils.Logger; + +@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline") +@ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false) +@ActionReferences(value = { + @ActionReference(path = "Menu/Tools", position = 100)}) +public class OpenTimelineAction extends CallableSystemAction { + + private static final Logger LOGGER = Logger.getLogger(OpenTimelineAction.class.getName()); + + private static final boolean fxInited = Installer.isJavaFxInited(); + + synchronized static void invalidateController() { + timeLineController = null; + } + + private static TimeLineController timeLineController = null; + + public OpenTimelineAction() { + super(); + } + + @Override + public boolean isEnabled() { + return Case.isCaseOpen() && fxInited && Case.getCurrentCase().hasData(); + } + + @Override + public void performAction() { + + //check case + if (!Case.existsCurrentCase()) { + return; + } + final Case currentCase = Case.getCurrentCase(); + + if (currentCase.hasData() == false) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), "Error creating timeline, there are no data sources."); + LOGGER.log(Level.INFO, "Error creating timeline, there are no data sources."); + return; + } + synchronized (OpenTimelineAction.class) { + if (timeLineController == null) { + timeLineController = new TimeLineController(); + LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S"); + } + } + timeLineController.openTimeLine(); + } + + @Override + public String getName() { + return "Timeline"; + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form similarity index 81% rename from Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form rename to Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form index 0a8a131d097019f41d3cd82e78a62ba822fbeebe..6c99e006c8d2b9d47d2dacfde1fb5ad5cce3e577 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineProgressDialog.form +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.form @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8" ?> -<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JDialogFormInfo"> +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> <SyntheticProperties> <SyntheticProperty name="formSizePolicy" type="int" value="1"/> <SyntheticProperty name="generateCenter" type="boolean" value="false"/> @@ -28,7 +28,7 @@ <Group type="103" groupAlignment="0" attributes="0"> <Component id="progressBar" pref="504" max="32767" attributes="0"/> <Group type="102" alignment="0" attributes="0"> - <Component id="jLabel1" min="-2" max="-2" attributes="0"/> + <Component id="progressHeader" min="-2" max="-2" attributes="0"/> <EmptySpace min="0" pref="0" max="32767" attributes="0"/> </Group> </Group> @@ -40,23 +40,23 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> <EmptySpace max="-2" attributes="0"/> - <Component id="jLabel1" min="-2" max="-2" attributes="0"/> - <EmptySpace min="-2" pref="7" max="-2" attributes="0"/> + <Component id="progressHeader" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> <Component id="progressBar" min="-2" max="-2" attributes="0"/> - <EmptySpace pref="16" max="32767" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> </Group> </Group> </DimensionLayout> </Layout> <SubComponents> - <Component class="javax.swing.JLabel" name="jLabel1"> + <Component class="javax.swing.JProgressBar" name="progressBar"> + </Component> + <Component class="javax.swing.JLabel" name="progressHeader"> <Properties> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="org/sleuthkit/autopsy/timeline/Bundle.properties" key="TimelineProgressDialog.jLabel1.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <ResourceString bundle="org/sleuthkit/autopsy/advancedtimeline/Bundle.properties" key="ProgressWindow.progressHeader.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </Property> </Properties> </Component> - <Component class="javax.swing.JProgressBar" name="progressBar"> - </Component> </SubComponents> </Form> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..3fb9624f2a5c88d31d900e98d68484c54402af3a --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ProgressWindow.java @@ -0,0 +1,278 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.annotation.concurrent.Immutable; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.GroupLayout; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JProgressBar; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import org.openide.awt.Mnemonics; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; + +/** + * Dialog with progress bar that pops up when timeline is being generated + */ +public class ProgressWindow extends JFrame { + + private final SwingWorker<?,?> worker; + + /** + * Creates new form TimelineProgressDialog + */ + public ProgressWindow(Component parent, boolean modal, SwingWorker<?,?> worker) { + super(); + initComponents(); + + setLocationRelativeTo(parent); + + setAlwaysOnTop(modal); + + //set icon the same as main app + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + setIconImage(WindowManager.getDefault().getMainWindow().getIconImage()); + } + }); + + + //progressBar.setIndeterminate(true); + + setName("Advanced Timeline"); + setTitle("Generating Advanced Timeline data"); + // Close the dialog when Esc is pressed + String cancelName = "cancel"; + InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelName); + ActionMap actionMap = getRootPane().getActionMap(); + + actionMap.put(cancelName, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + cancel(); + } + }); + this.worker = worker; + } + + public void updateProgress(final int progress) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + + progressBar.setValue(progress); + } + }); + } + + public void updateProgress(final int progress, final String message) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + + progressBar.setValue(progress); + progressBar.setString(message); + } + }); + } + + public void updateProgress(final String message) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + + progressBar.setString(message); + } + }); + } + + public void setProgressTotal(final int total) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressBar.setIndeterminate(false); + progressBar.setMaximum(total); + progressBar.setStringPainted(true); + + } + }); + } + + public void updateHeaderMessage(final String headerMessage) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressHeader.setText(headerMessage); + + } + }); + } + + public void setIndeterminate() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressBar.setIndeterminate(true); + progressBar.setStringPainted(true); + + } + }); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + progressBar = new JProgressBar(); + progressHeader = new JLabel(); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent evt) { + closeDialog(evt); + } + }); + + Mnemonics.setLocalizedText(progressHeader, NbBundle.getMessage(ProgressWindow.class, "ProgressWindow.progressHeader.text")); // NOI18N + + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(progressBar, GroupLayout.DEFAULT_SIZE, 504, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(progressHeader) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(progressHeader) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(progressBar, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + pack(); + }// </editor-fold>//GEN-END:initComponents + + /** + * Closes the dialog + */ + private void closeDialog(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_closeDialog + cancel(); + }//GEN-LAST:event_closeDialog + + public void cancel() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (isVisible()) { + int showConfirmDialog = JOptionPane.showConfirmDialog(ProgressWindow.this, "Do you want to cancel time line creation?", "Cancel timeline creation?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (showConfirmDialog == JOptionPane.YES_OPTION) { + close(); + } + } else { + close(); + } + } + }); + } + + public void close() { + worker.cancel(false); + setVisible(false); + dispose(); + } + // Variables declaration - do not modify//GEN-BEGIN:variables + private JProgressBar progressBar; + private JLabel progressHeader; + // End of variables declaration//GEN-END:variables + + public void update(ProgressUpdate chunk) { + updateHeaderMessage(chunk.getHeaderMessage()); + if (chunk.getTotal() >= 0) { + setProgressTotal(chunk.getTotal()); + updateProgress(chunk.getProgress(), chunk.getDetailMessage()); + } else { + setIndeterminate(); + updateProgress(chunk.getDetailMessage()); + } + } + + /** bundles up progress information to be shown in the progress dialog */ + @Immutable + public static class ProgressUpdate { + + private final int progress; + private final int total; + private final String headerMessage; + private final String detailMessage; + + public int getProgress() { + return progress; + } + + public int getTotal() { + return total; + } + + public String getHeaderMessage() { + return headerMessage; + } + + public String getDetailMessage() { + return detailMessage; + } + + public ProgressUpdate(int progress, int total, String headerMessage, String detailMessage) { + super(); + this.progress = progress; + this.total = total; + this.headerMessage = headerMessage; + this.detailMessage = detailMessage; + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java new file mode 100644 index 0000000000000000000000000000000000000000..97426a5f4e350baf883195e52c328b95cac255df --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -0,0 +1,736 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.awt.HeadlessException; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZoneId; +import java.util.Collection; +import java.util.MissingResourceException; +import java.util.TimeZone; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyListProperty; +import javafx.beans.property.ReadOnlyListWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.joda.time.ReadablePeriod; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.filters.Filter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; +import org.sleuthkit.autopsy.timeline.utils.ObservableStack; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.autopsy.casemodule.Case; +import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; +import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** Controller in the MVC design along with model = {@link FilteredEventsModel} + * and views = {@link TimeLineView}. Forwards interpreted user gestures form + * views to model. Provides model to view. Is entry point for timeline module. + * + * Concurrency Policy:<ul> + * <li>Since filteredEvents is internally synchronized, only compound access to + * it needs external synchronization</li> + * * <li>Since eventsRepository is internally synchronized, only compound + * access to it needs external synchronization <li> + * <li>Other state including listeningToAutopsy, mainFrame, viewMode, and the + * listeners should only be accessed with this object's intrinsic lock held + * </li> + * <ul> + */ +public class TimeLineController { + + private static final Logger LOGGER = Logger.getLogger(TimeLineController.class.getName()); + + private static final String DO_REPOPULATE_MESSAGE = "The events databse was prevously populated while ingest was running.\n" + "Some events may not have been populated or may have been populated inaccurately.\n" + "Do you want to repopulate the events database now?"; + + private static final ReadOnlyObjectWrapper<TimeZone> timeZone = new ReadOnlyObjectWrapper<>(TimeZone.getDefault()); + + public static ZoneId getTimeZoneID() { + return timeZone.get().toZoneId(); + } + + public static DateTimeFormatter getZonedFormatter() { + return DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss").withZone(getJodaTimeZone()); + } + + public static DateTimeZone getJodaTimeZone() { + return DateTimeZone.forTimeZone(getTimeZone().get()); + } + + public static ReadOnlyObjectProperty<TimeZone> getTimeZone() { + return timeZone.getReadOnlyProperty(); + } + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + private final ReadOnlyListWrapper<Task<?>> tasks = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); + + private final ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(-1); + + private final ReadOnlyStringWrapper message = new ReadOnlyStringWrapper(); + + private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper(); + + synchronized public ReadOnlyListProperty<Task<?>> getTasks() { + return tasks.getReadOnlyProperty(); + } + + synchronized public ReadOnlyDoubleProperty getProgress() { + return progress.getReadOnlyProperty(); + } + + synchronized public ReadOnlyStringProperty getMessage() { + return message.getReadOnlyProperty(); + } + + synchronized public ReadOnlyStringProperty getTaskTitle() { + return taskTitle.getReadOnlyProperty(); + } + + @GuardedBy("this") + private TimeLineTopComponent mainFrame; + + //are the listeners currently attached + @GuardedBy("this") + private boolean listeningToAutopsy = false; + + private final PropertyChangeListener caseListener; + + private final PropertyChangeListener ingestJobListener = new AutopsyIngestJobListener(); + + private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener(); + + @GuardedBy("this") + private final ReadOnlyObjectWrapper<VisualizationMode> viewMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); + + synchronized public ReadOnlyObjectProperty<VisualizationMode> getViewMode() { + return viewMode.getReadOnlyProperty(); + } + + @GuardedBy("filteredEvents") + private final FilteredEventsModel filteredEvents; + + @GuardedBy("eventsRepository") + private final EventsRepository eventsRepository; + + @GuardedBy("this") + private final ZoomParams InitialZoomState; + + /** list based stack to hold history, 'top' is at index 0; */ + @GuardedBy("this") + private final ObservableStack<ZoomParams> historyStack = new ObservableStack<>(); + + @GuardedBy("this") + private final ObservableStack<ZoomParams> forwardStack = new ObservableStack<>(); + + synchronized public ObservableStack<ZoomParams> getHistoryStack() { + return historyStack; + } + + synchronized public ObservableStack<ZoomParams> getForwardStack() { + return forwardStack; + } + + //all members should be access with the intrinsict lock of this object held + //selected events (ie shown in the result viewer) + @GuardedBy("this") + private final ObservableList<Long> selectedEventIDs = FXCollections.<Long>synchronizedObservableList(FXCollections.<Long>observableArrayList()); + + /** + * @return an unmodifiable list of the selected event ids + */ + synchronized public ObservableList<Long> getSelectedEventIDs() { + return selectedEventIDs; + } + + @GuardedBy("this") + private final ReadOnlyObjectWrapper<Interval> selectedTimeRange = new ReadOnlyObjectWrapper<>(); + + /** + * @return a read only view of the selected interval. + */ + synchronized public ReadOnlyObjectProperty<Interval> getSelectedTimeRange() { + return selectedTimeRange.getReadOnlyProperty(); + } + + public ReadOnlyBooleanProperty getNewEventsFlag() { + return newEventsFlag.getReadOnlyProperty(); + } + + private final ReadOnlyBooleanWrapper needsHistogramRebuild = new ReadOnlyBooleanWrapper(false); + + public ReadOnlyBooleanProperty getNeedsHistogramRebuild() { + return needsHistogramRebuild.getReadOnlyProperty(); + } + + private final ReadOnlyBooleanWrapper newEventsFlag = new ReadOnlyBooleanWrapper(false); + + public TimeLineController() { + //initalize repository and filteredEvents on creation + eventsRepository = new EventsRepository(); + + filteredEvents = eventsRepository.getEventsModel(); + InitialZoomState = new ZoomParams(filteredEvents.getSpanningInterval(), + EventTypeZoomLevel.BASE_TYPE, + Filter.getDefaultFilter(), + DescriptionLOD.SHORT); + filteredEvents.requestZoomState(InitialZoomState, true); + //persistent listener instances + caseListener = new AutopsyCaseListener(); + + } + + /** @return a shared events model */ + public FilteredEventsModel getEventsModel() { + return filteredEvents; + } + + public void applyDefaultFilters() { + pushFilters(Filter.getDefaultFilter()); + } + + public void zoomOutToActivity() { + Interval boundingEventsInterval = filteredEvents.getBoundingEventsInterval(); + pushZoom(filteredEvents.getRequestedZoomParamters().get().withTimeRange(boundingEventsInterval)); + } + + boolean rebuildRepo() { + if (IngestManager.getInstance().isIngestRunning()) { + //confirm timeline during ingest + if (showIngestConfirmation() != JOptionPane.YES_OPTION) { + return false; + } + } + LOGGER.log(Level.INFO, "Beginning generation of timeline"); + try { + SwingUtilities.invokeLater(() -> { + if (mainFrame != null) { + mainFrame.close(); + } + }); + final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + final long lastObjId = sleuthkitCase.getLastObjectId(); + final long lastArtfID = getCaseLastArtifactID(sleuthkitCase); + final Boolean injestRunning = IngestManager.getInstance().isIngestRunning(); + //TODO: verify this locking is correct? -jm + synchronized (eventsRepository) { + eventsRepository.rebuildRepository(() -> { + + synchronized (eventsRepository) { + eventsRepository.recordLastObjID(lastObjId); + eventsRepository.recordLastArtifactID(lastArtfID); + eventsRepository.recordWasIngestRunning(injestRunning); + } + synchronized (TimeLineController.this) { + needsHistogramRebuild.set(true); + needsHistogramRebuild.set(false); + showWindow(); + } + + Platform.runLater(() -> { + newEventsFlag.set(false); + TimeLineController.this.showFullRange(); + }); + }); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex); + return false; + } + return true; + } + + public void showFullRange() { + synchronized (filteredEvents) { + pushTimeRange(filteredEvents.getSpanningInterval()); + } + } + + synchronized public void closeTimeLine() { + if (mainFrame != null) { + listeningToAutopsy = false; + IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener); + IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); + Case.removePropertyChangeListener(caseListener); + mainFrame.close(); + mainFrame.setVisible(false); + mainFrame = null; + } + } + + /** show the timeline window and prompt for rebuilding database */ + synchronized void openTimeLine() { + + // listen for case changes (specifically images being added, and case changes). + if (Case.isCaseOpen() && !listeningToAutopsy) { + IngestManager.getInstance().addIngestModuleEventListener(ingestModuleListener); + IngestManager.getInstance().addIngestJobEventListener(ingestJobListener); + Case.addPropertyChangeListener(caseListener); + listeningToAutopsy = true; + } + + try { + long timeLineLastObjectId = eventsRepository.getLastObjID(); + + boolean rebuildingRepo = false; + if (timeLineLastObjectId == -1) { + rebuildingRepo = rebuildRepo(); + } + if (rebuildingRepo == false + && eventsRepository.getWasIngestRunning()) { + if (showLastPopulatedWhileIngestingConfirmation() == JOptionPane.YES_OPTION) { + rebuildingRepo = rebuildRepo(); + } + } + final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + if ((rebuildingRepo == false) + && (sleuthkitCase.getLastObjectId() != timeLineLastObjectId + || getCaseLastArtifactID(sleuthkitCase) != eventsRepository.getLastArtfactID())) { + rebuildingRepo = outOfDatePromptAndRebuild(); + } + + if (rebuildingRepo == false) { + showWindow(); + showFullRange(); + } + + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error when generating timeline, ", ex); + } catch (HeadlessException | MissingResourceException ex) { + LOGGER.log(Level.SEVERE, "Unexpected error when generating timeline, ", ex); + } + } + + private long getCaseLastArtifactID(final SleuthkitCase sleuthkitCase) { + long caseLastArtfId = -1; + try (ResultSet runQuery = sleuthkitCase.runQuery("select Max(artifact_id) as max_id from blackboard_artifacts")) { + while (runQuery.next()) { + caseLastArtfId = runQuery.getLong("max_id"); + } + sleuthkitCase.closeRunQuery(runQuery); + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } + return caseLastArtfId; + } + + /** + * request a time range the same length as the given period and centered + * around the middle of the currently selected range + * + * @param period + */ + synchronized public void pushPeriod(ReadablePeriod period) { + synchronized (filteredEvents) { + final DateTime middleOf = IntervalUtils.middleOf(filteredEvents.timeRange().get()); + pushTimeRange(IntervalUtils.getIntervalAround(middleOf, period)); + } + } + + synchronized public void pushZoomOutTime() { + final Interval timeRange = filteredEvents.timeRange().get(); + long toDurationMillis = timeRange.toDurationMillis() / 4; + DateTime start = timeRange.getStart().minus(toDurationMillis); + DateTime end = timeRange.getEnd().plus(toDurationMillis); + pushTimeRange(new Interval(start, end)); + } + + synchronized public void pushZoomInTime() { + final Interval timeRange = filteredEvents.timeRange().get(); + long toDurationMillis = timeRange.toDurationMillis() / 4; + DateTime start = timeRange.getStart().plus(toDurationMillis); + DateTime end = timeRange.getEnd().minus(toDurationMillis); + pushTimeRange(new Interval(start, end)); + } + + synchronized public void setViewMode(VisualizationMode visualizationMode) { + if (viewMode.get() != visualizationMode) { + viewMode.set(visualizationMode); + } + } + + public void selectEventIDs(Collection<Long> events) { + final LoggedTask<Interval> selectEventIDsTask = new LoggedTask<Interval>("Select Event IDs", true) { + @Override + protected Interval call() throws Exception { + return filteredEvents.getSpanningInterval(events); + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + synchronized (TimeLineController.this) { + selectedTimeRange.set(get()); + selectedEventIDs.setAll(events); + } + } catch (InterruptedException ex) { + Logger.getLogger(FilteredEventsModel.class + .getName()).log(Level.SEVERE, getTitle() + " interrupted unexpectedly", ex); + } catch (ExecutionException ex) { + Logger.getLogger(FilteredEventsModel.class + .getName()).log(Level.SEVERE, getTitle() + " unexpectedly threw " + ex.getCause(), ex); + } + } + }; + + monitorTask(selectEventIDsTask); + } + + /** + * private method to build gui if necessary and make it visible. + */ + synchronized private void showWindow() { + if (mainFrame == null) { + LOGGER.log(Level.WARNING, "Tried to show timeline with invalid window. Rebuilding GUI."); + mainFrame = (TimeLineTopComponent) WindowManager.getDefault().findTopComponent("TimeLineTopComponent"); + if (mainFrame == null) { + mainFrame = new TimeLineTopComponent(); + } + mainFrame.setController(this); + } + SwingUtilities.invokeLater(() -> { + mainFrame.open(); + mainFrame.setVisible(true); + mainFrame.toFront(); + }); + } + + synchronized public void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel) { + ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + if (currentZoom == null) { + pushZoom(InitialZoomState.withTypeZoomLevel(typeZoomeLevel)); + } else if (currentZoom.hasTypeZoomLevel(typeZoomeLevel) == false) { + pushZoom(currentZoom.withTypeZoomLevel(typeZoomeLevel)); + } + } + + synchronized public void pushTimeRange(Interval timeRange) { +// timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); + ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + if (currentZoom == null) { + pushZoom(InitialZoomState.withTimeRange(timeRange)); + } else if (currentZoom.hasTimeRange(timeRange) == false) { + pushZoom(currentZoom.withTimeRange(timeRange)); + } + } + + synchronized public void pushDescrLOD(DescriptionLOD newLOD) { + ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + if (currentZoom == null) { + pushZoom(InitialZoomState.withDescrLOD(newLOD)); + } else if (currentZoom.hasDescrLOD(newLOD) == false) { + pushZoom(currentZoom.withDescrLOD(newLOD)); + } + } + + synchronized public void pushTimeAndType(Interval timeRange, EventTypeZoomLevel typeZoom) { + timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); + ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + if (currentZoom == null) { + pushZoom(InitialZoomState.withTimeAndType(timeRange, typeZoom)); + } else if (currentZoom.hasTimeRange(timeRange) == false && currentZoom.hasTypeZoomLevel(typeZoom) == false) { + pushZoom(currentZoom.withTimeAndType(timeRange, typeZoom)); + } else if (currentZoom.hasTimeRange(timeRange) == false) { + pushZoom(currentZoom.withTimeRange(timeRange)); + } else if (currentZoom.hasTypeZoomLevel(typeZoom) == false) { + pushZoom(currentZoom.withTypeZoomLevel(typeZoom)); + } + } + + synchronized public void pushFilters(Filter filter) { + ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + if (currentZoom == null) { + pushZoom(InitialZoomState.withFilter(filter.copyOf())); + } else if (currentZoom.hasFilter(filter) == false) { + pushZoom(currentZoom.withFilter(filter.copyOf())); + } + } + + synchronized public ZoomParams goForward() { + + final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + + final ZoomParams fpeek = forwardStack.peek(); + + if (fpeek != null && currentZoom.equals(fpeek) == false) { + historyStack.push(currentZoom); + filteredEvents.requestZoomState(fpeek, false); + forwardStack.pop(); + } + return fpeek; + } + + synchronized public ZoomParams goBack() { + final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + final ZoomParams peek = historyStack.peek(); + + if (peek != null && peek.equals(currentZoom) == false) { + forwardStack.push(currentZoom); + filteredEvents.requestZoomState(historyStack.pop(), false); + } else if (peek != null && peek.equals(currentZoom)) { + historyStack.pop(); + return goBack(); + } + return peek; + } + + @Deprecated + synchronized public void popZoomUpTo(ZoomParams zCrumb) { + ZoomParams popped = historyStack.peek(); + + while (popped.equals(zCrumb) == false) { + popped = historyStack.pop(); + } + pushZoom(zCrumb); + } + + synchronized private void pushZoom(ZoomParams zCrumb, boolean force) { + final ZoomParams currentZoom = filteredEvents.getRequestedZoomParamters().get(); + + if (force || currentZoom.equals(zCrumb) == false) { + historyStack.push(currentZoom); + filteredEvents.requestZoomState(zCrumb, force); + if (zCrumb.equals(forwardStack.peek())) { + forwardStack.pop(); + } else { + forwardStack.clear(); + } + } + } + + synchronized private void pushZoom(ZoomParams zCrumb) { + pushZoom(zCrumb, false); + } + + public void selectTimeAndType(Interval interval, EventType type) { + final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval); + + final LoggedTask<Collection<Long>> selectTimeAndTypeTask = new LoggedTask<Collection<Long>>("Select Time and Type", true) { + @Override + protected Collection< Long> call() throws Exception { + synchronized (TimeLineController.this) { + return filteredEvents.getEventIDs(timeRange, new TypeFilter(type)); + } + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + synchronized (TimeLineController.this) { + selectedTimeRange.set(timeRange); + selectedEventIDs.setAll(get()); + } + } catch (InterruptedException ex) { + Logger.getLogger(FilteredEventsModel.class + .getName()).log(Level.SEVERE, getTitle() + " interrupted unexpectedly", ex); + } catch (ExecutionException ex) { + Logger.getLogger(FilteredEventsModel.class + .getName()).log(Level.SEVERE, getTitle() + " unexpectedly threw " + ex.getCause(), ex); + } + } + }; + + monitorTask(selectTimeAndTypeTask); + } + + synchronized public void monitorTask(final Task<?> task) { + if (task != null) { + Platform.runLater(() -> { + + //is this actually threadsafe, could we get a finished task stuck in the list? + task.stateProperty().addListener((Observable observable) -> { + switch (task.getState()) { + case READY: + case RUNNING: + case SCHEDULED: + break; + case SUCCEEDED: + case CANCELLED: + case FAILED: + tasks.remove(task); + if (tasks.isEmpty() == false) { + progress.bind(tasks.get(0).progressProperty()); + message.bind(tasks.get(0).messageProperty()); + taskTitle.bind(tasks.get(0).titleProperty()); + } + break; + } + }); + tasks.add(task); + progress.bind(task.progressProperty()); + message.bind(task.messageProperty()); + taskTitle.bind(task.titleProperty()); + switch (task.getState()) { + case READY: + executor.submit(task); + break; + case SCHEDULED: + case RUNNING: + case SUCCEEDED: + case CANCELLED: + case FAILED: + break; + } + }); + } + } + + static synchronized public void setTimeZone(TimeZone timeZone) { + TimeLineController.timeZone.set(timeZone); + } + + Interval getSpanningInterval(Collection<Long> eventIDs) { + return filteredEvents.getSpanningInterval(eventIDs); + + } + + /** + * prompt the user to rebuild and then rebuild if the user chooses to + */ + synchronized public boolean outOfDatePromptAndRebuild() { + return showOutOfDateConfirmation() == JOptionPane.YES_OPTION + ? rebuildRepo() + : false; + } + + synchronized int showLastPopulatedWhileIngestingConfirmation() { + return JOptionPane.showConfirmDialog(mainFrame, + DO_REPOPULATE_MESSAGE, + "re populate events?", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + + } + + synchronized int showOutOfDateConfirmation() throws MissingResourceException, HeadlessException { + return JOptionPane.showConfirmDialog(mainFrame, + NbBundle.getMessage(TimeLineController.class, + "AdvTimeline.propChg.confDlg.timelineOOD.msg"), + NbBundle.getMessage(TimeLineController.class, + "AdvTimeline.propChg.confDlg.timelineOOD.details"), + JOptionPane.YES_NO_OPTION); + } + + synchronized int showIngestConfirmation() throws MissingResourceException, HeadlessException { + return JOptionPane.showConfirmDialog(mainFrame, + NbBundle.getMessage(TimeLineController.class, + "AdvTimeline.initTimeline.confDlg.genBeforeIngest.msg"), + NbBundle.getMessage(TimeLineController.class, + "AdvTimeline.initTimeline.confDlg.genBeforeIngest.details"), + JOptionPane.YES_NO_OPTION); + } + + private class AutopsyIngestModuleListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) { + case CONTENT_CHANGED: +// ((ModuleContentEvent)evt.getOldValue())???? + //ModuleContentEvent doesn't seem to provide any usefull information... + break; + case DATA_ADDED: +// Collection<BlackboardArtifact> artifacts = ((ModuleDataEvent) evt.getOldValue()).getArtifacts(); + //new artifacts, insert them into db + break; + case FILE_DONE: +// Long fileID = (Long) evt.getOldValue(); + //update file (known status) for file with id + Platform.runLater(() -> { + newEventsFlag.set(true); + }); + break; + } + } + } + + @Immutable + private class AutopsyIngestJobListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) { + case CANCELLED: + case COMPLETED: + //if we are doing incremental updates, drop this + outOfDatePromptAndRebuild(); + break; + } + } + } + + @Immutable + class AutopsyCaseListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + switch (Case.Events.valueOf(evt.getPropertyName())) { + case DATA_SOURCE_ADDED: +// Content content = (Content) evt.getNewValue(); + //if we are doing incremental updates, drop this + outOfDatePromptAndRebuild(); + break; + case CURRENT_CASE: + OpenTimelineAction.invalidateController(); + closeTimeLine(); + break; + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java new file mode 100644 index 0000000000000000000000000000000000000000..82aa4df51f2908dc3f9b578d3da93db5d7f4d01d --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineException.java @@ -0,0 +1,33 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +/** + * + */ +public class TimeLineException extends Exception { + + public TimeLineException(String string, Exception e) { + super(string, e); + } + + public TimeLineException(String string) { + super(string); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form similarity index 75% rename from Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form rename to Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form index 545399bf0dfbce86954fd2126b71d91d1faef608..1f0f367115b175ab2204726be65ee489e31a3880 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimelineFrame.form +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form @@ -1,19 +1,6 @@ <?xml version="1.0" encoding="UTF-8" ?> -<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JFrameFormInfo"> - <Properties> - <Property name="defaultCloseOperation" type="int" value="2"/> - <Property name="title" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="org/sleuthkit/autopsy/timeline/Bundle.properties" key="TimelineFrame.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> - <Dimension value="[1200, 700]"/> - </Property> - </Properties> - <SyntheticProperties> - <SyntheticProperty name="formSizePolicy" type="int" value="1"/> - <SyntheticProperty name="generateCenter" type="boolean" value="false"/> - </SyntheticProperties> +<Form version="1.5" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> <AuxValues> <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> @@ -29,21 +16,37 @@ <Layout> <DimensionLayout dim="0"> <Group type="103" groupAlignment="0" attributes="0"> - <Component id="splitYPane" alignment="1" max="32767" attributes="0"/> + <Component id="splitYPane" alignment="0" pref="972" max="32767" attributes="0"/> + <Component id="jFXstatusPanel" alignment="0" max="32767" attributes="0"/> </Group> </DimensionLayout> <DimensionLayout dim="1"> <Group type="103" groupAlignment="0" attributes="0"> - <Component id="splitYPane" alignment="1" max="32767" attributes="0"/> + <Group type="102" alignment="0" attributes="0"> + <Component id="splitYPane" pref="482" max="32767" attributes="0"/> + <EmptySpace min="0" pref="0" max="-2" attributes="0"/> + <Component id="jFXstatusPanel" min="-2" pref="29" max="-2" attributes="0"/> + </Group> </Group> </DimensionLayout> </Layout> <SubComponents> + <Container class="javafx.embed.swing.JFXPanel" name="jFXstatusPanel"> + <Properties> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 16]"/> + </Property> + </Properties> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout"> + <Property name="useNullLayout" type="boolean" value="true"/> + </Layout> + </Container> <Container class="javax.swing.JSplitPane" name="splitYPane"> <Properties> <Property name="dividerLocation" type="int" value="420"/> <Property name="orientation" type="int" value="0"/> - <Property name="resizeWeight" type="double" value="0.5"/> + <Property name="resizeWeight" type="double" value="0.9"/> <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[1024, 400]"/> </Property> @@ -51,21 +54,18 @@ <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/> <SubComponents> - <Container class="javax.swing.JPanel" name="topPane"> - <Properties> - <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> - <Dimension value="[1200, 400]"/> - </Property> - </Properties> + <Container class="javafx.embed.swing.JFXPanel" name="jFXVizPanel"> <Constraints> <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout" value="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout$JSplitPaneConstraintsDescription"> <JSplitPaneConstraints position="left"/> </Constraint> </Constraints> - <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout"> + <Property name="useNullLayout" type="boolean" value="true"/> + </Layout> </Container> - <Container class="javax.swing.JSplitPane" name="splitXPane"> + <Container class="javax.swing.JSplitPane" name="lowerSplitXPane"> <Properties> <Property name="dividerLocation" type="int" value="600"/> <Property name="resizeWeight" type="double" value="0.5"/> @@ -82,7 +82,7 @@ <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/> <SubComponents> - <Container class="javax.swing.JPanel" name="leftPane"> + <Container class="javax.swing.JPanel" name="resultContainerPanel"> <Properties> <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[700, 300]"/> @@ -96,7 +96,7 @@ <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> </Container> - <Container class="javax.swing.JPanel" name="rightPane"> + <Container class="javax.swing.JPanel" name="contentViewerContainerPanel"> <Properties> <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[500, 300]"/> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..f326576862bc05ab783a8c5a490b751fc8b7e5e6 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -0,0 +1,287 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +import java.awt.BorderLayout; +import java.util.Collections; +import java.util.List; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.event.ActionEvent; +import javafx.scene.Scene; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.netbeans.api.settings.ConvertAsProperties; +import org.openide.explorer.ExplorerManager; +import org.openide.explorer.ExplorerUtils; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.Mode; +import org.openide.windows.TopComponent; +import static org.openide.windows.TopComponent.PROP_UNDOCKING_DISABLED; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.corecomponents.DataContentPanel; +import org.sleuthkit.autopsy.corecomponents.DataResultPanel; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.actions.Back; +import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.ui.StatusBar; +import org.sleuthkit.autopsy.timeline.ui.TimeLineResultView; +import org.sleuthkit.autopsy.timeline.ui.TimeZonePanel; +import org.sleuthkit.autopsy.timeline.ui.VisualizationPanel; +import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavPanel; +import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane; + +/** + * TopComponent for the advanced timeline module. + */ +@ConvertAsProperties( + dtd = "-//org.sleuthkit.autopsy.timeline//TimeLine//EN", + autostore = false) +@TopComponent.Description( + preferredID = "TimeLineTopComponent", + //iconBase="SET/PATH/TO/ICON/HERE", + persistenceType = TopComponent.PERSISTENCE_NEVER) +@TopComponent.Registration(mode = "timeline", openAtStartup = false) +@Messages({ + "CTL_TimeLineTopComponentAction=TimeLineTopComponent", + "CTL_TimeLineTopComponent=Timeline Window", + "HINT_TimeLineTopComponent=This is a Timeline window" +}) +public final class TimeLineTopComponent extends TopComponent implements ExplorerManager.Provider, TimeLineUI { + + private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); + + private DataContentPanel dataContentPanel; + + private TimeLineResultView tlrv; + + private final ExplorerManager em = new ExplorerManager(); + + private TimeLineController controller; + + ////jfx componenets that make up the interface + private final FilterSetPanel filtersPanel = new FilterSetPanel(); + + private final Tab eventsTab = new Tab("Events"); + + private final Tab filterTab = new Tab("Filters"); + + private final VBox leftVBox = new VBox(5); + + private final NavPanel navPanel = new NavPanel(); + + private final StatusBar statusBar = new StatusBar(); + + private final TabPane tabPane = new TabPane(); + + private final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(); + + private final VisualizationPanel visualizationPanel = new VisualizationPanel(navPanel); + + private final SplitPane splitPane = new SplitPane(); + + private final TimeZonePanel timeZonePanel = new TimeZonePanel(); + + public TimeLineTopComponent() { + initComponents(); + + associateLookup(ExplorerUtils.createLookup(em, getActionMap())); + + setName(Bundle.CTL_TimeLineTopComponent()); + setToolTipText(Bundle.HINT_TimeLineTopComponent()); + setIcon(WindowManager.getDefault().getMainWindow().getIconImage()); //use the same icon as main application + + customizeComponents(); + } + + synchronized private void customizeComponents() { + + dataContentPanel = DataContentPanel.createInstance(); + this.contentViewerContainerPanel.add(dataContentPanel, BorderLayout.CENTER); + tlrv = new TimeLineResultView(dataContentPanel); + DataResultPanel dataResultPanel = tlrv.getDataResultPanel(); + this.resultContainerPanel.add(dataResultPanel, BorderLayout.CENTER); + dataResultPanel.open(); + + Platform.runLater(() -> { + //assemble ui componenets together + jFXstatusPanel.setScene(new Scene(statusBar)); + + splitPane.setDividerPositions(0); + jFXVizPanel.setScene(new Scene(splitPane)); + + filterTab.setClosable(false); + filterTab.setContent(filtersPanel); + filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png")); + + eventsTab.setClosable(false); + eventsTab.setContent(navPanel); + eventsTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png")); + + tabPane.getTabs().addAll(filterTab, eventsTab); + VBox.setVgrow(tabPane, Priority.ALWAYS); + + VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); + leftVBox.getChildren().addAll(timeZonePanel, zoomSettingsPane, tabPane); + + SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); + splitPane.getItems().addAll(leftVBox, visualizationPanel); + + }); + } + + public synchronized void setController(TimeLineController controller) { + this.controller = controller; + + tlrv.setController(controller); + Platform.runLater(() -> { + jFXVizPanel.getScene().addEventFilter(KeyEvent.KEY_PRESSED, ( + KeyEvent event) -> { + if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(event)) { + new Back(controller).handle(new ActionEvent()); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(event)) { + new Back(controller).handle(new ActionEvent()); + } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(event)) { + new Forward(controller).handle(new ActionEvent()); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(event)) { + new Forward(controller).handle(new ActionEvent()); + } + }); + controller.getViewMode().addListener((Observable observable) -> { + if (controller.getViewMode().get().equals(VisualizationMode.COUNTS)) { + tabPane.getSelectionModel().select(filterTab); + } + }); + eventsTab.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + visualizationPanel.setController(controller); + navPanel.setController(controller); + filtersPanel.setController(controller); + zoomSettingsPane.setController(controller); + statusBar.setController(controller); + }); + } + + @Override + public List<Mode> availableModes(List<Mode> modes) { + return Collections.emptyList(); + } + + /** + * 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. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + jFXstatusPanel = new javafx.embed.swing.JFXPanel(); + splitYPane = new javax.swing.JSplitPane(); + jFXVizPanel = new javafx.embed.swing.JFXPanel(); + lowerSplitXPane = new javax.swing.JSplitPane(); + resultContainerPanel = new javax.swing.JPanel(); + contentViewerContainerPanel = new javax.swing.JPanel(); + + jFXstatusPanel.setPreferredSize(new java.awt.Dimension(100, 16)); + + splitYPane.setDividerLocation(420); + splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); + splitYPane.setResizeWeight(0.9); + splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400)); + splitYPane.setLeftComponent(jFXVizPanel); + + lowerSplitXPane.setDividerLocation(600); + lowerSplitXPane.setResizeWeight(0.5); + lowerSplitXPane.setPreferredSize(new java.awt.Dimension(1200, 300)); + lowerSplitXPane.setRequestFocusEnabled(false); + + resultContainerPanel.setPreferredSize(new java.awt.Dimension(700, 300)); + resultContainerPanel.setLayout(new java.awt.BorderLayout()); + lowerSplitXPane.setLeftComponent(resultContainerPanel); + + contentViewerContainerPanel.setPreferredSize(new java.awt.Dimension(500, 300)); + contentViewerContainerPanel.setLayout(new java.awt.BorderLayout()); + lowerSplitXPane.setRightComponent(contentViewerContainerPanel); + + splitYPane.setRightComponent(lowerSplitXPane); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 972, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(jFXstatusPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(0, 0, 0)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(splitYPane, javax.swing.GroupLayout.DEFAULT_SIZE, 482, Short.MAX_VALUE) + .addGap(0, 0, 0) + .addComponent(jFXstatusPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)) + ); + }// </editor-fold>//GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel contentViewerContainerPanel; + private javafx.embed.swing.JFXPanel jFXVizPanel; + private javafx.embed.swing.JFXPanel jFXstatusPanel; + private javax.swing.JSplitPane lowerSplitXPane; + private javax.swing.JPanel resultContainerPanel; + private javax.swing.JSplitPane splitYPane; + // End of variables declaration//GEN-END:variables + + @Override + public void componentOpened() { + WindowManager.getDefault().setTopComponentFloating(this, true); + + putClientProperty(PROP_UNDOCKING_DISABLED, true); + } + + @Override + public void componentClosed() { + // TODO add custom code on component closing + } + + void writeProperties(java.util.Properties p) { + // better to version settings since initial version as advocated at + // http://wiki.apidesign.org/wiki/PropertyFiles + p.setProperty("version", "1.0"); + // TODO store your settings + } + + void readProperties(java.util.Properties p) { + String version = p.getProperty("version"); + // TODO read your settings according to their version + } + + @Override + public ExplorerManager getExplorerManager() { + return em; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java new file mode 100644 index 0000000000000000000000000000000000000000..bb14f76aeee6a38639bb8891b4281202dcafcff8 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineUI.java @@ -0,0 +1,27 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +/** + * + */ +public interface TimeLineUI { + + void setController(TimeLineController controller); +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java new file mode 100644 index 0000000000000000000000000000000000000000..a29058ac6fa17a9251ce94aef9577103d145c121 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/TimeLineView.java @@ -0,0 +1,35 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +/** Interface to be implemented by views of the data. + * + * Most implementations should install the relevant listeners in their + * {@link #setController} and {@link #setModel} methods */ +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; + +public interface TimeLineView extends TimeLineUI { + + @Override + void setController(TimeLineController controller); + + void setModel(final FilteredEventsModel filteredEvents); + + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java new file mode 100644 index 0000000000000000000000000000000000000000..8990b792bce6ac1ad39174bdfea97f37b19a8ecb --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java @@ -0,0 +1,27 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline; + +/** + * + */ +public enum VisualizationMode { + + COUNTS, DETAIL; +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java new file mode 100644 index 0000000000000000000000000000000000000000..b25c79b55cb283b9e9a718700ab907f189f7c33b --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Back.java @@ -0,0 +1,50 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import javafx.event.ActionEvent; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * + */ +public class Back extends AbstractAction { + + private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/timeline/images/arrow-180.png", 16, 16, true, true, true); + + private final TimeLineController controller; + + public Back(TimeLineController controller) { + super("Back"); + setGraphic(new ImageView(BACK_IMAGE)); + setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN)); + this.controller = controller; + disabledProperty().bind(controller.getHistoryStack().sizeProperty().isEqualTo(0)); + } + + @Override + public void handle(ActionEvent ae) { + controller.goBack(); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java new file mode 100644 index 0000000000000000000000000000000000000000..f9182bc90aee9e868001a2a4726f8c2157f4f4ba --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/DefaultFilters.java @@ -0,0 +1,57 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import javafx.beans.binding.BooleanBinding; +import javafx.event.ActionEvent; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.filters.Filter; + +/** + * + */ +public class DefaultFilters extends AbstractAction { + + private final TimeLineController controller; + + private FilteredEventsModel eventsModel; + + public DefaultFilters(final TimeLineController controller) { + super("apply default filters"); + this.controller = controller; + eventsModel = controller.getEventsModel(); + disabledProperty().bind(new BooleanBinding() { + { + bind(eventsModel.getRequestedZoomParamters()); + } + + @Override + protected boolean computeValue() { + return eventsModel.getRequestedZoomParamters().getValue().getFilter().equals(Filter.getDefaultFilter()); + } + }); + } + + @Override + public void handle(ActionEvent event) { + controller.applyDefaultFilters(); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java new file mode 100644 index 0000000000000000000000000000000000000000..d68a3e4734b1663447e14c6c8836fda7cfef57ea --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/Forward.java @@ -0,0 +1,50 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import javafx.event.ActionEvent; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * + */ +public class Forward extends AbstractAction { + + private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/timeline/images/arrow.png", 16, 16, true, true, true); + + private final TimeLineController controller; + + public Forward(TimeLineController controller) { + super("Forward"); + setGraphic(new ImageView(BACK_IMAGE)); + setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN)); + this.controller = controller; + disabledProperty().bind(controller.getForwardStack().sizeProperty().isEqualTo(0)); + } + + @Override + public void handle(ActionEvent ae) { + controller.goForward(); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java new file mode 100644 index 0000000000000000000000000000000000000000..0be925b4eb32048dca126a97cb53026cebe9958e --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java @@ -0,0 +1,134 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javafx.embed.swing.SwingFXUtils; +import javafx.event.ActionEvent; +import javafx.scene.image.WritableImage; +import javafx.stage.DirectoryChooser; +import javafx.util.Pair; +import javax.imageio.ImageIO; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.TskCoreException; + +/** + */ +public class SaveSnapshot extends AbstractAction { + + private static final String HTML_EXT = ".html"; + + private static final String REPORT_IMAGE_EXTENSION = ".png"; + + private static final Logger LOGGER = Logger.getLogger(SaveSnapshot.class.getName()); + + private final TimeLineController controller; + + private final WritableImage snapshot; + + public SaveSnapshot(TimeLineController controller, WritableImage snapshot) { + super("save snapshot"); + this.controller = controller; + this.snapshot = snapshot; + } + + @Override + public void handle(ActionEvent event) { + //choose location/name + DirectoryChooser fileChooser = new DirectoryChooser(); + fileChooser.setTitle("Save snapshot to"); + fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Reports")); + File outFolder = fileChooser.showDialog(null); + if (outFolder == null) { + return; + } + outFolder.mkdir(); + String name = outFolder.getName(); + + //gather metadata + List<Pair<String, String>> reportMetaData = new ArrayList<>(); + + reportMetaData.add(new Pair<>("Case", Case.getCurrentCase().getName())); + + ZoomParams get = controller.getEventsModel().getRequestedZoomParamters().get(); + reportMetaData.add(new Pair<>("Time Range", get.getTimeRange().toString())); + reportMetaData.add(new Pair<>("Description Level of Detail", get.getDescrLOD().getDisplayName())); + reportMetaData.add(new Pair<>("Event Type Zoom Level", get.getTypeZoomLevel().getDisplayName())); + reportMetaData.add(new Pair<>("Filters", get.getFilter().getHTMLReportString())); + + //save snapshot as png + try { + ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", new File(outFolder.getPath() + File.separator + outFolder.getName() + REPORT_IMAGE_EXTENSION)); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "failed to write snapshot to disk", ex); + return; + } + + //build html string + StringBuilder wrapper = new StringBuilder(); + wrapper.append("<html>\n<head>\n\t<title>").append("timeline snapshot").append("</title>\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"index.css\" />\n</head>\n<body>\n"); + wrapper.append("<div id=\"content\">\n<h1>").append(outFolder.getName()).append("</h1>\n"); + wrapper.append("<img src = \"").append(outFolder.getName()).append(REPORT_IMAGE_EXTENSION + "\" alt = \"snaphot\">"); + wrapper.append("<table>\n"); + for (Pair<String, String> pair : reportMetaData) { + wrapper.append("<tr><td>").append(pair.getKey()).append(": </td><td>").append(pair.getValue()).append("</td></tr>\n"); + } + wrapper.append("</table>\n"); + wrapper.append("</div>\n</body>\n</html>"); + + //write html wrapper + try (Writer htmlWriter = new FileWriter(new File(outFolder, name + HTML_EXT))) { + htmlWriter.write(wrapper.toString()); + } catch (FileNotFoundException ex) { + LOGGER.log(Level.WARNING, "failed to open html wrapper file for writing ", ex); + return; + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "failed to write html wrapper file", ex); + return; + } + + //copy css + try (InputStream resource = this.getClass().getResourceAsStream("/org/sleuthkit/autopsy/timeline/index.css")) { + Files.copy(resource, Paths.get(outFolder.getPath(), "index.css")); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "failed to copy css file", ex); + } + + //add html file as report to case + try { + Case.getCurrentCase().addReport(outFolder.getPath() + File.separator + outFolder.getName() + HTML_EXT, "Timeline", outFolder.getName() + HTML_EXT); + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "failed add html wrapper as a report", ex); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..85f7e5552d3118e2c42225372c74c097690d3271 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/SwingMenuItemAdapter.java @@ -0,0 +1,91 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import javafx.event.ActionEvent; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.MenuElement; +import javax.swing.SwingUtilities; + +public class SwingMenuItemAdapter extends MenuItem { + + JMenuItem jMenuItem; + + SwingMenuItemAdapter(final JMenuItem jMenuItem) { + super(jMenuItem.getText()); + this.jMenuItem = jMenuItem; + setOnAction((ActionEvent t) -> { + SwingUtilities.invokeLater(() -> { + jMenuItem.doClick(); + }); + + }); + } + + public static MenuItem create(MenuElement jmenuItem) { + if (jmenuItem instanceof JMenu) { + return new SwingMenuAdapter((JMenu) jmenuItem); + } else if (jmenuItem instanceof JPopupMenu) { + return new SwingMenuAdapter((JPopupMenu) jmenuItem); + } else { + return new SwingMenuItemAdapter((JMenuItem) jmenuItem); + } + + } +} + +class SwingMenuAdapter extends Menu { + + private final MenuElement jMenu; + + SwingMenuAdapter(final JMenu jMenu) { + super(jMenu.getText()); + this.jMenu = jMenu; + buildChildren(jMenu); + + } + + SwingMenuAdapter(JPopupMenu jPopupMenu) { + super(jPopupMenu.getLabel()); + this.jMenu = jPopupMenu; + + buildChildren(jMenu); + } + + private void buildChildren(MenuElement jMenu) { + + for (MenuElement menuE : jMenu.getSubElements()) { + if (menuE instanceof JMenu) { + getItems().add(SwingMenuItemAdapter.create((JMenu) menuE)); + } else if (menuE instanceof JMenuItem) { + getItems().add(SwingMenuItemAdapter.create((JMenuItem) menuE)); + } else if (menuE instanceof JPopupMenu) { + buildChildren(menuE); + } else { + + System.out.println(menuE.toString()); +// throw new UnsupportedOperationException(); + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java new file mode 100644 index 0000000000000000000000000000000000000000..3925efc9f3a858e73c6f59c919fc8c5bc5fea05d --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/actions/ZoomOut.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.actions; + +import javafx.beans.binding.BooleanBinding; +import javafx.event.ActionEvent; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; + +/** + * + */ +public class ZoomOut extends AbstractAction { + + private final TimeLineController controller; + + private final FilteredEventsModel eventsModel; + + public ZoomOut(final TimeLineController controller) { + super("apply default filters"); + this.controller = controller; + eventsModel = controller.getEventsModel(); + disabledProperty().bind(new BooleanBinding() { + { + bind(eventsModel.getRequestedZoomParamters()); + } + + @Override + protected boolean computeValue() { + return eventsModel.getRequestedZoomParamters().getValue().getTimeRange().contains(eventsModel.getSpanningInterval()); + } + }); + } + + @Override + public void handle(ActionEvent event) { + controller.zoomOutToActivity(); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..c97008328d5762c39b32c408aaa646277ef5fb12 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/AggregateEvent.java @@ -0,0 +1,114 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events; + +import com.google.common.collect.Collections2; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.concurrent.Immutable; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; + +/** An event that represent a set of other events aggregated together. All the + * sub events should have the same type and matching descriptions at the + * designated 'zoom level'. + */ +@Immutable +public class AggregateEvent { + + final private Interval span; + + final private EventType type; + + final private Set<Long> eventIDs; + + final private String description; + + private final DescriptionLOD lod; + + public AggregateEvent(Interval spanningInterval, EventType type, Set<Long> eventIDs, String description, DescriptionLOD lod) { + + this.span = spanningInterval; + this.type = type; + this.description = description; + + this.eventIDs = eventIDs; + this.lod = lod; + } + + public AggregateEvent(Interval spanningInterval, EventType type, List<String> events, String description, DescriptionLOD lod) { + + this.span = spanningInterval; + this.type = type; + this.description = description; + + this.eventIDs = new HashSet<>(Collections2.transform(events, Long::valueOf)); + this.lod = lod; + } + + /** @return the actual interval from the first event to the last event */ + public Interval getSpan() { + return span; + } + + public Set<Long> getEventIDs() { + return Collections.unmodifiableSet(eventIDs); + } + + public String getDescription() { + return description; + } + + public EventType getType() { + return type; + } + + /** + * merge two aggregate events into one new aggregate event. + * + * @param ag1 + * @param ag2 + * + * @return + */ + public static AggregateEvent merge(AggregateEvent ag1, AggregateEvent ag2) { + + if (ag1.getType() != ag2.getType()) { + throw new IllegalArgumentException(NbBundle.getMessage(AggregateEvent.class, "AggregateEvent.differentTypes")); + } + + if (!ag1.getDescription().equals(ag2.getDescription())) { + throw new IllegalArgumentException(NbBundle.getMessage(AggregateEvent.class, "AggregateEvent.differentDescriptions")); + } + HashSet<Long> ids = new HashSet<>(ag1.getEventIDs()); + ids.addAll(ag2.getEventIDs()); + + //TODO: check that types/descriptions are actually the same -jm + return new AggregateEvent(IntervalUtils.span(ag1.span, ag2.span), ag1.getType(), ids, ag1.getDescription(), ag1.lod); + } + + DescriptionLOD getLOD() { + return lod; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java new file mode 100644 index 0000000000000000000000000000000000000000..6bd3481d42dee7672315f3ec6cfcc2a95e328c6a --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/FilteredEventsModel.java @@ -0,0 +1,250 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javax.annotation.concurrent.GuardedBy; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.events.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.filters.Filter; +import org.sleuthkit.autopsy.timeline.filters.IntersectionFilter; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; + +/** + * This class acts as the model for a {@link TimeLineView} + * + * Views can register listeners on properties returned by methods. + * + * This class is implemented as a filtered view into an underlying + * {@link EventsRepository}. + * + * TODO: as many methods as possible should cache their results so as to avoid + * unnecessary db calls through the {@link EventsRepository} -jm + * + * Concurrency Policy: repo is internally synchronized, so methods that only + * access the repo atomicaly do not need further synchronization + * + * all other member state variables should only be accessed with intrinsic lock + * of containing FilteredEventsModel held. Many methods delegate to a task + * submitted to the dbQueryThread executor. These methods should synchronize on + * this object, and the tasks should too. Since the tasks execute asynchronously + * from the invoking methods, the methods will return and release the lock for + * the tasks to obtain. + * + */ +public class FilteredEventsModel { + + /** + * time range that spans the filtered events + */ + //requested time range, filter, event_type zoom, and description level of detail. if specifics are not passed to methods, the values of these members are used to query repository. + @GuardedBy("this") + private final ReadOnlyObjectWrapper<Interval> requestedTimeRange = new ReadOnlyObjectWrapper<>(); + + @GuardedBy("this") + private final ReadOnlyObjectWrapper<Filter> requestedFilter = new ReadOnlyObjectWrapper<>(Filter.getDefaultFilter()); + + @GuardedBy("this") + private final ReadOnlyObjectWrapper< EventTypeZoomLevel> requestedTypeZoom = new ReadOnlyObjectWrapper<>(EventTypeZoomLevel.BASE_TYPE); + + @GuardedBy("this") + private final ReadOnlyObjectWrapper< DescriptionLOD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLOD.SHORT); + + @GuardedBy("this") + private final ReadOnlyObjectWrapper<ZoomParams> requestedZoomParamters = new ReadOnlyObjectWrapper<>(); + + /** + * The underlying repo for events. Atomic access to repo is synchronized + * internally, but compound access should be done with the intrinsic lock of + * this FilteredEventsModel object + */ + @GuardedBy("this") + private final EventsRepository repo; + + public FilteredEventsModel(EventsRepository repo) { + this.repo = repo; + requestedZoomParamters.set(new ZoomParams(new Interval(repo.getMinTime(), repo.getMaxTime(), DateTimeZone.UTC), + EventTypeZoomLevel.BASE_TYPE, + Filter.getDefaultFilter(), + DescriptionLOD.SHORT, + EnumSet.noneOf(ZoomParams.Field.class))); + this.requestedTimeRange.set(getSpanningInterval()); + } + + public Interval getBoundingEventsInterval() { + return repo.getBoundingEventsInterval(getRequestedZoomParamters().get().getTimeRange(), getRequestedZoomParamters().get().getFilter()); + } + + synchronized public ReadOnlyObjectProperty<ZoomParams> getRequestedZoomParamters() { + return requestedZoomParamters.getReadOnlyProperty(); + } + + public TimeLineEvent getEventById(Long eventID) { + return repo.getEventById(eventID); + } + + public Set<Long> getEventIDs(Interval timeRange, Filter filter) { + final Interval overlap; + final IntersectionFilter intersect; + synchronized (this) { + overlap = getSpanningInterval().overlap(timeRange); + intersect = Filter.intersect(new Filter[]{filter, requestedFilter.get()}); + } + return repo.getEventIDs(overlap, intersect); + } + + /** + * return the number of events that pass the requested filter and are within + * the given time range. + * + * NOTE: this method does not change the requested time range + * + * @param timeRange + * + * @return + */ + public Map<EventType, Long> getEventCounts(Interval timeRange) { + + final Filter filter; + final EventTypeZoomLevel typeZoom; + synchronized (this) { + filter = requestedFilter.get(); + typeZoom = requestedTypeZoom.get(); + } + return repo.countEvents(new ZoomParams(timeRange, typeZoom, filter, null)); + } + + /** + * @return a read only view of the time range requested via + * {@link #requestTimeRange(org.joda.time.Interval)} + */ + synchronized public ReadOnlyObjectProperty<Interval> timeRange() { + if (requestedTimeRange.get() == null) { + requestedTimeRange.set(getSpanningInterval()); + } + return requestedTimeRange.getReadOnlyProperty(); + } + + synchronized public ReadOnlyObjectProperty<DescriptionLOD> descriptionLOD() { + return requestedLOD.getReadOnlyProperty(); + } + + synchronized public ReadOnlyObjectProperty<Filter> filter() { + return requestedFilter.getReadOnlyProperty(); + } + + /** + * @return the smallest interval spanning all the events from the + * repository, ignoring any filters or requested ranges + */ + public final Interval getSpanningInterval() { + return new Interval(getMinTime() * 1000, 1000 + getMaxTime() * 1000, DateTimeZone.UTC); + } + + /** + * @return the smallest interval spanning all the given events + */ + public Interval getSpanningInterval(Collection<Long> eventIDs) { + return repo.getSpanningInterval(eventIDs); + } + + /** + * @return the time (in seconds from unix epoch) of the absolutely first + * event available from the repository, ignoring any filters or requested + * ranges + */ + public final Long getMinTime() { + return repo.getMinTime(); + } + + /** + * @return the time (in seconds from unix epoch) of the absolutely last + * event available from the repository, ignoring any filters or requested + * ranges + */ + public final Long getMaxTime() { + return repo.getMaxTime(); + } + + /** + * @param aggregation + * + * @return a list of aggregated events that are within the requested time + * range and pass the requested filter, using the given aggregation to + * control the grouping of events + */ + public List<AggregateEvent> getAggregatedEvents() { + final Interval range; + final Filter filter; + final EventTypeZoomLevel zoom; + final DescriptionLOD lod; + synchronized (this) { + range = requestedTimeRange.get(); + filter = requestedFilter.get(); + zoom = requestedTypeZoom.get(); + lod = requestedLOD.get(); + } + return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod)); + } + + /** + * @param aggregation + * + * @return a list of aggregated events that are within the requested time + * range and pass the requested filter, using the given aggregation to + * control the grouping of events + */ + public List<AggregateEvent> getAggregatedEvents(ZoomParams params) { + return repo.getAggregatedEvents(params); + } + + synchronized public ReadOnlyObjectProperty<EventTypeZoomLevel> eventTypeZoom() { + return requestedTypeZoom.getReadOnlyProperty(); + } + + synchronized public EventTypeZoomLevel getEventTypeZoom() { + return requestedTypeZoom.get(); + } + + synchronized public void requestZoomState(ZoomParams zCrumb, boolean force) { + if (force + || zCrumb.getTypeZoomLevel().equals(requestedTypeZoom.get()) == false + || zCrumb.getDescrLOD().equals(requestedLOD.get()) == false + || zCrumb.getFilter().equals(requestedFilter.get()) == false + || zCrumb.getTimeRange().equals(requestedTimeRange.get()) == false) { + + requestedZoomParamters.set(zCrumb); + requestedTypeZoom.set(zCrumb.getTypeZoomLevel()); + requestedFilter.set(zCrumb.getFilter().copyOf()); + requestedTimeRange.set(zCrumb.getTimeRange()); + requestedLOD.set(zCrumb.getDescrLOD()); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..b1340db2347f780a3b98d93c9fd110a9b03d19c6 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/TimeLineEvent.java @@ -0,0 +1,91 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events; + +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.datamodel.TskData; + +/** + * + */ +public class TimeLineEvent { + + private final Long eventID; + + private final Long fileID, time; + + private final Long artifactID; + + private final EventType subType; + + private final String fullDescription, medDescription, shortDescription; + + private final TskData.FileKnown known; + + public Long getArtifactID() { + return artifactID; + } + + public Long getEventID() { + return eventID; + } + + public Long getFileID() { + return fileID; + } + + /** @return the time in seconds from unix epoch */ + public Long getTime() { + return time; + } + + public EventType getType() { + return subType; + } + + public String getFullDescription() { + return fullDescription; + } + + public String getMedDescription() { + return medDescription; + } + + public String getShortDescription() { + return shortDescription; + } + + public TimeLineEvent(Long eventID, Long objID, Long artifactID, Long time, + EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known) { + this.eventID = eventID; + this.fileID = objID; + this.artifactID = artifactID; + this.time = time; + this.subType = type; + + this.fullDescription = fullDescription; + this.medDescription = medDescription; + this.shortDescription = shortDescription; + this.known = known; + } + + public TskData.FileKnown getKnown() { + return known; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java new file mode 100644 index 0000000000000000000000000000000000000000..979317bee41c97eac43434c5a5d790d50e012df7 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventDB.java @@ -0,0 +1,1122 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.db; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.SetMultimap; +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import org.sleuthkit.autopsy.timeline.events.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.events.type.BaseTypes; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.events.type.RootEventType; +import org.sleuthkit.autopsy.timeline.filters.Filter; +import org.sleuthkit.autopsy.timeline.filters.HideKnownFilter; +import org.sleuthkit.autopsy.timeline.filters.IntersectionFilter; +import org.sleuthkit.autopsy.timeline.filters.TextFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.filters.UnionFilter; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.TimeUnits; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.TskData; +import org.sqlite.SQLiteJDBCLoader; + +/** + * This class provides access to the Advanced Timeline SQLite database. This + * class borrows a lot of ideas and techniques from {@link SleuthkitCase}, + * Creating an abstract base class for sqlite databases, or using a higherlevel + * persistence api may make sense in the future. + */ +public class EventDB { + + private static final String ARTIFACT_ID_COLUMN = "artifact_id"; + + private static final String BASE_TYPE_COLUMN = "base_type"; + + private static final String EVENT_ID_COLUMN = "event_id"; + + //column name constants////////////////////// + private static final String FILE_ID_COLUMN = "file_id"; + + private static final String FULL_DESCRIPTION_COLUMN = "full_description"; + + private static final String KNOWN_COLUMN = "known_state"; + + private static final String LAST_ARTIFACT_ID_KEY = "last_artifact_id"; + + private static final String LAST_OBJECT_ID_KEY = "last_object_id"; + + private static final java.util.logging.Logger LOGGER = Logger.getLogger(EventDB.class.getName()); + + private static final String MED_DESCRIPTION_COLUMN = "med_description"; + + private static final String SHORT_DESCRIPTION_COLUMN = "short_description"; + + private static final String SUB_TYPE_COLUMN = "sub_type"; + + private static final String TIME_COLUMN = "time"; + + private static final String WAS_INGEST_RUNNING_KEY = "was_ingest_running"; + + static { + //make sure sqlite driver is loaded // possibly redundant + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException ex) { + LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); + } + } + + /** + * public factory method. Creates and opens a connection to a database at + * the given path. If a database does not already exist at that path, one is + * created. + * + * @param dbPath + * + * @return + */ + public static EventDB getEventDB(String dbPath) { + try { + EventDB eventDB = new EventDB(dbPath + File.separator + "events.db"); + + return eventDB; + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "sql error creating database connection", ex); + return null; + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "error creating database connection", ex); + return null; + } + } + + static List<Integer> getActiveSubTypes(TypeFilter filter) { + if (filter.isActive()) { + if (filter.getSubFilters().isEmpty()) { + return Collections.singletonList(RootEventType.allTypes.indexOf(filter.getEventType())); + } else { + return filter.getSubFilters().stream().flatMap((Filter t) -> getActiveSubTypes((TypeFilter) t).stream()).collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + + static String getSQLWhere(IntersectionFilter filter) { + return filter.getSubFilters().stream() + .filter(Filter::isActive) + .map(EventDB::getSQLWhere) + .collect(Collectors.joining(" and ", "( ", ")")); + } + + static String getSQLWhere(UnionFilter filter) { + return filter.getSubFilters().stream() + .filter(Filter::isActive) + .map(EventDB::getSQLWhere) + .collect(Collectors.joining(" or ", "( ", ")")); + } + + private static String getSQLWhere(Filter filter) { + //TODO: this is here so that the filters don't depend, even implicitly, on the db, but it leads to some nasty code + //it would all be much easier if all the getSQLWhere methods where moved to their respective filter classes + String result = ""; + if (filter == null) { + return "1"; + } else if (filter instanceof HideKnownFilter) { + result = getSQLWhere((HideKnownFilter) filter); + } else if (filter instanceof TextFilter) { + result = getSQLWhere((TextFilter) filter); + } else if (filter instanceof TypeFilter) { + result = getSQLWhere((TypeFilter) filter); + } else if (filter instanceof IntersectionFilter) { + result = getSQLWhere((IntersectionFilter) filter); + } else if (filter instanceof UnionFilter) { + result = getSQLWhere((UnionFilter) filter); + } else { + return "1"; + } + result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result; + System.out.println(result); + return result; + } + + private static String getSQLWhere(HideKnownFilter filter) { + return (filter.isActive()) + ? "(known_state is not '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')" + : "1"; + } + + private static String getSQLWhere(TextFilter filter) { + if (filter.isActive()) { + if (StringUtils.isBlank(filter.getText())) { + return "1"; + } + String strip = StringUtils.strip(filter.getText()); + return "((" + MED_DESCRIPTION_COLUMN + " like '%" + strip + "%') or (" + + FULL_DESCRIPTION_COLUMN + " like '%" + strip + "%') or (" + + SHORT_DESCRIPTION_COLUMN + " like '%" + strip + "%'))"; + } else { + return "1"; + } + } + + /** + * generate a sql where clause for the given type filter, while trying to be + * as simple as possible to improve performance. + * + * @param filter + * + * @return + */ + private static String getSQLWhere(TypeFilter filter) { + if (filter.isActive() == false) { + return "0"; + } else if (filter.getEventType() instanceof RootEventType) { + //if the filter is a root filter and all base type filtes and subtype filters are active, + if (filter.getSubFilters().stream().allMatch(f + -> f.isActive() && ((TypeFilter) f).getSubFilters().stream().allMatch(Filter::isActive))) { + return "1"; //then collapse clause to true + } + } + return "(" + SUB_TYPE_COLUMN + " in (" + StringUtils.join(getActiveSubTypes(filter), ",") + "))"; + } + + private volatile Connection con; + + private final String dbPath; + + private PreparedStatement getDBInfoStmt; + + private PreparedStatement getEventByIDStmt; + + private PreparedStatement getMaxTimeStmt; + + private PreparedStatement getMinTimeStmt; + + private PreparedStatement insertRowStmt; + + private final Set<PreparedStatement> preparedStatements = new HashSet<>(); + + private PreparedStatement recordDBInfoStmt; + + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy + + private final Lock DBLock = rwLock.writeLock(); //using exclusing lock for all db ops for now + + private EventDB(String dbPath) throws SQLException, Exception { + this.dbPath = dbPath; + initializeDB(); + } + + @Override + public void finalize() throws Throwable { + try { + closeDBCon(); + } finally { + super.finalize(); + } + } + + public Interval getSpanningInterval(Collection<Long> eventIDs) { + + Interval span = null; + dbReadLock(); + try (Statement stmt = con.createStatement(); + //You can't inject multiple values into one ? paramater in prepared statement, + //so we make new statement each time... + ResultSet rs = stmt.executeQuery("select Min(time), Max(time) from events where event_id in (" + StringUtils.join(eventIDs, ", ") + ")");) { + while (rs.next()) { + span = new Interval(rs.getLong("Min(time)"), rs.getLong("Max(time)") + 1, DateTimeZone.UTC); + + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex); + } finally { + dbReadUnlock(); + } + return span; + } + + EventTransaction beginTransaction() { + return new EventTransaction(); + } + + void closeDBCon() { + if (con != null) { + try { + closeStatements(); + con.close(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Failed to close connection to evetns.db", ex); + } + } + con = null; + } + + void commitTransaction(EventTransaction tr, Boolean notify) { + if (tr.isClosed()) { + throw new IllegalArgumentException("can't close already closed transaction"); + } + tr.commit(notify); + } + + int countAllEvents() { + int result = -1; + dbReadLock(); + //TODO convert this to prepared statement -jm + try (ResultSet rs = con.createStatement().executeQuery("select count(*) as count from events")) { + while (rs.next()) { + result = rs.getInt("count"); + break; + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + dbReadUnlock(); + } + return result; + } + + Map<EventType, Long> countEvents(ZoomParams params) { + if (params.getTimeRange() != null) { + return countEvents(params.getTimeRange().getStartMillis() / 1000, params.getTimeRange().getEndMillis() / 1000, params.getFilter(), params.getTypeZoomLevel()); + } else { + return Collections.emptyMap(); + } + } + + /** + * Lock to protect against read while it is in a write transaction state. + * Supports multiple concurrent readers if there is no writer. MUST always + * call dbReadUnLock() as early as possible, in the same thread where + * dbReadLock() was called. + */ + void dbReadLock() { + DBLock.lock(); + } + + /** + * Release previously acquired read lock acquired in this thread using + * dbReadLock(). Call in "finally" block to ensure the lock is always + * released. + */ + void dbReadUnlock() { + DBLock.unlock(); + } + + //////////////general database logic , mostly borrowed from sleuthkitcase + void dbWriteLock() { + //Logger.getLogger("LOCK").log(Level.INFO, "Locking " + rwLock.toString()); + DBLock.lock(); + } + + /** + * Release previously acquired write lock acquired in this thread using + * dbWriteLock(). Call in "finally" block to ensure the lock is always + * released. + */ + void dbWriteUnlock() { + //Logger.getLogger("LOCK").log(Level.INFO, "UNLocking " + rwLock.toString()); + DBLock.unlock(); + } + + void dropTable() { + //TODO: use prepared statement - jm + dbWriteLock(); + try (Statement createStatement = con.createStatement()) { + createStatement.execute("drop table if exists events"); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "could not drop old events table", ex); + } finally { + dbWriteUnlock(); + } + } + + List<AggregateEvent> getAggregatedEvents(ZoomParams params) { + return getAggregatedEvents(params.getTimeRange(), params.getFilter(), params.getTypeZoomLevel(), params.getDescrLOD()); + } + + Interval getBoundingEventsInterval(Interval timeRange, Filter filter) { + long start = timeRange.getStartMillis() / 1000; + long end = timeRange.getEndMillis() / 1000; + final String sqlWhere = getSQLWhere(filter); + + dbReadLock(); + try (Statement stmt = con.createStatement(); //can't use prepared statement because of complex where clause + ResultSet rs = stmt.executeQuery(" select (select Max(time) from events where time <=" + start + " and " + sqlWhere + ") as start,(select Min(time) from events where time >= " + end + " and " + sqlWhere + ") as end")) { + while (rs.next()) { + + long start2 = rs.getLong("start"); + long end2 = rs.getLong("end"); + + if (end2 == 0) { + end2 = getMaxTime(); + } + System.out.println(start2 + " " + start + " " + end + " " + end2); + return new Interval(start2 * 1000, (end2 + 1) * 1000, TimeLineController.getJodaTimeZone()); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex); + } finally { + dbReadUnlock(); + } + return null; + } + + TimeLineEvent getEventById(Long eventID) { + TimeLineEvent result = null; + dbReadLock(); + try { + getEventByIDStmt.clearParameters(); + getEventByIDStmt.setLong(1, eventID); + try (ResultSet rs = getEventByIDStmt.executeQuery()) { + while (rs.next()) { + result = constructTimeLineEvent(rs); + break; + } + } + } catch (SQLException sqlEx) { + LOGGER.log(Level.SEVERE, "exception while querying for event with id = " + eventID, sqlEx); + } finally { + dbReadUnlock(); + } + return result; + } + + Set<Long> getEventIDs(Interval timeRange, Filter filter) { + return getEventIDs(timeRange.getStartMillis() / 1000, timeRange.getEndMillis() / 1000, filter); + } + + Set<Long> getEventIDs(Long startTime, Long endTime, Filter filter) { + if (Objects.equals(startTime, endTime)) { + endTime++; + } + Set<Long> resultIDs = new HashSet<>(); + + dbReadLock(); + final String query = "select event_id from events where time >= " + startTime + " and time <" + endTime + " and " + getSQLWhere(filter); + System.out.println(query); + try (Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery(query)) { + + while (rs.next()) { + resultIDs.add(rs.getLong(EVENT_ID_COLUMN)); + } + + } catch (SQLException sqlEx) { + LOGGER.log(Level.SEVERE, "failed to execute query for event ids in range", sqlEx); + } finally { + dbReadUnlock(); + } + + return resultIDs; + } + + long getLastArtfactID() { + return getDBInfo(LAST_ARTIFACT_ID_KEY, -1); + } + + long getLastObjID() { + return getDBInfo(LAST_OBJECT_ID_KEY, -1); + } + + /** @return maximum time in seconds from unix epoch */ + Long getMaxTime() { + dbReadLock(); + try (ResultSet rs = getMaxTimeStmt.executeQuery()) { + while (rs.next()) { + return rs.getLong("max"); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); + } finally { + dbReadUnlock(); + } + return -1l; + } + + /** @return maximum time in seconds from unix epoch */ + Long getMinTime() { + dbReadLock(); + try (ResultSet rs = getMinTimeStmt.executeQuery()) { + while (rs.next()) { + return rs.getLong("min"); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex); + } finally { + dbReadUnlock(); + } + return -1l; + } + + boolean getWasIngestRunning() { + return getDBInfo(WAS_INGEST_RUNNING_KEY, 0) != 0; + } + + /** + * create the table and indices if they don't already exist + * + * + * @return the number of rows in the table , count > 0 indicating an + * existing table + */ + final synchronized void initializeDB() { + try { + if (isClosed()) { + openDBCon(); + } + configureDB(); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem accessing database", ex); + } + + dbWriteLock(); + try { + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists db_info " + + " ( key TEXT, " + + " value INTEGER, " + + "PRIMARY KEY (key))"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating db_info table", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists events " + + " (event_id INTEGER PRIMARY KEY, " + + " file_id INTEGER, " + + " artifact_id INTEGER, " + + " time INTEGER, " + + " sub_type INTEGER, " + + " base_type INTEGER, " + + " full_description TEXT, " + + " med_description TEXT, " + + " short_description TEXT, " + + " known_state INTEGER)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating database table", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists file_idx ON events(file_id)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating file_idx", ex); + } + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists artifact_idx ON events(artifact_id)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating artifact_idx", ex); + } + + //for common queries the covering indexes below were better, but having the time index 'blocke' them +// try (Statement stmt = con.createStatement()) { +// String sql = "CREATE INDEX if not exists time_idx ON events(time)"; +// stmt.execute(sql); +// } catch (SQLException ex) { +// LOGGER.log(Level.SEVERE, "problem creating time_idx", ex); +// } + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists sub_type_idx ON events(sub_type, time)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating sub_type_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists base_type_idx ON events(base_type, time)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating base_type_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists known_idx ON events(known_state)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating known_idx", ex); + } + + try { + insertRowStmt = prepareStatement( + "INSERT INTO events (file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + + getMaxTimeStmt = prepareStatement("select Max(time) as max from events"); + getMinTimeStmt = prepareStatement("select Min(time) as min from events"); + getEventByIDStmt = prepareStatement("select * from events where event_id = ?"); + recordDBInfoStmt = prepareStatement("insert or replace into db_info (key, value) values (?, ?)"); + getDBInfoStmt = prepareStatement("select value from db_info where key = ?"); + } catch (SQLException sQLException) { + LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); + } + + } finally { + dbWriteUnlock(); + } + + } + + void insertEvent(long time, EventType type, Long objID, Long artifactID, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known) { + EventTransaction trans = beginTransaction(); + insertEvent(time, type, objID, artifactID, fullDescription, medDescription, shortDescription, known, trans); + commitTransaction(trans, true); + } + + /** + * use transactions to update files + * + * @param f + * @param tr + */ + void insertEvent(long time, EventType type, Long objID, Long artifactID, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, EventTransaction tr) { + if (tr.isClosed()) { + throw new IllegalArgumentException("can't update database with closed transaction"); + } + int typeNum; + int superTypeNum; + + typeNum = RootEventType.allTypes.indexOf(type); + superTypeNum = type.getSuperType().ordinal(); + + dbWriteLock(); + try { + + //"INSERT INTO events (file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description) " + insertRowStmt.clearParameters(); + if (objID != null) { + insertRowStmt.setLong(1, objID); + } else { + insertRowStmt.setNull(1, Types.INTEGER); + } + if (artifactID != null) { + insertRowStmt.setLong(2, artifactID); + } else { + insertRowStmt.setNull(2, Types.INTEGER); + } + insertRowStmt.setLong(3, time); + + if (typeNum != -1) { + insertRowStmt.setInt(4, typeNum); + } else { + insertRowStmt.setNull(4, Types.INTEGER); + } + + insertRowStmt.setInt(5, superTypeNum); + insertRowStmt.setString(6, fullDescription); + insertRowStmt.setString(7, medDescription); + insertRowStmt.setString(8, shortDescription); + + insertRowStmt.setByte(9, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue()); + + insertRowStmt.executeUpdate(); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to insert event", ex); + } finally { + dbWriteUnlock(); + } + } + + boolean isClosed() throws SQLException { + if (con == null) { + return true; + } + return con.isClosed(); + } + + void openDBCon() { + try { + if (con == null || con.isClosed()) { + con = DriverManager.getConnection("jdbc:sqlite:" + dbPath); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Failed to open connection to events.db", ex); + } + } + + void recordLastArtifactID(long lastArtfID) { + recordDBInfo(LAST_ARTIFACT_ID_KEY, lastArtfID); + } + + void recordLastObjID(Long lastObjID) { + recordDBInfo(LAST_OBJECT_ID_KEY, lastObjID); + } + + void recordWasIngestRunning(boolean wasIngestRunning) { + recordDBInfo(WAS_INGEST_RUNNING_KEY, (wasIngestRunning ? 1 : 0)); + } + + void rollBackTransaction(EventTransaction trans) { + trans.rollback(); + } + + boolean tableExists() { + //TODO: use prepared statement - jm + try (Statement createStatement = con.createStatement(); + ResultSet executeQuery = createStatement.executeQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='events'")) { + if (executeQuery.getString("name").equals("events") == false) { + return false; + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } + return true; + } + + private void closeStatements() throws SQLException { + for (PreparedStatement pStmt : preparedStatements) { + pStmt.close(); + } + } + + private void configureDB() throws SQLException { + dbWriteLock(); + //this should match Sleuthkit db setupt + try (Statement statement = con.createStatement()) { + //reduce i/o operations, we have no OS crash recovery anyway + statement.execute("PRAGMA synchronous = OFF;"); + //we don't use this feature, so turn it off for minimal speed up on queries + //this is deprecated and not recomended + statement.execute("PRAGMA count_changes = OFF;"); + //this made a big difference to query speed + statement.execute("PRAGMA temp_store = MEMORY"); + //this made a modest improvement in query speeds + statement.execute("PRAGMA cache_size = 50000"); + //we never delete anything so... + statement.execute("PRAGMA auto_vacuum = 0"); + //allow to query while in transaction - no need read locks + statement.execute("PRAGMA read_uncommitted = True;"); + } finally { + dbWriteUnlock(); + } + + try { + LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", + SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode() + ? "native" : "pure-java")); + } catch (Exception exception) { + } + } + + private TimeLineEvent constructTimeLineEvent(ResultSet rs) throws SQLException { + EventType type = RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN)); + return new TimeLineEvent(rs.getLong(EVENT_ID_COLUMN), + rs.getLong(FILE_ID_COLUMN), + rs.getLong(ARTIFACT_ID_COLUMN), + rs.getLong(TIME_COLUMN), + type, + rs.getString(FULL_DESCRIPTION_COLUMN), + rs.getString(MED_DESCRIPTION_COLUMN), + rs.getString(SHORT_DESCRIPTION_COLUMN), + TskData.FileKnown.valueOf(rs.getByte(KNOWN_COLUMN))); + } + + /** + * count all the events with the given options and return a map organizing + * the counts in a hierarchy from date > eventtype> count + * + * + * @param startTime events before this time will be excluded (seconds from + * unix epoch) + * @param endTime events at or after this time will be excluded (seconds + * from unix epoch) + * @param filter only events that pass this filter will be counted + * @param zoomLevel only events of this type or a subtype will be counted + * and the counts will be organized into bins for each of the subtypes of + * the given event type + * + * @return a map organizing the counts in a hierarchy from date > eventtype> + * count + */ + private Map<EventType, Long> countEvents(Long startTime, Long endTime, Filter filter, EventTypeZoomLevel zoomLevel) { + if (Objects.equals(startTime, endTime)) { + endTime++; + } + + Map<EventType, Long> typeMap = new HashMap<>(); + + //do we want the root or subtype column of the databse + final boolean useSubTypes = (zoomLevel == EventTypeZoomLevel.SUB_TYPE); + + //get some info about the range of dates requested + final String queryString = "select count(*), " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN) + + " from events where time >= " + startTime + " and time < " + endTime + " and " + getSQLWhere(filter) + + " GROUP BY " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN); + + ResultSet rs = null; + dbReadLock(); + System.out.println(queryString); + try (Statement stmt = con.createStatement();) { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.start(); + rs = stmt.executeQuery(queryString); + stopwatch.stop(); + System.out.println(stopwatch.elapsedMillis() / 1000.0 + " seconds"); + while (rs.next()) { + + EventType type = useSubTypes + ? RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN)) + : BaseTypes.values()[rs.getInt(BASE_TYPE_COLUMN)]; + + typeMap.put(type, rs.getLong("count(*)")); + } + + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "error getting count of events from db.", ex); + } finally { + try { + rs.close(); + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } + dbReadUnlock(); + } + return typeMap; + } + + /** + * //TODO: update javadoc //TODO: split this into helper methods + * + * get a list of {@link AggregateEvent}s. + * + * General algorithm is as follows: + * + * - get all aggregate events, via one db query. + * - sort them into a map from (type, description)-> aggevent + * - for each key in map, merge the events and accumulate them in a list + * to return + * + * + * @param timeRange the Interval within in which all returned aggregate + * events will be. + * @param filter only events that pass the filter will be included in + * aggregates events returned + * @param zoomLevel only events of this level will be included + * @param lod description level of detail to use when grouping events + * + * + * @return a list of aggregate events within the given timerange, that pass + * the supplied filter, aggregated according to the given event type and + * description zoom levels + */ + private List<AggregateEvent> getAggregatedEvents(Interval timeRange, Filter filter, EventTypeZoomLevel zoomLevel, DescriptionLOD lod) { + String descriptionColumn = getDescriptionColumn(lod); + final boolean useSubTypes = (zoomLevel.equals(EventTypeZoomLevel.SUB_TYPE)); + + //get some info about the time range requested + RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(timeRange); + //use 'rounded out' range + long start = timeRange.getStartMillis() / 1000;//.getLowerBound(); + long end = timeRange.getEndMillis() / 1000;//Millis();//rangeInfo.getUpperBound(); + if (Objects.equals(start, end)) { + end++; + } + + //get a sqlite srtftime format string + String strfTimeFormat = getStrfTimeFormat(rangeInfo.getPeriodSize()); + + //effectively map from type to (map from description to events) + Map<EventType, SetMultimap< String, AggregateEvent>> typeMap = new HashMap<>(); + + //get all agregate events in this time unit + dbReadLock(); + String query = "select strftime('" + strfTimeFormat + "',time , 'unixepoch'" + (TimeLineController.getTimeZone().get().equals(TimeZone.getDefault()) ? ", 'localtime'" : "") + ") as interval, group_concat(event_id) as event_ids, Min(time), Max(time), " + descriptionColumn + ", " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN) + + " from events where time >= " + start + " and time < " + end + " and " + getSQLWhere(filter) + + " group by interval, " + (useSubTypes ? SUB_TYPE_COLUMN : BASE_TYPE_COLUMN) + " , " + descriptionColumn + + " order by Min(time)"; + System.out.println(query); + ResultSet rs = null; + try (Statement stmt = con.createStatement(); // scoop up requested events in groups organized by interval, type, and desription + ) { + + Stopwatch stopwatch = new Stopwatch(); + stopwatch.start(); + + rs = stmt.executeQuery(query); + stopwatch.stop(); + System.out.println(stopwatch.elapsedMillis() / 1000.0 + " seconds"); + while (rs.next()) { + EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt(SUB_TYPE_COLUMN)) : BaseTypes.values()[rs.getInt(BASE_TYPE_COLUMN)]; + + AggregateEvent aggregateEvent = new AggregateEvent( + new Interval(rs.getLong("Min(time)") * 1000, rs.getLong("Max(time)") * 1000, TimeLineController.getJodaTimeZone()), + type, + Arrays.asList(rs.getString("event_ids").split(",")), + rs.getString(descriptionColumn), lod); + + //put events in map from type/descrition -> event + SetMultimap<String, AggregateEvent> descrMap = typeMap.get(type); + if (descrMap == null) { + descrMap = HashMultimap.<String, AggregateEvent>create(); + typeMap.put(type, descrMap); + } + descrMap.put(aggregateEvent.getDescription(), aggregateEvent); + } + + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + try { + rs.close(); + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } + dbReadUnlock(); + } + + //result list to return + ArrayList<AggregateEvent> aggEvents = new ArrayList<>(); + + //save this for use when comparing gap size + Period timeUnitLength = rangeInfo.getPeriodSize().getPeriod(); + + //For each (type, description) key, merge agg events + for (SetMultimap<String, AggregateEvent> descrMap : typeMap.values()) { + for (String descr : descrMap.keySet()) { + //run through the sorted events, merging together adjacent events + Iterator<AggregateEvent> iterator = descrMap.get(descr).stream() + .sorted((AggregateEvent o1, AggregateEvent o2) + -> Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis())) + .iterator(); + AggregateEvent current = iterator.next(); + while (iterator.hasNext()) { + AggregateEvent next = iterator.next(); + Interval gap = current.getSpan().gap(next.getSpan()); + + //if they overlap or gap is less one quarter timeUnitLength + //TODO: 1/4 factor is arbitrary. review! -jm + if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) { + //merge them + current = AggregateEvent.merge(current, next); + } else { + //done merging into current, set next as new current + aggEvents.add(current); + current = next; + } + } + aggEvents.add(current); + } + } + + //at this point we should have a list of aggregate events. + //one per type/description spanning consecutive time units as determined in rangeInfo + return aggEvents; + } + + private long getDBInfo(String key, long defaultValue) { + dbReadLock(); + try { + getDBInfoStmt.setString(1, key); + + try (ResultSet rs = getDBInfoStmt.executeQuery()) { + long result = defaultValue; + while (rs.next()) { + result = rs.getLong("value"); + } + return result; + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to read key: " + key + " from db_info", ex); + } finally { + dbReadUnlock(); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to set key: " + key + " on getDBInfoStmt ", ex); + } + + return defaultValue; + } + + private String getDescriptionColumn(DescriptionLOD lod) { + switch (lod) { + case FULL: + return FULL_DESCRIPTION_COLUMN; + case MEDIUM: + return MED_DESCRIPTION_COLUMN; + case SHORT: + default: + return SHORT_DESCRIPTION_COLUMN; + } + } + + private String getStrfTimeFormat(TimeUnits info) { + switch (info) { + case DAYS: + return "%Y-%m-%dT00:00:00"; + case HOURS: + return "%Y-%m-%dT%H:00:00"; + case MINUTES: + return "%Y-%m-%dT%H:%M:00"; + case MONTHS: + return "%Y-%m-01T00:00:00"; + case SECONDS: + return "%Y-%m-%dT%H:%M:%S"; + case YEARS: + return "%Y-01-01T00:00:00"; + default: + return "%Y-%m-%dT%H:%M:%S"; + } + } + + private PreparedStatement prepareStatement(String queryString) throws SQLException { + PreparedStatement prepareStatement = con.prepareStatement(queryString); + preparedStatements.add(prepareStatement); + return prepareStatement; + } + + private void recordDBInfo(String key, long value) { + dbWriteLock(); + try { + recordDBInfoStmt.setString(1, key); + recordDBInfoStmt.setLong(2, value); + recordDBInfoStmt.executeUpdate(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to set dbinfo key: " + key + " value: " + value, ex); + } finally { + dbWriteUnlock(); + } + } + + /** + * inner class that can reference access database connection + */ + public class EventTransaction { + + private boolean closed = false; + + /** + * factory creation method + * + * @param con the {@link ava.sql.Connection} + * + * @return a LogicalFileTransaction for the given connection + * + * @throws SQLException + */ + private EventTransaction() { + + //get the write lock, released in close() + dbWriteLock(); + try { + con.setAutoCommit(false); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex); + } + + } + + private void rollback() { + if (!closed) { + try { + con.rollback(); + + } catch (SQLException ex1) { + LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); + } finally { + close(); + } + } + } + + private void commit(Boolean notify) { + if (!closed) { + try { + con.commit(); + // make sure we close before we update, bc they'll need locks + close(); + + if (notify) { +// fireNewEvents(newEvents); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error commiting events.db.", ex); + rollback(); + } + } + } + + private void close() { + if (!closed) { + try { + con.setAutoCommit(true); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex); + } finally { + closed = true; + + dbWriteUnlock(); + } + } + } + + public Boolean isClosed() { + return closed; + } + } + + public class MultipleTransactionException extends IllegalStateException { + + private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction"; + + public MultipleTransactionException() { + super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO); + } + + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..da9bab52d8b97aef1095527120f2434361dbbe1f --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/db/EventsRepository.java @@ -0,0 +1,360 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.db; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.RemovalNotification; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import javax.annotation.concurrent.GuardedBy; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.ProgressWindow; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.events.type.ArtifactEventType; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.events.type.FileSystemTypes; +import org.sleuthkit.autopsy.timeline.events.type.RootEventType; +import org.sleuthkit.autopsy.timeline.filters.Filter; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides public API (over EventsDB) to access events. In theory this + * insulates the rest of the timeline module form the details of the db + * implementation. Since there are no other implementations of the database or + * clients of this class, and no Java Interface defined yet, in practice this + * just delegates everything to the eventDB + * + * Concurrency Policy: + * + * Since almost everything just delegates to the EventDB, which is internally + * synchronized, we only have to worry about rebuildRepository() which we + * synchronize on our intrinsic lock. + * + */ +public class EventsRepository { + + private static final String FILES_AND_DIRS_WHERE_CLAUSE = "name != '.' AND name != '..'"; + + private final EventDB eventDB; + + private final static Logger LOGGER = Logger.getLogger(EventsRepository.class.getName()); + + @GuardedBy("this") + private SwingWorker<Void, ProgressWindow.ProgressUpdate> dbPopulationWorker; + + private final LoadingCache<Object, Long> maxCache; + + private final LoadingCache<Object, Long> minCache; + + private final FilteredEventsModel modelInstance; + + private final LoadingCache<Long, TimeLineEvent> idToEventCache; + + private final LoadingCache<ZoomParams, Map<EventType, Long>> eventCountsCache; + + private final LoadingCache<ZoomParams, List<AggregateEvent>> aggregateEventsCache; + + public Interval getBoundingEventsInterval(Interval timeRange, Filter filter) { + return eventDB.getBoundingEventsInterval(timeRange, filter); + } + + /** + * @return a FilteredEvetns object with this repository as underlying source + * of events + */ + public FilteredEventsModel getEventsModel() { + return modelInstance; + } + + public EventsRepository() { + //TODO: we should check that case is open, or get passed a case object/directory -jm + this.eventDB = EventDB.getEventDB(Case.getCurrentCase().getCaseDirectory()); + + idToEventCache = CacheBuilder.newBuilder().maximumSize(5000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<Long, TimeLineEvent> rn) -> { + LOGGER.log(Level.INFO, "evicting event: {0}", rn.toString()); + }).build(CacheLoader.from(eventDB::getEventById)); + eventCountsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, Map<EventType, Long>> rn) -> { + LOGGER.log(Level.INFO, "evicting counts: {0}", rn.toString()); + }).build(CacheLoader.from(eventDB::countEvents)); + aggregateEventsCache = CacheBuilder.newBuilder().maximumSize(1000L).expireAfterAccess(10, TimeUnit.MINUTES).removalListener((RemovalNotification<ZoomParams, List<AggregateEvent>> rn) -> { + LOGGER.log(Level.INFO, "evicting aggregated events: {0}", rn.toString()); + }).build(CacheLoader.from(eventDB::getAggregatedEvents)); + maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime)); + minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime)); + this.modelInstance = new FilteredEventsModel(this); + } + + + + /** @return min time (in seconds from unix epoch) */ + public Long getMaxTime() { + return maxCache.getUnchecked("max"); +// return eventDB.getMaxTime(); + } + + /** @return max tie (in seconds from unix epoch) */ + public Long getMinTime() { + return minCache.getUnchecked("min"); +// return eventDB.getMinTime(); + } + + public void recordLastArtifactID(long lastArtfID) { + eventDB.recordLastArtifactID(lastArtfID); + } + + public void recordWasIngestRunning(Boolean wasIngestRunning) { + eventDB.recordWasIngestRunning(wasIngestRunning); + } + + public void recordLastObjID(Long lastObjID) { + eventDB.recordLastObjID(lastObjID); + } + + public boolean getWasIngestRunning() { + return eventDB.getWasIngestRunning(); + } + + public Long getLastObjID() { + return eventDB.getLastObjID(); + } + public long getLastArtfactID() { + return eventDB.getLastArtfactID(); + } + public TimeLineEvent getEventById(Long eventID) { + return idToEventCache.getUnchecked(eventID); + } + + public List<AggregateEvent> getAggregatedEvents(ZoomParams params) { + + return aggregateEventsCache.getUnchecked(params); + } + + public Map<EventType, Long> countEvents(ZoomParams params) { + return eventCountsCache.getUnchecked(params); + + } + + private void invalidateCaches() { + minCache.invalidateAll(); + maxCache.invalidateAll(); + eventCountsCache.invalidateAll(); + aggregateEventsCache.invalidateAll(); + } + + public Set<Long> getEventIDs(Interval timeRange, Filter filter) { + return eventDB.getEventIDs(timeRange, filter); + } + + public Interval getSpanningInterval(Collection<Long> eventIDs) { + return eventDB.getSpanningInterval(eventIDs); + } + + synchronized public void rebuildRepository(Runnable r) { + if (dbPopulationWorker != null) { + dbPopulationWorker.cancel(true); + + } + dbPopulationWorker = new DBPopulationWorker(r); + dbPopulationWorker.execute(); + } + + /** + * SwingWorker to populate event db with data from main autopsy database. + * + * has only local state. accesses eventDB but that is internally + * synchronized/ thread-safe. + */ + private class DBPopulationWorker extends SwingWorker<Void, ProgressWindow.ProgressUpdate> { + + private final ProgressWindow progressDialog; + + //TODO: can we avoid this with a state listener? does it amount to the same thing? + //post population operation to execute + private final Runnable r; + + public DBPopulationWorker(Runnable r) { + progressDialog = new ProgressWindow(null, true, this); + progressDialog.setVisible(true); + this.r = r; + } + + @Override + protected Void doInBackground() throws Exception { + process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, "(re)initializing events database", ""))); + //reset database + //TODO: can we do more incremental updates? -jm + eventDB.dropTable(); + eventDB.initializeDB(); + + //grab ids of all files + SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); + List<Long> files = skCase.findAllFileIdsWhere(FILES_AND_DIRS_WHERE_CLAUSE); + + final int numFiles = files.size(); + process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numFiles, "populating mac events for files: ", ""))); + + //insert file events into db + int i = 1; + EventDB.EventTransaction trans = eventDB.beginTransaction(); + for (final Long fID : files) { + if (isCancelled()) { + break; + } else { + try { + AbstractFile f = skCase.getAbstractFileById(fID); + //TODO: This is broken for logical files? fix -jm + //TODO: logical files don't necessarily have valid timestamps, so ... -jm + final String uniquePath = f.getUniquePath(); + final String parentPath = f.getParentPath(); + String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath); + String rootFolder = StringUtils.substringBetween(parentPath, "/", "/"); + String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, ""); + String medD = datasourceName + parentPath; + + //insert it into the db if time is > 0 => time is legitimate (drops logical files) + if (f.getAtime() > 0) { + eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans); + } + if (f.getMtime() > 0) { + eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans); + } + if (f.getCtime() > 0) { + eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans); + } + if (f.getCrtime() > 0) { + eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, fID, null, uniquePath, medD, shortDesc, f.getKnown(), trans); + } + + process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numFiles, "populating mac events for files: ", f.getName()))); + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "failed to insert mac event for file : " + fID, tskCoreException); + } + } + i++; + } + + //insert artifact based events + //TODO: use (not-yet existing api) to grab all artifacts with timestamps, rather than the hardcoded lists in EventType -jm + for (EventType type : RootEventType.allTypes) { + if (isCancelled()) { + break; + } + //skip file_system events, they are already handled above. + if (type instanceof ArtifactEventType) { + populateEventType((ArtifactEventType) type, trans, skCase); + } + } + + process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, -1, "commiting events db", ""))); + if (isCancelled()) { + eventDB.rollBackTransaction(trans); + } else { + eventDB.commitTransaction(trans, true); + } + + invalidateCaches(); + + return null; + } + + /** + * handle intermediate 'results': just update progress dialog + * + * @param chunks + */ + @Override + protected void process(List<ProgressWindow.ProgressUpdate> chunks) { + super.process(chunks); + ProgressWindow.ProgressUpdate chunk = chunks.get(chunks.size() - 1); + progressDialog.update(chunk); + } + + @Override + protected void done() { + super.done(); + try { + progressDialog.close(); + get(); + + } catch (CancellationException ex) { + LOGGER.log(Level.INFO, "Database population was cancelled by the user. Not all events may be present or accurate. See the log for details.", ex); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.WARNING, "Exception while populating database.", ex); + JOptionPane.showMessageDialog(null, "There was a problem populating the timeline. Not all events may be present or accurate. See the log for details."); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, "Unexpected exception while populating database.", ex); + JOptionPane.showMessageDialog(null, "There was a problem populating the timeline. Not all events may be present or accurate. See the log for details."); + } + r.run(); //execute post db population operation + } + + /** + * populate all the events of one subtype + * + * @param subType the subtype to populate + * @param trans the db transaction to use + * @param skCase a reference to the sleuthkit case + */ + private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans, SleuthkitCase skCase) { + try { + //get all the blackboard artifacts corresponding to the given event sub_type + final ArrayList<BlackboardArtifact> blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactType()); + final int numArtifacts = blackboardArtifacts.size(); + + process(Arrays.asList(new ProgressWindow.ProgressUpdate(0, numArtifacts, "populating " + type.toString() + " events", ""))); + + int i = 0; + for (final BlackboardArtifact bbart : blackboardArtifacts) { + //for each artifact, extract the relevant information for the descriptions + ArtifactEventType.AttributeEventDescription eventDescription = ArtifactEventType.AttributeEventDescription.buildEventDescription(type, bbart); + + if (eventDescription != null && eventDescription.getTime() > 0L) { //insert it into the db if time is > 0 => time is legitimate + eventDB.insertEvent(eventDescription.getTime(), type, bbart.getObjectID(), bbart.getArtifactID(), eventDescription.getFullDescription(), eventDescription.getMedDescription(), eventDescription.getShortDescription(), null, trans); + } + + i++; + process(Arrays.asList(new ProgressWindow.ProgressUpdate(i, numArtifacts, "populating " + type.toString() + " events", ""))); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "There was a problem getting events with sub type = " + type.toString() + ".", ex); + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java new file mode 100644 index 0000000000000000000000000000000000000000..38a0de16c0272ff62c4252bee1a4a0f712b93113 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/ArtifactEventType.java @@ -0,0 +1,189 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.logging.Level; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +public interface ArtifactEventType extends EventType { + + /** + * @return the Artifact type this event type is derived form, or null if + * there is no artifact type (eg file system events) + */ + public BlackboardArtifact.ARTIFACT_TYPE getArtifactType(); + + public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType(); + + /** given an artifact, and a map from attribute types to attributes, pull + * out the time stamp, and compose the descriptions. Each implementation + * of {@link ArtifactEventType} needs to implement parseAttributesHelper() + * as hook for {@link buildEventDescription(org.sleuthkit.datamodel.BlackboardArtifact) + * to invoke. Most subtypes can use this default implementation. + * + * @param artf + * @param attrMap + * + * @return an {@link AttributeEventDescription} containing the timestamp + * and description information + * + * @throws TskCoreException + */ + default AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) throws TskCoreException { + final BlackboardAttribute dateTimeAttr = attrMap.get(getDateTimeAttrubuteType()); + + long time = dateTimeAttr.getValueLong(); + String shortDescription = getShortExtractor().apply(artf, attrMap); + String medDescription = shortDescription + " : " + getMedExtractor().apply(artf, attrMap); + String fullDescription = medDescription + " : " + getFullExtractor().apply(artf, attrMap); + return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription); + } + + /** @return a function from an artifact and a map of its attributes, to a + * String to use as part of the full event description */ + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor(); + + /** @return a function from an artifact and a map of its attributes, to a + * String to use as part of the medium event description */ + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor(); + + /** @return a function from an artifact and a map of its attributes, to a + * String to use as part of the short event description */ + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor(); + + /** + * bundles the per event information derived from a BlackBoard Artifact into + * one object. Primarily used to have a single return value for + * {@link SubType#buildEventDescription(org.sleuthkit.datamodel.BlackboardArtifact). + */ + static class AttributeEventDescription { + + final private long time; + + public long getTime() { + return time; + } + + public String getShortDescription() { + return shortDescription; + } + + public String getMedDescription() { + return medDescription; + } + + public String getFullDescription() { + return fullDescription; + } + + final private String shortDescription; + + final private String medDescription; + + final private String fullDescription; + + public AttributeEventDescription(long time, String shortDescription, + String medDescription, + String fullDescription) { + this.time = time; + this.shortDescription = shortDescription; + this.medDescription = medDescription; + this.fullDescription = fullDescription; + } + + /** + * Build a {@link AttributeEventDescription} derived from a + * {@link BlackboardArtifact}. This is a template method that relies on + * each {@link SubType}'s implementation of + * {@link SubType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)} + * know how to go from {@link BlackboardAttribute}s to the event + * description. + * + * @param artf the {@link BlackboardArtifact} to derive the event + * description from + * + * @return an {@link AttributeEventDescription} derived from the given + * artifact + * + * @throws TskCoreException is there is a problem accessing the + * blackboard data + */ + static public AttributeEventDescription buildEventDescription( + ArtifactEventType type, BlackboardArtifact artf) throws TskCoreException { + //if we got passed an artifact that doesn't correspond to the type of the event, + //something went very wrong. throw an exception. + if (type.getArtifactType().getTypeID() != artf.getArtifactTypeID()) { + throw new IllegalArgumentException(); + } + + /* build a map from attribute type to attribute, this makes + * implementing the parseAttributeHelper easier but could be + * ineffecient if we don't need most of the attributes. This would + * be unnessecary if there was an api on Blackboard artifacts to get + * specific attributes by type */ + List<BlackboardAttribute> attributes = artf.getAttributes(); + Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap = new HashMap<>(); + for (BlackboardAttribute attr : attributes) { + attrMap.put(BlackboardAttribute.ATTRIBUTE_TYPE.fromLabel(attr. + getAttributeTypeName()), attr); + } + + if (attrMap.get(type.getDateTimeAttrubuteType()) == null) { + Logger.getLogger(AttributeEventDescription.class.getName()).log(Level.WARNING, "Artifact {0} has no date/time attribute, skipping it.", artf.getArtifactID()); + return null; + } + //use the hook provided by this subtype implementation + return type.parseAttributesHelper(artf, attrMap); + } + } + + public static class AttributeExtractor implements BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> { + + @Override + public String apply(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) { + final BlackboardAttribute attr = attrMap.get(attribute); + return (attr != null) ? StringUtils.defaultString(attr.getValueString()) : " "; + } + + private final BlackboardAttribute.ATTRIBUTE_TYPE attribute; + + public AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE attribute) { + this.attribute = attribute; + } + } + + public static class EmptyExtractor implements BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> { + + @Override + public String apply(BlackboardArtifact t, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> u) { + return ""; + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..ff7fc925ff73edd462b772a870b2353c2d9d879e --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/BaseTypes.java @@ -0,0 +1,108 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.Arrays; +import java.util.List; +import javafx.scene.image.Image; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** + * RootTypes are event types that have no super type. + */ +public enum BaseTypes implements EventType { + + FILE_SYSTEM("File System", "blue-document.png") { + + @Override + public List<? extends EventType> getSubTypes() { + return Arrays.asList(FileSystemTypes.values()); + } + + @Override + public EventType getSubType(String string) { + return FileSystemTypes.valueOf(string); + } + }, + WEB_ACTIVITY("Web Activity", "web-file.png") { + + @Override + public List<? extends EventType> getSubTypes() { + return Arrays.asList(WebTypes.values()); + } + + @Override + public EventType getSubType(String string) { + return WebTypes.valueOf(string); + } + }, + MISC_TYPES("Misc Types", "block.png") { + + @Override + public List<? extends EventType> getSubTypes() { + return Arrays.asList(MiscTypes.values()); + } + + @Override + public EventType getSubType(String string) { + return MiscTypes.valueOf(string); + } + }; + + private final String displayName; + + private final String iconBase; + + private Image image; + + public Image getFXImage() { + return image; + } + + @Override + public String getIconBase() { + return iconBase; + } + + @Override + public EventTypeZoomLevel getZoomLevel() { + return EventTypeZoomLevel.BASE_TYPE; + } + + @Override + public String getDisplayName() { + return displayName; + } + + private BaseTypes(String displayName, String iconBase) { + this.displayName = displayName; + this.iconBase = iconBase; + this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); + } + + @Override + public EventType getSuperType() { + return RootEventType.getInstance(); + } + + @Override + public EventType getSubType(String string) { + return BaseTypes.valueOf(string); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java new file mode 100644 index 0000000000000000000000000000000000000000..fbbc95e0ecc2b41ecb7e464bf60e52161d17713d --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/EventType.java @@ -0,0 +1,108 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** + * An Event Type represents a distinct kind of event ie file system or web + * activity. An EventType may have an optional super-type and 0 or more + * subtypes, allowing events to be organized in a type hierarchy. + */ +public interface EventType { + + final static List<? extends EventType> allTypes = RootEventType.getInstance().getSubTypesRecusive(); + + static Comparator<EventType> getComparator() { + return Comparator.comparing(EventType.allTypes::indexOf); + + } + + default BaseTypes getBaseType() { + if (this instanceof BaseTypes) { + return (BaseTypes) this; + } else { + return getSuperType().getBaseType(); + } + } + + default List<? extends EventType> getSubTypesRecusive() { + ArrayList<EventType> flatList = new ArrayList<>(); + + for (EventType et : getSubTypes()) { + flatList.add(et); + flatList.addAll(et.getSubTypesRecusive()); + } + return flatList; + } + + /** + * @return the color used to represent this event type visually + */ + default Color getColor() { + + Color baseColor = this.getSuperType().getColor(); + int siblings = getSuperType().getSiblingTypes().stream().max(( + EventType t, EventType t1) + -> Integer.compare(t.getSubTypes().size(), t1.getSubTypes().size())) + .get().getSubTypes().size() + 1; + int superSiblings = this.getSuperType().getSiblingTypes().size(); + + double offset = (360.0 / superSiblings) / siblings; + final Color deriveColor = baseColor.deriveColor(ordinal() * offset, 1, 1, 1); + + return Color.hsb(deriveColor.getHue(), deriveColor.getSaturation(), deriveColor.getBrightness()); + + } + + default List<? extends EventType> getSiblingTypes() { + return this.getSuperType().getSubTypes(); + } + + /** + * @return the super type of this event + */ + public EventType getSuperType(); + + public EventTypeZoomLevel getZoomLevel(); + + /** + * @return a list of event types, one for each subtype of this eventype, or + * an empty list if this event type has no subtypes + */ + public List<? extends EventType> getSubTypes(); + + /* return the name of the icon file for this type, it will be resolved in + * the org/sleuthkit/autopsy/timeline/images */ + public String getIconBase(); + + public String getDisplayName(); + + public EventType getSubType(String string); + + public Image getFXImage(); + + public int ordinal(); + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..771b692023e4e42c75fe261817001544f28b2ddf --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/FileSystemTypes.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.Collections; +import java.util.List; +import javafx.scene.image.Image; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** + * + */ +public enum FileSystemTypes implements EventType { + + FILE_MODIFIED("File Modified", "blue-document-attribute-m.png"), + FILE_ACCESSED("File Accessed", "blue-document-attribute-a.png"), + FILE_CREATED("File Created", "blue-document-attribute-b.png"), + FILE_CHANGED("File Changed", "blue-document-attribute-c.png"); + + private final String iconBase; + + private final Image image; + + @Override + public Image getFXImage() { + return image; + } + + @Override + public String getIconBase() { + return iconBase; + } + + @Override + public EventTypeZoomLevel getZoomLevel() { + return EventTypeZoomLevel.SUB_TYPE; + } + + private final String displayName; + + @Override + public EventType getSubType(String string) { + return FileSystemTypes.valueOf(string); + } + + @Override + public EventType getSuperType() { + return BaseTypes.FILE_SYSTEM; + } + + @Override + public List<? extends EventType> getSubTypes() { + return Collections.emptyList(); + } + + private FileSystemTypes(String displayName, String iconBase) { + this.displayName = displayName; + this.iconBase = iconBase; + this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); + } + + @Override + public String getDisplayName() { + return displayName; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..1d44a251611a8e013b2b67b2ca286622d394cf9b --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/MiscTypes.java @@ -0,0 +1,253 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import javafx.scene.image.Image; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +public enum MiscTypes implements EventType, ArtifactEventType { + + MESSAGE("Messages", "message.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_MESSAGE_TYPE), + (artf, attrMap) -> { + final BlackboardAttribute dir = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION); + final BlackboardAttribute name = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + final BlackboardAttribute phoneNumber = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER); + final BlackboardAttribute subject = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT); + List<String> asList = Arrays.asList(stringValueOf(dir), name != null || phoneNumber != null ? toFrom(dir) : "", stringValueOf(name != null ? name : phoneNumber), (subject == null ? "" : stringValueOf(subject))); + return StringUtils.join(asList, " "); + }, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT)), + GPS_ROUTE("GPS Routes", "gps-search.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION), + (BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) -> { + final BlackboardAttribute latStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); + final BlackboardAttribute longStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); + final BlackboardAttribute latEnd = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); + final BlackboardAttribute longEnd = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); + return String.format("from %1$g %2$g to %3$g %4$g", latStart.getValueDouble(), longStart.getValueDouble(), latEnd.getValueDouble(), longEnd.getValueDouble()); + }), + GPS_TRACKPOINT("Location History", "gps-trackpoint.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME), + (artf, attrMap) -> { + final BlackboardAttribute longitude = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); + final BlackboardAttribute latitude = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); + return (latitude != null ? latitude.getValueDouble() : "") + " " + (longitude != null ? longitude.getValueDouble() : ""); + }, + (artf, attrMap) -> ""), + CALL_LOG("Calls", "calllog.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION)), + EMAIL("Email", "mail-icon-16.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT, + (artifact, attrMap) -> { + final BlackboardAttribute emailFrom = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_FROM); + final BlackboardAttribute emailTo = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_TO); + return (emailFrom != null ? emailFrom.getValueString() : "") + " to " + (emailTo != null ? emailTo.getValueString() : ""); + }, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN)), + RECENT_DOCUMENTS("Recent Documents", "recent_docs.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_RECENT_OBJECT, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH).andThen( + (String t) -> (StringUtils.substringBeforeLast(StringUtils.substringBeforeLast(t, "\\"), "\\"))), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH).andThen( + (String t) -> StringUtils.substringBeforeLast(t, "\\")), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH)) { + + /** Override + * {@link ArtifactEventType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)} + * with non-default description construction */ + @Override + public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) throws TskCoreException { + final BlackboardAttribute dateTimeAttr = attrMap.get(getDateTimeAttrubuteType()); + + long time = dateTimeAttr.getValueLong(); + + //Non-default description construction + String shortDescription = getShortExtractor().apply(artf, attrMap); + String medDescription = getMedExtractor().apply(artf, attrMap); + String fullDescription = getFullExtractor().apply(artf, attrMap); + + return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription); + } + }, + INSTALLED_PROGRAM("Installed Programs", "programs.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_INSTALLED_PROG, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME), + new EmptyExtractor(), + new EmptyExtractor()), + EXIF("Exif", "camera-icon-16.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL), + (BlackboardArtifact t, + Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> u) -> { + try { + return t.getSleuthkitCase().getAbstractFileById(t.getObjectID()).getName(); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + return " error loading file name"; + } + }), + DEVICES_ATTACHED("Devices Attached", "usb_devices.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_DEVICE_ATTACHED, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_ID)); + + static public String stringValueOf(BlackboardAttribute attr) { + return attr != null ? attr.getValueString() : ""; + } + + public static String toFrom(BlackboardAttribute dir) { + if (dir == null) { + return ""; + } else { + switch (dir.getValueString()) { + case "Incoming": + return "from"; + case "Outgoing": + return "to"; + default: + return ""; + } + } + } + + private final BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType; + + private final String iconBase; + + private final Image image; + + @Override + public Image getFXImage() { + return image; + } + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor; + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor; + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor; + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor() { + return longExtractor; + } + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor() { + return medExtractor; + } + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor() { + return shortExtractor; + } + + @Override + public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType() { + return dateTimeAttributeType; + } + + @Override + public EventTypeZoomLevel getZoomLevel() { + return EventTypeZoomLevel.SUB_TYPE; + } + + private final String displayName; + + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public String getIconBase() { + return iconBase; + } + + @Override + public EventType getSubType(String string) { + return MiscTypes.valueOf(string); + } + + private MiscTypes(String displayName, String iconBase, BlackboardArtifact.ARTIFACT_TYPE artifactType, + BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor) { + this.displayName = displayName; + this.iconBase = iconBase; + this.artifactType = artifactType; + this.dateTimeAttributeType = dateTimeAttributeType; + this.shortExtractor = shortExtractor; + this.medExtractor = medExtractor; + this.longExtractor = longExtractor; + this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); + } + + @Override + public EventType getSuperType() { + return BaseTypes.MISC_TYPES; + } + + @Override + public List<? extends EventType> getSubTypes() { + return Collections.emptyList(); + } + + @Override + public BlackboardArtifact.ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java new file mode 100644 index 0000000000000000000000000000000000000000..79716db2f7693745513d308fc4da5460fe42f2eb --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/RootEventType.java @@ -0,0 +1,95 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** A singleton {@link } EventType to represent the root type of all event + * types. */ +public class RootEventType implements EventType { + + @Override + public List<RootEventType> getSiblingTypes() { + return Collections.singletonList(this); + } + + @Override + public EventTypeZoomLevel getZoomLevel() { + return EventTypeZoomLevel.ROOT_TYPE; + } + + private RootEventType() { + } + + public static RootEventType getInstance() { + return RootEventTypeHolder.INSTANCE; + } + + @Override + public EventType getSubType(String string) { + return BaseTypes.valueOf(string); + } + + @Override + public int ordinal() { + return 0; + } + + private static class RootEventTypeHolder { + + private static final RootEventType INSTANCE = new RootEventType(); + } + + @Override + public Color getColor() { + return Color.hsb(359, .9, .9, 0); + } + + @Override + public RootEventType getSuperType() { + return this; + } + + @Override + public List<BaseTypes> getSubTypes() { + return Arrays.asList(BaseTypes.values()); + } + + + + @Override + public String getIconBase() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public String getDisplayName() { + return "Event Types"; + } + + @Override + public Image getFXImage() { + return null; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java new file mode 100644 index 0000000000000000000000000000000000000000..63093209940307fb63de9abef9bcdb760ddc56ad --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/events/type/WebTypes.java @@ -0,0 +1,186 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.events.type; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import javafx.scene.image.Image; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; + +/** + * + */ +public enum WebTypes implements EventType, ArtifactEventType { + + WEB_DOWNLOADS("Web Downloads", + "downloads.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL)) { + + /** Override + * {@link ArtifactEventType#parseAttributesHelper(org.sleuthkit.datamodel.BlackboardArtifact, java.util.Map)} + * with non default descritpion construction */ + @Override + public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute> attrMap) { + long time = attrMap.get(getDateTimeAttrubuteType()).getValueLong(); + String domain = getShortExtractor().apply(artf, attrMap); + String path = getMedExtractor().apply(artf, attrMap); + String fileName = StringUtils.substringAfterLast(path, "/"); + String url = getFullExtractor().apply(artf, attrMap); + + //TODO: review non default descritpion construction + String shortDescription = fileName + " from " + domain; + String medDescription = fileName + " from " + url; + String fullDescription = path + " from " + url; + return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription); + } + }, + //TODO: review description seperators + WEB_COOKIE("Web Cookies", + "cookies.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_VALUE)), + //TODO: review description seperators + WEB_BOOKMARK("Web Bookmarks", + "bookmarks.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE)), + //TODO: review description seperators + WEB_HISTORY("Web History", + "history.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE)), + //TODO: review description seperators + WEB_SEARCH("Web Searches", + "searchquery.png", + BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN), + new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME)); + + private final BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType; + + private final String iconBase; + + private final Image image; + + @Override + public Image getFXImage() { + return image; + } + + @Override + public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType() { + return dateTimeAttributeType; + } + + @Override + public EventTypeZoomLevel getZoomLevel() { + return EventTypeZoomLevel.SUB_TYPE; + } + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor; + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor; + + private final BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor; + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getFullExtractor() { + return longExtractor; + } + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getMedExtractor() { + return medExtractor; + } + + @Override + public BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> getShortExtractor() { + return shortExtractor; + } + + private final String displayName; + + BlackboardArtifact.ARTIFACT_TYPE artifactType; + + @Override + public String getIconBase() { + return iconBase; + } + + @Override + public BlackboardArtifact.ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + + private WebTypes(String displayName, String iconBase, BlackboardArtifact.ARTIFACT_TYPE artifactType, + BlackboardAttribute.ATTRIBUTE_TYPE dateTimeAttributeType, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> shortExtractor, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> medExtractor, + BiFunction<BlackboardArtifact, Map<BlackboardAttribute.ATTRIBUTE_TYPE, BlackboardAttribute>, String> longExtractor) { + this.displayName = displayName; + this.iconBase = iconBase; + this.artifactType = artifactType; + this.dateTimeAttributeType = dateTimeAttributeType; + this.shortExtractor = shortExtractor; + this.medExtractor = medExtractor; + this.longExtractor = longExtractor; + this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); + } + + @Override + public EventType getSuperType() { + return BaseTypes.WEB_ACTIVITY; + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public EventType getSubType(String string) { + return WebTypes.valueOf(string); + } + + @Override + public List<? extends EventType> getSubTypes() { + return Collections.emptyList(); + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java new file mode 100644 index 0000000000000000000000000000000000000000..5e44cb9d79d5f96224237f9c3a3e564ea4e461be --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -0,0 +1,151 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.explorernodes; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javafx.beans.Observable; +import javax.swing.Action; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.openide.nodes.Children; +import org.openide.nodes.PropertySupport; +import org.openide.nodes.Sheet; +import org.openide.util.Exceptions; +import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.events.TimeLineEvent; +import org.sleuthkit.autopsy.datamodel.DataModelActionsFactory; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; +import org.sleuthkit.autopsy.datamodel.NodeProperty; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; + +/** * Node for {@link TimeLineEvent}s. */ +class EventNode extends DisplayableItemNode { + + private final TimeLineEvent e; + + EventNode(TimeLineEvent eventById, AbstractFile file, BlackboardArtifact artifact) { + super(Children.LEAF, Lookups.fixed(eventById, file, artifact)); + this.e = eventById; + this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase()); + } + + EventNode(TimeLineEvent eventById, AbstractFile file) { + super(Children.LEAF, Lookups.fixed(eventById, file)); + this.e = eventById; + this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getType().getIconBase()); + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set properties = s.get(Sheet.PROPERTIES); + if (properties == null) { + properties = Sheet.createPropertiesSet(); + s.put(properties); + } + + final TimeProperty timePropery = new TimeProperty("time", "Date/Time", "time ", getDateTimeString()); + + TimeLineController.getTimeZone().addListener((Observable observable) -> { + try { + timePropery.setValue(getDateTimeString()); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } + }); + + // String name, String displayName, String desc, T value + properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); //gets overriden with icon + + properties.put(timePropery); + properties.put(new NodeProperty<>("eventID", "Event ID", "event id", e.getEventID())); + properties.put(new NodeProperty<>("fileID", "File ID", "File id", e.getFileID())); + properties.put(new NodeProperty<>("artifactID", "Result ID", "result id", e.getArtifactID() == 0 ? "" : e.getArtifactID())); + properties.put(new NodeProperty<>("eventBaseType", "Base Type", "base type", e.getType().getSuperType().getDisplayName())); + properties.put(new NodeProperty<>("eventSubType", "Sub Type", "sub type", e.getType().getDisplayName())); + properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString())); + properties.put(new NodeProperty<>("description", "Description", "description", e.getFullDescription())); + return s; + } + + private String getDateTimeString() { + return new DateTime(e.getTime() * 1000, DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter()); + } + + @Override + public Action[] getActions(boolean context) { + Action[] superActions = super.getActions(context); + List<Action> actionsList = new ArrayList<>(); + actionsList.addAll(Arrays.asList(superActions)); + + final Content content = getLookup().lookup(Content.class); + final BlackboardArtifact artifact = getLookup().lookup(BlackboardArtifact.class); + + final List<Action> factoryActions = DataModelActionsFactory.getActions(content, artifact != null); + + actionsList.addAll(factoryActions); + return actionsList.toArray(new Action[0]); + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + public <T> T accept(DisplayableItemNodeVisitor<T> dinv) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + class TimeProperty extends PropertySupport.ReadWrite<String> { + + private String value; + + @Override + public boolean canWrite() { + return false; + } + + public TimeProperty(String name, String displayName, String shortDescription, String value) { + super(name, String.class, displayName, shortDescription); + setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button NON-NLS + this.value = value; + } + + @Override + public String getValue() throws IllegalAccessException, InvocationTargetException { + return value; + } + + @Override + public void setValue(String t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + String oldValue = getValue(); + value = t; + firePropertyChange("time", oldValue, t); + } + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java new file mode 100644 index 0000000000000000000000000000000000000000..7716d392d9ab199bae4be9216cef4be8e7c8bcb5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -0,0 +1,130 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.explorernodes; + +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.events.type.BaseTypes; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +public class EventRootNode extends DisplayableItemNode { + + public static final int MAX_EVENTS_TO_DISPLAY = 5000; + + private final int childCount; + + public EventRootNode(String NAME, Collection<Long> fileIds, FilteredEventsModel filteredEvents) { + super(Children.create(new EventNodeChildFactory(fileIds, filteredEvents), true), Lookups.singleton(fileIds)); + + super.setName(NAME); + super.setDisplayName(NAME); + + childCount = fileIds.size(); + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public <T> T accept(DisplayableItemNodeVisitor<T> v) { + return null; + } + + public int getChildCount() { + return childCount; + } + + /** The node factories used to make lists of files to send to the result + * viewer using the lazy loading (rather than background) loading option to + * facilitate */ + private static class EventNodeChildFactory extends ChildFactory<Long> { + + private static final Logger LOGGER = Logger.getLogger(EventNodeChildFactory.class.getName()); + + private final Collection<Long> eventIDs; + + private final FilteredEventsModel filteredEvents; + + EventNodeChildFactory(Collection<Long> fileIds, FilteredEventsModel filteredEvents) { + this.eventIDs = fileIds; + this.filteredEvents = filteredEvents; + } + + @Override + protected boolean createKeys(List<Long> toPopulate) { + if (eventIDs.size() < MAX_EVENTS_TO_DISPLAY) { + toPopulate.addAll(eventIDs); + } else { + toPopulate.add(-1l); + } + return true; + } + + @Override + protected Node createNodeForKey(Long eventID) { + if (eventID >= 0) { + final TimeLineEvent eventById = filteredEvents.getEventById(eventID); + try { + if (eventById.getType().getSuperType() == BaseTypes.FILE_SYSTEM) { + return new EventNode(eventById, Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID())); + } else { + AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(eventById.getFileID()); + BlackboardArtifact blackboardArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(eventById.getArtifactID()); + + return new EventNode(eventById, file, blackboardArtifact); + } + + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent.", tskCoreException); + return null; + } + } else { + return new TooManyNode(eventIDs.size()); + } + } + } + + private static class TooManyNode extends AbstractNode { + + public TooManyNode(int size) { + super(Children.LEAF); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/info-icon-16.png"); + setDisplayName("Too many events to display. Maximum = " + MAX_EVENTS_TO_DISPLAY + ". But there are " + size + " to display."); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..4e4f122f24b8731f66c0e19d82893baa931e7e25 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java @@ -0,0 +1,53 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import javafx.beans.property.SimpleBooleanProperty; + +/** + * Base implementation of a {@link Filter}. Implements active property. + * + */ +public abstract class AbstractFilter implements Filter { + + private final SimpleBooleanProperty active = new SimpleBooleanProperty(true); + + private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false); + + @Override + public SimpleBooleanProperty getActiveProperty() { + return active; + } + + @Override + public SimpleBooleanProperty getDisabledProperty() { + return disabled; + } + + @Override + public void setActive(Boolean act) { + active.set(act); + } + + @Override + public boolean isActive() { + return active.get(); + } + + @Override + public void setDisabled(Boolean act) { + disabled.set(act); + } + + @Override + public boolean isdisabled() { + return disabled.get(); + } + + @Override + public String getStringCheckBox() { + return "[" + (isActive() ? "x" : " ") + "]"; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..f1fab113c2581f24efd1b2d42ace28d4636ef2a3 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java @@ -0,0 +1,106 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import java.util.List; +import javafx.beans.Observable; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +/** + * A Filter with a collection of {@link Filter} sub-filters. If this + * filter is not active than none of its sub-filters are applied either. + * Concrete implementations can decide how to combine the sub-filters. + * + * a {@link CompoundFilter} uses listeners to enforce the following + * relationships between it and its sub-filters: + * <ol> + * <le>if a filter becomes active, and all its sub-filters were inactive, make + * them all active</le> + * <le>if a filter becomes inactive and all its sub-filters were active, make + * them all inactive</le> + * <le>if a sub-filter changes active state set the parent filter active if any + * of its sub-filters are active.</le> + * </ol> + */ +public abstract class CompoundFilter extends AbstractFilter { + + /** the list of sub-filters that make up this filter */ + private final ObservableList<Filter> subFilters; + + public final ObservableList<Filter> getSubFilters() { + return subFilters; + } + + /** construct a compound filter from a list of other filters to combine. + * + * @param subFilters + */ + public CompoundFilter(ObservableList<Filter> subFilters) { + super(); + this.subFilters = FXCollections.<Filter>synchronizedObservableList(subFilters); + + //listen to changes in list of subfilters and add active state listener to newly added filters + this.subFilters.addListener((ListChangeListener.Change<? extends Filter> c) -> { + while (c.next()) { + addListeners(c.getAddedSubList()); + //TODO: remove listeners from removed subfilters + } + }); + + //add listeners to subfilters + addListeners(subFilters); + + //disable subfilters if this filter is disabled + getDisabledProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { + if (newValue) { + getSubFilters().forEach((Filter t) -> { + t.setDisabled(true); + }); + } else { + final boolean isActive = !isActive(); + getSubFilters().forEach((Filter t) -> { + t.setDisabled(isActive); + }); + } + }); + + //listen to active property and adjust subfilters active property + getActiveProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { + if (newValue) { + // if this filter become active, and all its subfilters were inactive, make them all active + if (getSubFilters().stream().noneMatch(Filter::isActive)) { + getSubFilters().forEach((Filter filter) -> { + filter.setActive(true); + }); + } + } else { + //if this filter beceoms inactive and all its subfilters where active, make them inactive + if (getSubFilters().stream().allMatch(Filter::isActive)) { + getSubFilters().forEach((Filter filter) -> { + filter.setActive(false); + }); + } + } + + //disabled subfilters if this filter is not active + getSubFilters().forEach((Filter t) -> { + t.setDisabled(!newValue); + }); + }); + } + + private void addListeners(List<? extends Filter> newSubfilters) { + for (Filter sf : newSubfilters) { + //if a subfilter changes active state + sf.getActiveProperty().addListener((Observable observable) -> { + //set this filter acttive af any of the subfilters are active. + setActive(getSubFilters().parallelStream().anyMatch(Filter::isActive)); + }); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java new file mode 100644 index 0000000000000000000000000000000000000000..df07d23ffa7443ef6bcd71d75e1fd35799a4767b --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/Filter.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.sleuthkit.autopsy.timeline.events.type.RootEventType; + +/** Interface for Filters */ +public interface Filter { + + /** @return the default filter used at startup */ + static Filter getDefaultFilter() { + return Filter.intersect(new Filter[]{new HideKnownFilter(), new TextFilter(), new TypeFilter(RootEventType.getInstance())}); + } + + /** @param <S> the type of the given filters + * @param filters a set of filters to intersect + * + * @return a filter that is the intersection of the given filters */ + public static IntersectionFilter intersect(ObservableList<Filter> filters) { + return new IntersectionFilter(filters); + } + + /** @param <S> the type of the given filters + * @param filters a set of filters to intersect + * + * @return a filter that is the intersection of the given filters */ + public static IntersectionFilter intersect(Filter[] filters) { + return new IntersectionFilter(FXCollections.observableArrayList(filters)); + } + + /** since filters have mutable state (active) and are observed in various + * places, we need a mechanism to copy the current state to keep in history. + * + * Concrete subtasks should implement this in a way that preserves the + * active state and any subfilters. + * + * @return a copy of this filter. */ + Filter copyOf(); + + String getDisplayName(); + + String getHTMLReportString(); + + String getStringCheckBox(); + + boolean isActive(); + + void setActive(Boolean act); + + SimpleBooleanProperty getActiveProperty(); + + /* TODO: disabled state only affects the state of the checkboxes in the ui + * and not the actual filters and shouldn't be implemented here, but it + * was too hard to figure out how it should be implemented without intruding + * on the ui-ignorant filters */ + void setDisabled(Boolean act); + + SimpleBooleanProperty getDisabledProperty(); + + boolean isdisabled(); + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..86c36ce5fe26534861500879d11ee24c5c2640f8 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java @@ -0,0 +1,69 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.filters; + +/** + * Filter to hide known files + */ +public class HideKnownFilter extends AbstractFilter { + + @Override + public String getDisplayName() { + return "Hide Known Files"; + } + + public HideKnownFilter() { + super(); + getActiveProperty().set(false); + } + + @Override + public HideKnownFilter copyOf() { + HideKnownFilter hideKnownFilter = new HideKnownFilter(); + hideKnownFilter.setActive(isActive()); + hideKnownFilter.setDisabled(isdisabled()); + return hideKnownFilter; + } + + @Override + public String getHTMLReportString() { + return "hide known" + getStringCheckBox(); + } + + @Override + public int hashCode() { + int hash = 7; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final HideKnownFilter other = (HideKnownFilter) obj; + + return isActive() == other.isActive(); + + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..b11b0f53b7f5718ec4efad9d46e72a91e599be4d --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java @@ -0,0 +1,70 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** Intersection(And) filter */ +public class IntersectionFilter extends CompoundFilter { + + public IntersectionFilter(ObservableList<Filter> subFilters) { + super(subFilters); + } + + public IntersectionFilter() { + super(FXCollections.<Filter>observableArrayList()); + } + + @Override + public IntersectionFilter copyOf() { + IntersectionFilter filter = new IntersectionFilter(FXCollections.observableArrayList( + this.getSubFilters().stream() + .map(Filter::copyOf) + .collect(Collectors.toList()))); + filter.setActive(isActive()); + filter.setDisabled(isdisabled()); + return filter; + } + + @Override + public String getDisplayName() { + return "Intersection"; + } + + @Override + public String getHTMLReportString() { + return getSubFilters().stream().filter(Filter::isActive).map(Filter::getHTMLReportString).collect(Collectors.joining("</li><li>", "<ul><li>", "</li></ul>")); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final IntersectionFilter other = (IntersectionFilter) obj; + + if (isActive() != other.isActive()) { + return false; + } + + for (int i = 0; i < getSubFilters().size(); i++) { + if (getSubFilters().get(i).equals(other.getSubFilters().get(i)) == false) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hash = 7; + return hash; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..44a38c30e9a793ee1cb9593e100f921aa784c0a2 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java @@ -0,0 +1,91 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import java.util.Objects; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; +import org.apache.commons.lang3.StringUtils; + +/** Filter for text matching */ +public class TextFilter extends AbstractFilter { + + public TextFilter() { + } + + public TextFilter(String text) { + this.text.set(text); + } + + private final SimpleStringProperty text = new SimpleStringProperty(); + + synchronized public void setText(String text) { + this.text.set(text); + } + + @Override + public String getDisplayName() { + return "Text Filter"; + } + + synchronized public String getText() { + return text.getValue(); + } + + public Property<String> textProperty() { + return text; + } + + @Override + synchronized public TextFilter copyOf() { + TextFilter textFilter = new TextFilter(getText()); + textFilter.setActive(isActive()); + textFilter.setDisabled(isdisabled()); + return textFilter; + } + + @Override + public String getHTMLReportString() { + return "text like \"" + StringUtils.defaultIfBlank(text.getValue(), "") + "\"" + getStringCheckBox(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TextFilter other = (TextFilter) obj; + + if (isActive() != other.isActive()) { + return false; + } + return Objects.equals(text.get(), other.text.get()); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 29 * hash + Objects.hashCode(this.text.get()); + return hash; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0306e463d9203f0cbe73a56f4b763ccf306d9aa5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/TypeFilter.java @@ -0,0 +1,136 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import java.util.Objects; +import java.util.stream.Collectors; +import javafx.collections.FXCollections; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.events.type.RootEventType; + +/** Event Type Filter. An instance of TypeFilter is usually a tree that + * parallels the event type hierarchy with one filter/node for each event type. */ +public class TypeFilter extends UnionFilter { + + /** the event type this filter passes */ + private final EventType eventType; + + /** private constructor that enables non recursive/tree construction of the + * filter hierarchy for use in {@link TypeFilter#copyOf()}. + * + * @param et the event type this filter passes + * @param recursive true if subfilters should be added for each subtype. + * False if no subfilters should be added. */ + private TypeFilter(EventType et, boolean recursive) { + super(FXCollections.observableArrayList()); + this.eventType = et; + + if (recursive) { // add subfilters for each subtype + for (EventType subType : et.getSubTypes()) { + this.getSubFilters().add(new TypeFilter(subType)); + } + } + } + + /** public constructor. creates a subfilter for each subtype of the given + * event type + * + * @param et the event type this filter will pass */ + public TypeFilter(EventType et) { + this(et, true); + } + + public EventType getEventType() { + return eventType; + } + + @Override + public String getDisplayName() { + return eventType == RootEventType.getInstance() ? "Event Type Filter" : eventType.getDisplayName(); + } + + /** @return a color to use in GUI components representing this filter */ + public Color getColor() { + return eventType.getColor(); + } + + /** @return an image to use in GUI components representing this filter */ + public Image getFXImage() { + return eventType.getFXImage(); + } + + @Override + public TypeFilter copyOf() { + //make a nonrecursive copy of this filter + final TypeFilter typeFilter = new TypeFilter(eventType, false); + typeFilter.setActive(isActive()); + typeFilter.setDisabled(isdisabled()); + //add a copy of each subfilter + this.getSubFilters().forEach((Filter t) -> { + typeFilter.getSubFilters().add(t.copyOf()); + }); + + return typeFilter; + } + + @Override + public String getHTMLReportString() { + String string = getEventType().getDisplayName() + getStringCheckBox(); + if (getSubFilters().isEmpty() == false) { + string = string + " : " + getSubFilters().stream().filter(Filter::isActive).map(Filter::getHTMLReportString).collect(Collectors.joining("</li><li>", "<ul><li>", "</li></ul>")); + } + return string; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TypeFilter other = (TypeFilter) obj; + + if (isActive() != other.isActive()) { + return false; + } + + if (this.eventType != other.eventType) { + return false; + } + + for (int i = 0; i < getSubFilters().size(); i++) { + if (getSubFilters().get(i).equals(other.getSubFilters().get(i)) == false) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 67 * hash + Objects.hashCode(this.eventType); + return hash; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..b4d4db64f669c058d0d53389c59fa2e70db8f658 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java @@ -0,0 +1,25 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * + * Union(or) filter + */ +abstract public class UnionFilter extends CompoundFilter{ + + public UnionFilter(ObservableList<Filter> subFilters) { + super(subFilters); + } + + public UnionFilter() { + super(FXCollections.<Filter>observableArrayList()); + } + + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png new file mode 100644 index 0000000000000000000000000000000000000000..cd3148ba91fe520543b2173128458f64b827deae Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png new file mode 100644 index 0000000000000000000000000000000000000000..af23a9822867ab17b5d1d69f80af0c66879909ea Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/20140521121247760_easyicon_net_32_colorized.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png new file mode 100644 index 0000000000000000000000000000000000000000..f62345e491b0af46cf10f59d46467c257d5be8d9 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-090.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2aa3ccb2d59ecf6cc1db2006bc330a7b32423c Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-180.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png new file mode 100644 index 0000000000000000000000000000000000000000..56e9a63d6233853be9c14ec522652611d7b9056c Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-270.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png new file mode 100644 index 0000000000000000000000000000000000000000..4f40ba52062c62295a89856c8459052c0b78eadb Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-circle-double-135.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ac44f93fc06039a89d8b3f11e9c0e2e654dcfd Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-in.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png new file mode 100644 index 0000000000000000000000000000000000000000..5bbb31178d34a03a47a98395d9ceaec27042ee8c Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-out.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png new file mode 100644 index 0000000000000000000000000000000000000000..04bb4f415ef2ab96888bf36a3e48a97fcf5c44dd Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step-out.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png new file mode 100644 index 0000000000000000000000000000000000000000..5d13ddc048345ef451c73afab190ab6cb0a2ceb3 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow-step.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..12077d33242acb0933fb8a1eb1e9b283173bf3b6 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png new file mode 100644 index 0000000000000000000000000000000000000000..745c65134db478a64016d63a7104e585452f2b9f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_in.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png new file mode 100644 index 0000000000000000000000000000000000000000..2e9bc42bec16e3077a9680e7af0f90395bfeb60c Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/arrow_out.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png new file mode 100644 index 0000000000000000000000000000000000000000..d38c058f2ff9cb262a9d0874e57d579f6f8f8b53 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/block.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png new file mode 100644 index 0000000000000000000000000000000000000000..aa5ee3f37c43430aefa8fc75f6027fa9c4192516 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-a.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png new file mode 100644 index 0000000000000000000000000000000000000000..57f91ce198f61a5fde189f07148b27f712bbd87f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-b.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png new file mode 100644 index 0000000000000000000000000000000000000000..5d12ddd67b3b1439a56113a8b84fecbdbebc1a74 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-c.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png new file mode 100644 index 0000000000000000000000000000000000000000..a61fd8627a4b0debb3a1ff964c7a1f05911d6ff9 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document-attribute-m.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png new file mode 100644 index 0000000000000000000000000000000000000000..6b2545a5a31915b051147d705e8ae6563b08b8cd Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/blue-document.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0eb49fcff6172f3d47c9bf77b1ff594c12ced4 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmark--plus.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..cbb650ad4c18ea5542ed8fe043e2f9acd57a3c70 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/bookmarks.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png new file mode 100644 index 0000000000000000000000000000000000000000..b9d9ffe6229c1ed53b008f4b06a127373fd494f9 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..c71ad536c6012a29f4249e75eb42586044b6f4c7 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_disabled.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..387374f5b6a5a9ee66df3b5868dfbc2e0b041e28 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_back_hover.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..c88640951f972d734f6c09a0555321537bbd65e3 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..61a0867c7922509044d27dc000ca2b0f7142b9a9 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_disabled.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..d201b31bf5dbdd46f01b2edc8261f788b9f94627 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/btn_step_forward_hover.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png new file mode 100644 index 0000000000000000000000000000000000000000..83eb9c448d592e1cc125710cd5d02e9520543457 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/calllog.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..046f049a487675b35e0f28f3ef09ebe7f75ab714 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/camera-icon-16.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..9051fbc609b92b15af9be410e368b7adc20283b8 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/chart_bar.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg new file mode 100644 index 0000000000000000000000000000000000000000..919d4507951966610867b410060bd43f66c14e45 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checker64.jpg differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png new file mode 100644 index 0000000000000000000000000000000000000000..b757ac3cbef6a6b3c6ca9f65908a73947beb0c28 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..9387a300dae695532dfa5162d94a7f1fe943fa08 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/checkerboard_transparent.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png new file mode 100644 index 0000000000000000000000000000000000000000..7ce390d4c9095e4e755586e7e52917afda0a83da Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/clock-history.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png new file mode 100644 index 0000000000000000000000000000000000000000..793964127b0d6213342eea899523870a21b4b576 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cookies.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png new file mode 100644 index 0000000000000000000000000000000000000000..be8bdd2731127872a12ac1e92a6e507547ace35a Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle (2).png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b37afb85093a9b9578adf66d543559e9578143 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/cross-circle.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png new file mode 100644 index 0000000000000000000000000000000000000000..a976964fc543a7506a0b232413d3114d1799ca44 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/downloads.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3534ee4454471c972b00610f590acd76eddaf1 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel--minus.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png new file mode 100644 index 0000000000000000000000000000000000000000..1f69604528f29ca95e3b124de2849067797d839f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/funnel.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png new file mode 100644 index 0000000000000000000000000000000000000000..08857a3ae6db9adbcfbc3ea7de73a6f693460d4f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/geolocation.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png new file mode 100644 index 0000000000000000000000000000000000000000..26c97d488809de8088c6896490014015a5beb5f0 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-search.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..78f1f97dcc3026b5cbe20a356df6eb7ea56d97ff Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/gps-trackpoint.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png new file mode 100644 index 0000000000000000000000000000000000000000..48f4d5f3240f031daf482f300bbd50194daced1e Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/history.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png new file mode 100644 index 0000000000000000000000000000000000000000..fc3c393caa3bc4371d12d0c67ffd6d333ecf1d8e Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/image.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e499782c6e78fc6b23d743f4b29baa398699ce Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/info-icon-16.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png new file mode 100644 index 0000000000000000000000000000000000000000..5d353a191098dee1e1e8f4bdf99c7f4bccb0f292 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/information.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png new file mode 100644 index 0000000000000000000000000000000000000000..f865c5787fd53ae4809bdecc318ed273d8c60e31 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-left.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png new file mode 100644 index 0000000000000000000000000000000000000000..52972052c5e9f68880c12353b5008eab9df665d8 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual-equal.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0137c5b4e66a12edf613daf6578ec9ee076d2c Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-actual.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..e29d97104517b832e17261a2c768784198ea2f5e Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-fit.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png new file mode 100644 index 0000000000000000000000000000000000000000..53bb613f8cebaf171fde016a9c5fca4bfabc303e Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in-green.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..281af0cde172dac2a38734350dfd35d840881fde Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-in.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png new file mode 100644 index 0000000000000000000000000000000000000000..04446b048ca454541a9bc74b5f34200a35afe041 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out-red.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png new file mode 100644 index 0000000000000000000000000000000000000000..c11249f5286caa237c1128a6e500fc0e6e9619fb Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom-out.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..a705d33d9231f2ca64f90a4505422f45bec80935 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier-zoom.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3d97f75e9cde9c143980d89272fe61fc2d64ee Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png new file mode 100644 index 0000000000000000000000000000000000000000..af4fe07477243b9b2099899d1ef47b8e3fd87b09 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_in.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png new file mode 100644 index 0000000000000000000000000000000000000000..81f28199ac1c979f440f0586e6e0da48672e74a4 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/magnifier_zoom_out.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..a919ae3e50664bd01a78732d9a07e223608b3aa4 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/mail-icon-16.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..74c3b9143461531ce53eaa91a394309139cd10d1 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/marker.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png new file mode 100644 index 0000000000000000000000000000000000000000..6223516e3e122204aef38296213d0a4d2b382a36 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/message.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png new file mode 100644 index 0000000000000000000000000000000000000000..b8872bbcf9f54f7afd563bf0e43fffb387cea35f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/programs.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png new file mode 100644 index 0000000000000000000000000000000000000000..7a368df09f18b1bfced6e4d179b9c0053f0b2748 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/prohibition.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png new file mode 100644 index 0000000000000000000000000000000000000000..b6d2a9c65cbd9e63c8858cabe2beea80ec8ae424 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/recent_docs.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png new file mode 100644 index 0000000000000000000000000000000000000000..c6cb1709304c3e81d1f55dfd30aa38f1830cbd4f Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/searchquery.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d7a96be3f2282a62e3c0733bac89c7f6de7b4a Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/tick.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..a3fbddf88b7661e9ee2a434ad4152cc724db24c5 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/timeline_marker.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png new file mode 100644 index 0000000000000000000000000000000000000000..e49540dccced741b99d90ebb10a959769dea6439 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/usb_devices.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png b/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png new file mode 100644 index 0000000000000000000000000000000000000000..ac5957ad62d73408cd754a27453b4ce601a2b042 Binary files /dev/null and b/Timeline/src/org/sleuthkit/autopsy/timeline/images/web-file.png differ diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/index.css b/Timeline/src/org/sleuthkit/autopsy/timeline/index.css new file mode 100644 index 0000000000000000000000000000000000000000..fd6e48afd9746bd5b8089cbad0908bc59a389067 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/index.css @@ -0,0 +1,16 @@ +body {margin: 0px; padding: 0px; background: #FFFFFF; font: 13px/20px Arial, Helvetica, sans-serif; color: #535353;} +#content {padding: 30px;} +#header {width:100%; padding: 10px; line-height: 25px; background: #07A; color: #FFF; font-size: 20px;} +h1 {font-size: 20px; font-weight: normal; color: #07A; padding: 0 0 7px 0; margin-top: 25px; border-bottom: 1px solid #D6D6D6;} +h2 {font-size: 20px; font-weight: bolder; color: #07A;} +h3 {font-size: 16px; color: #07A;} +h4 {background: #07A; color: #FFF; font-size: 16px; margin: 0 0 0 25px; padding: 0; padding-left: 15px;} +ul.nav {list-style-type: none; line-height: 35px; padding: 0px; margin-left: 15px;} +ul li a {font-size: 14px; color: #444; text-decoration: none; padding-left: 25px;} +ul li a:hover {text-decoration: underline;} +p {margin: 0 0 20px 0;} +table {max-width: 100%; min-width: 700px; padding: 0; margin: 0; border-collapse: collapse; border-bottom: 2px solid #e5e5e5;} +.keyword_list table {width: 100%; margin: 0 0 25px 25px; border-bottom: 2px solid #dedede;} +table th {display: table-cell; text-align: left; padding: 8px 16px; background: #e5e5e5; color: #777; font-size: 11px; text-shadow: #e9f9fd 0 1px 0; border-top: 1px solid #dedede; border-bottom: 2px solid #e5e5e5;} +table td {display: table-cell; padding: 8px 16px; font: 13px/20px Arial, Helvetica, sans-serif; max-width: 500px; min-width: 125px; word-break: break-all; overflow: auto;} +table tr:nth-child(even) td {background: #f3f3f3;} \ No newline at end of file diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt b/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt new file mode 100644 index 0000000000000000000000000000000000000000..df5d98b91af0bc83731262657fe918d7b28586a5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/license-advanced_timeline.txt @@ -0,0 +1,18 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ \ No newline at end of file diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java new file mode 100644 index 0000000000000000000000000000000000000000..aabac6e1bb5b91e0c1410e490b063e2e4c932197 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java @@ -0,0 +1,412 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyListProperty; +import javafx.beans.property.ReadOnlyListWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.Chart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; +import javafx.scene.effect.Effect; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javax.annotation.concurrent.Immutable; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; + +/** Abstract base class for {@link Chart} based {@link TimeLineView}s used in + * the main visualization area. + * + * @param <X> the type of data plotted along the x axis + * @param <Y> the type of data plotted along the y axis + * @param <N> the type of nodes used to represent data items + * @param <C> the type of the {@link XYChart<X,Y>} this class uses to plot the + * data. + * + * TODO: this is becoming (too?) closely tied to the notion that their is a + * {@link XYChart} doing the rendering. Is this a good idea? -jm + * TODO: pull up common history context menu items out of derived classes? -jm + */ +public abstract class AbstractVisualization<X, Y, N extends Node, C extends XYChart<X, Y> & TimeLineChart<X>> extends BorderPane implements TimeLineView { + + protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); + + protected final ObservableList<BarChart.Series<X, Y>> dataSets = FXCollections.<BarChart.Series<X, Y>>observableArrayList(); + + protected C chart; + + //// replacement axis label componenets + private final Pane leafPane; // container for the leaf lables in the declutterd axis + + private final Pane branchPane;// container for the branch lables in the declutterd axis + + protected final Region spacer; + + /** task used to reload the content of this visualization */ + private Task<Boolean> updateTask; + + protected TimeLineController controller; + + protected FilteredEventsModel filteredEvents; + + protected ReadOnlyListWrapper<N> selectedNodes = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); + + public ReadOnlyListProperty<N> getSelectedNodes() { + return selectedNodes.getReadOnlyProperty(); + } + + /** list of {@link Node}s to insert into the toolbar. This should be + * set in an implementations constructor. */ + protected List<Node> settingsNodes; + + /** @return the list of nodes containing settings widgets to insert into + * this visualization's header */ + protected List<Node> getSettingsNodes() { + return settingsNodes; + } + + /** @param value a value along this visualization's x axis + * + * @return true if the tick label for the given value should be bold ( has + * relevant data), + * false* otherwise */ + protected abstract Boolean isTickBold(X value); + + /** apply this visualization's 'selection effect' to the given node + * + * @param node the node to apply the 'effect' to + * @param applied true if the effect should be applied, false if the effect + * should + */ + protected abstract void applySelectionEffect(N node, Boolean applied); + + /** @return a task to execute on a background thread to reload this + * visualization with different data. */ + protected abstract Task<Boolean> getUpdateTask(); + + /** @return return the {@link Effect} applied to 'selected nodes' in this + * visualization, or null if selection is visualized via another + * mechanism */ + protected abstract Effect getSelectionEffect(); + + /** @param tickValue + * + * @return a String to use for a tick mark label given a tick value + */ + protected abstract String getTickMarkLabel(X tickValue); + + /** the spacing (in pixels) between tick marks of the horizontal + * axis. This will be used to layout the decluttered replacement labels. + * + * @return the spacing in pixels between tick marks of the horizontal + * axis */ + protected abstract double getTickSpacing(); + + /** @return the horizontal axis used by this Visualization's chart */ + protected abstract Axis<X> getXAxis(); + + /** @return the vertical axis used by this Visualization's chart */ + protected abstract Axis<Y> getYAxis(); + + /** * update this visualization based on current state of zoom / + * filters.Primarily this invokes the background {@link Task} returned + * by {@link #getUpdateTask()} which derived classes must implement. */ + synchronized public void update() { + if (updateTask != null) { + updateTask.cancel(true); + updateTask = null; + } + updateTask = getUpdateTask(); + updateTask.stateProperty().addListener((Observable observable) -> { + switch (updateTask.getState()) { + case CANCELLED: + case FAILED: + case READY: + case RUNNING: + case SCHEDULED: + break; + case SUCCEEDED: + try { + this.hasEvents.set(updateTask.get()); + } catch (InterruptedException | ExecutionException ex) { + Exceptions.printStackTrace(ex); + } + break; + } + }); + controller.monitorTask(updateTask); + } + + synchronized public void dispose() { + if (updateTask != null) { + updateTask.cancel(true); + } + this.filteredEvents.getRequestedZoomParamters().removeListener(invalidationListener); + invalidationListener = null; + } + + protected AbstractVisualization(Pane partPane, Pane contextPane, Region spacer) { + this.leafPane = partPane; + this.branchPane = contextPane; + this.spacer = spacer; + selectedNodes.addListener((ListChangeListener.Change<? extends N> c) -> { + while (c.next()) { + c.getRemoved().forEach((N n) -> { + applySelectionEffect(n, false); + }); + + c.getAddedSubList().forEach((N c1) -> { + applySelectionEffect(c1, true); + }); + } + }); + } + + @Override + synchronized public void setController(TimeLineController controller) { + this.controller = controller; + chart.setController(controller); + + setModel(controller.getEventsModel()); + TimeLineController.getTimeZone().addListener((Observable observable) -> { + update(); + }); + } + + @Override + synchronized public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + + this.filteredEvents.getRequestedZoomParamters().addListener(invalidationListener); + update(); + } + + protected InvalidationListener invalidationListener = (Observable observable) -> { + update(); + }; + + /** iterate through the list of tick-marks building a two level structure of + * replacement tick marl labels. (Visually) upper level has most + * detailed/highest frequency part of date/time. Second level has rest of + * date/time grouped by unchanging part. + * eg: + * + * + * october-30_october-31_september-01_september-02_september-03 + * + * becomes + * + * _________30_________31___________01___________02___________03 + * + * _________october___________|_____________september___________ + * + * + * NOTE: This method should only be invoked on the JFX thread + */ + public synchronized void layoutDateLabels() { + + //clear old labels + branchPane.getChildren().clear(); + leafPane.getChildren().clear(); + //since the tickmarks aren't necessarily in value/position order, + //make a clone of the list sorted by position along axis + ObservableList<Axis.TickMark<X>> tickMarks = FXCollections.observableArrayList(getXAxis().getTickMarks()); + tickMarks.sort((Axis.TickMark<X> t, Axis.TickMark<X> t1) -> Double.compare(t.getPosition(), t1.getPosition())); + + if (tickMarks.isEmpty() == false) { + //get the spacing between ticks in the underlying axis + double spacing = getTickSpacing(); + + //initialize values from first tick + TwoPartDateTime dateTime = new TwoPartDateTime(getTickMarkLabel(tickMarks.get(0).getValue())); + String lastSeenBranchLabel = dateTime.branch; + //cumulative width of the current branch label + + //x-positions (pixels) of the current branch and leaf labels + double leafLabelX = 0; + + if (dateTime.branch.equals("")) { + //if there is only one part to the date (ie only year), just add a label for each tick + for (Axis.TickMark<X> t : tickMarks) { + assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf, + spacing, + leafLabelX, + isTickBold(t.getValue()) + ); + + leafLabelX += spacing; //increment x + } + } else { + //there are two parts so ... + //initialize additional state + double branchLabelX = 0; + double branchLabelWidth = 0; + + for (Axis.TickMark<X> t : tickMarks) { //for each tick + + //split the label into a TwoPartDateTime + dateTime = new TwoPartDateTime(getTickMarkLabel(t.getValue())); + + //if we are still on the same branch + if (lastSeenBranchLabel.equals(dateTime.branch)) { + //increment branch width + branchLabelWidth += spacing; + } else {// we are on to a new branch, so ... + assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX); + //and then update label, x-pos, and width + lastSeenBranchLabel = dateTime.branch; + branchLabelX += branchLabelWidth; + branchLabelWidth = spacing; + } + //add the label for the leaf (highest frequency part) + assignLeafLabel(dateTime.leaf, spacing, leafLabelX, isTickBold(t.getValue())); + + //increment leaf position + leafLabelX += spacing; + } + //we have reached end so add branch label for current branch + assignBranchLabel(lastSeenBranchLabel, branchLabelWidth, branchLabelX); + } + } + //request layout since we have modified scene graph structure + requestParentLayout(); + } + + protected void setChartClickHandler() { + chart.addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> { + if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) { + selectedNodes.clear(); + } + }); + } + + /** add a {@link Text} node to the leaf container for the decluttered axis + * labels + * + * @param labelText the string to add + * @param labelWidth the width of the space available for the text + * @param labelX the horizontal position in the partPane of the text + * @param bold true if the text should be bold, false otherwise + */ + private synchronized void assignLeafLabel(String labelText, double labelWidth, double labelX, boolean bold) { + + Text label = new Text(" " + labelText + " "); + label.setTextAlignment(TextAlignment.CENTER); + label.setFont(Font.font(null, bold ? FontWeight.BOLD : FontWeight.NORMAL, 10)); + //position label accounting for width + label.relocate(labelX + labelWidth / 2 - label.getBoundsInLocal().getWidth() / 2, 0); + label.autosize(); + + if (leafPane.getChildren().isEmpty()) { + //just add first label + leafPane.getChildren().add(label); + } else { + //otherwise don't actually add the label if it would intersect with previous label + final Text lastLabel = (Text) leafPane.getChildren().get(leafPane.getChildren().size() - 1); + + if (!lastLabel.getBoundsInParent().intersects(label.getBoundsInParent())) { + leafPane.getChildren().add(label); + } + } + } + + /** add a {@link Label} node to the branch container for the decluttered + * axis labels + * + * @param labelText the string to add + * @param labelWidth the width of the space to use for the label + * @param labelX the horizontal position in the partPane of the text + */ + private synchronized void assignBranchLabel(String labelText, double labelWidth, double labelX) { + + Label label = new Label(labelText); + label.setAlignment(Pos.CENTER); + label.setTextAlignment(TextAlignment.CENTER); + label.setFont(Font.font(10)); + //use a leading ellipse since that is the lowest frequency part, + //and can be infered more easily from other surrounding labels + label.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); + //force size + label.setMinWidth(labelWidth); + label.setPrefWidth(labelWidth); + label.setMaxWidth(labelWidth); + label.relocate(labelX, 0); + + if (labelX == 0) { // first label has no border + label.setStyle("-fx-border-width: 0 0 0 0 ; -fx-border-color:black;"); + } else { // subsequent labels have border on left to create dividers + label.setStyle("-fx-border-width: 0 0 0 1; -fx-border-color:black;"); + } + + branchPane.getChildren().add(label); + } + + /** A simple data object used to represent a partial date as up to two + * parts. A low frequency part (branch) containing all but the most + * specific element, and a highest frequency part (leaf) containing the most + * specific element. The branch and leaf names come from thinking of the + * space of all date times as a tree with higher frequency information + * further from the root. If there is only one part, it will be in the + * branch and the leaf will equal an empty string */ + @Immutable + private static final class TwoPartDateTime { + + /** the low frequency part of a date/time eg 2001-May-4 */ + private final String branch; + + /** the highest frequency part of a date/time eg 14 (2pm) */ + private final String leaf; + + TwoPartDateTime(String dateString) { + //find index of separator to spit on + int splitIndex = StringUtils.lastIndexOfAny(dateString, " ", "-", ":"); + if (splitIndex < 0) { // there is only one part + leaf = dateString; + branch = ""; + } else { //split at index + leaf = StringUtils.substring(dateString, splitIndex + 1); + branch = StringUtils.substring(dateString, 0, splitIndex); + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties new file mode 100644 index 0000000000000000000000000000000000000000..3b123bc3b076322166ddd3f653556e2dd7eca4ce --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties @@ -0,0 +1,21 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +AggregateEvent.diferentDescriptions=aggregate events are not compatible they have different descriptions + +AdvTimeline.node.root=Root \ No newline at end of file diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml new file mode 100644 index 0000000000000000000000000000000000000000..926f634900e93fa3a20b8cb8b0055f015d1acb3c --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/NoEventsDialog.fxml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<fx:root collapsible="false" contentDisplay="RIGHT" maxHeight="-Infinity" maxWidth="-Infinity" text="No Visible Events" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<graphic><HBox> +<children><Region prefWidth="150.0" /><Button fx:id="dismissButton" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" minHeight="16.0" minWidth="16.0" mnemonicParsing="false" prefHeight="16.0" prefWidth="16.0"> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../images/cross-circle.png" /> +</image></ImageView> +</graphic></Button> +</children></HBox> +</graphic> +<content> +<VBox alignment="CENTER_RIGHT" fillWidth="false" prefHeight="130.0" prefWidth="259.0" spacing="5.0"> +<children><Label graphicTextGap="10.0" text="There are no events visible with the current zoom / filter settings. " wrapText="true" GridPane.columnIndex="1" VBox.vgrow="ALWAYS"> +<GridPane.margin> +<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> +</GridPane.margin> +<font> +<Font size="14.0" /> +</font> +<VBox.margin> +<Insets bottom="10.0" /> +</VBox.margin> +<graphic><ImageView fitHeight="32.0" fitWidth="32.0" pickOnBounds="true" preserveRatio="true" GridPane.halignment="CENTER" GridPane.valignment="CENTER"> +<image> +<Image url="@../images/information.png" /> +</image> +<GridPane.margin> +<Insets /> +</GridPane.margin> +<HBox.margin> +<Insets /> +</HBox.margin></ImageView> +</graphic></Label><Button fx:id="zoomButton" alignment="BASELINE_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Zoom to events" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER"> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../images/magnifier-zoom-out-red.png" /> +</image></ImageView> +</graphic> +<GridPane.margin> +<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> +</GridPane.margin> +<VBox.margin> +<Insets /> +</VBox.margin></Button><Button fx:id="resetFiltersButton" alignment="BASELINE_LEFT" maxWidth="1.7976931348623157E308" mnemonicParsing="false" prefHeight="25.0" prefWidth="120.0" text="Reset all filters" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER"> +<graphic> +<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../images/arrow-circle-double-135.png" /> +</image> +</ImageView> +</graphic> +<GridPane.margin> +<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> +</GridPane.margin> +<VBox.margin> +<Insets /> +</VBox.margin> +</Button> +</children> +<HBox.margin> +<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> +</HBox.margin> +</VBox> +</content> +</fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml new file mode 100644 index 0000000000000000000000000000000000000000..e5bfae1b99b64da81263cd18922fbf4600bf7e4d --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.fxml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" type="ToolBar" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<items><Label fx:id="refreshLabel" text="New events may be available, re-open the timeline to refresh."> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../images/info-icon-16.png" /> +</image></ImageView> +</graphic></Label><Separator orientation="VERTICAL" /><Region fx:id="spacer" /><Separator orientation="VERTICAL" /><Label fx:id="taskLabel" contentDisplay="RIGHT" text="Label"> +<graphic><StackPane> +<children><ProgressBar fx:id="progressBar" progress="0.0" /><Label fx:id="messageLabel" text="Label" /> +</children></StackPane> +</graphic></Label> +</items> +</fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java new file mode 100644 index 0000000000000000000000000000000000000000..0a80d3146dc91ee7e15f010fd31fadc067815084 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/StatusBar.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineUI; + +/** + * simple status bar that only shows one possible message determined by + * {@link TimeLineController#newEventsFlag} + */ +public class StatusBar extends ToolBar implements TimeLineUI { + + private TimeLineController controller; + + @FXML + private Label refreshLabel; + + @FXML + private ProgressBar progressBar; + + @FXML + private Region spacer; + + @FXML + private Label taskLabel; + + @FXML + private Label messageLabel; + + public StatusBar() { + FXMLConstructor.construct(this, "StatusBar.fxml"); + } + + @FXML + void initialize() { + assert refreshLabel != null : "fx:id=\"refreshLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert progressBar != null : "fx:id=\"progressBar\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert spacer != null : "fx:id=\"spacer\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert taskLabel != null : "fx:id=\"taskLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert messageLabel != null : "fx:id=\"messageLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + refreshLabel.setVisible(false); + taskLabel.setVisible(false); + HBox.setHgrow(spacer, Priority.ALWAYS); + } + + @Override + public void setController(TimeLineController controller) { + this.controller = controller; + refreshLabel.visibleProperty().bind(this.controller.getNewEventsFlag()); + taskLabel.textProperty().bind(this.controller.getTaskTitle()); + messageLabel.textProperty().bind(this.controller.getMessage()); + progressBar.progressProperty().bind(this.controller.getProgress()); + taskLabel.visibleProperty().bind(this.controller.getTasks().emptyProperty().not()); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java new file mode 100644 index 0000000000000000000000000000000000000000..70e76b2fe017b61bf96fbcd60c2e629383b142e9 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineChart.java @@ -0,0 +1,275 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import javafx.event.EventHandler; +import javafx.geometry.Point2D; +import javafx.scene.Cursor; +import javafx.scene.chart.Axis; +import javafx.scene.chart.Chart; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; + +/** Interface for TimeLineViews that are 'charts'. + * + * @param <X> the type of values along the horizontal axis */ +public interface TimeLineChart<X> extends TimeLineView { + + IntervalSelector<? extends X> getIntervalSelector(); + + void setIntervalSelector(IntervalSelector<? extends X> newIntervalSelector); + + /** derived classes should implement this so as to supply an appropriate + * subclass of {@link IntervalSelector} + * + * @param x the initial x position of the new interval selector + * @param axis the axis the new interval selector will be over + * + * @return a new interval selector + */ + IntervalSelector<X> newIntervalSelector(double x, Axis<X> axis); + + /** clear any references to previous interval selectors , including removing + * the interval selector from the ui / scene-graph */ + void clearIntervalSelector(); + + /** + * drag handler class used by {@link TimeLineChart}s to create + * {@link IntervalSelector}s + * + * @param <X> the type of values along the horizontal axis + * @param <Y> the type of chart this is a drag handler for + */ + class ChartDragHandler<X, Y extends Chart & TimeLineChart<X>> implements EventHandler<MouseEvent> { + + private final Y chart; + + private final Axis<X> dateAxis; + + private double startX; //hanlder mainstains position of drag start + + public ChartDragHandler(Y chart, Axis<X> dateAxis) { + this.chart = chart; + this.dateAxis = dateAxis; + } + + @Override + public void handle(MouseEvent t) { + if (t.getButton() == MouseButton.SECONDARY) { + + if (t.getEventType() == MouseEvent.MOUSE_PRESSED) { + //caputure x-position, incase we are repositioning existing selector + startX = t.getX(); + chart.setCursor(Cursor.E_RESIZE); + } else if (t.getEventType() == MouseEvent.MOUSE_DRAGGED) { + if (chart.getIntervalSelector() == null) { + //make new interval selector + chart.setIntervalSelector(chart.newIntervalSelector(t.getX(), dateAxis)); + chart.getIntervalSelector().heightProperty().bind(chart.heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty()))); + chart.getIntervalSelector().addEventHandler(MouseEvent.MOUSE_CLICKED, (MouseEvent event) -> { + if (event.getButton() == MouseButton.SECONDARY) { + chart.clearIntervalSelector(); + event.consume(); + } + }); + startX = t.getX(); + } else { + //resize/position existing selector + if (t.getX() > startX) { + chart.getIntervalSelector().setX(startX); + chart.getIntervalSelector().setWidth(t.getX() - startX); + } else { + chart.getIntervalSelector().setX(t.getX()); + chart.getIntervalSelector().setWidth(startX - t.getX()); + } + } + } else if (t.getEventType() == MouseEvent.MOUSE_RELEASED) { + chart.setCursor(Cursor.DEFAULT); + } + t.consume(); + } + } + } + + /** Visually represents a 'selected' time range, and allows mouse + * interactions with it. + * + * @param <X> the type of values along the x axis this is a selector for + * + * This abstract class requires concrete implementations to implement + * hook methods to handle formating and date 'lookup' of the + * generic x-axis type */ + static abstract class IntervalSelector<X> extends Rectangle { + + private static final double STROKE_WIDTH = 3; + + private static final double HALF_STROKE = STROKE_WIDTH / 2; + + /** the Axis this is a selector over */ + private final Axis<X> dateAxis; + + protected Tooltip tooltip; + + /////////drag state + private DragPosition dragPosition; + + private double startLeft; + + private double startX; + + private double startWidth; + /////////end drag state + + /** + * + * @param x the initial x position of this selector + * @param height the initial height of this selector + * @param axis the {@link Axis<X>} this is a selector over + * @param controller the controller to invoke when this selector is + * double clicked + */ + public IntervalSelector(double x, double height, Axis<X> axis, TimeLineController controller) { + super(x, 0, x, height); + dateAxis = axis; + setStroke(Color.BLUE); + setStrokeWidth(STROKE_WIDTH); + setFill(Color.BLUE.deriveColor(0, 1, 1, 0.5)); + setOpacity(0.5); + widthProperty().addListener(o -> { + setTooltip(); + }); + xProperty().addListener(o -> { + setTooltip(); + }); + setTooltip(); + + setOnMouseMoved((MouseEvent event) -> { + Point2D localMouse = sceneToLocal(new Point2D(event.getSceneX(), event.getSceneY())); + final double diffX = getX() - localMouse.getX(); + if (Math.abs(diffX) <= HALF_STROKE || Math.abs(diffX + getWidth()) <= HALF_STROKE) { + setCursor(Cursor.E_RESIZE); + } else { + setCursor(Cursor.HAND); + } + }); + setOnMousePressed((MouseEvent event) -> { + Point2D localMouse = sceneToLocal(new Point2D(event.getSceneX(), event.getSceneY())); + final double diffX = getX() - localMouse.getX(); + startX = event.getX(); + startWidth = getWidth(); + startLeft = getX(); + if (Math.abs(diffX) <= HALF_STROKE) { + dragPosition = IntervalSelector.DragPosition.LEFT; + } else if (Math.abs(diffX + getWidth()) <= HALF_STROKE) { + dragPosition = IntervalSelector.DragPosition.RIGHT; + } else { + dragPosition = IntervalSelector.DragPosition.CENTER; + } + }); + setOnMouseDragged((MouseEvent event) -> { + double dX = event.getX() - startX; + switch (dragPosition) { + case CENTER: + setX(startLeft + dX); + break; + case LEFT: + setX(startLeft + dX); + setWidth(startWidth - dX); + break; + case RIGHT: + setWidth(startWidth + dX); + break; + } + }); + //have to add handler rather than use convenience methods so that charts can listen for dismisal click + addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { + + public void handle(MouseEvent event) { + if (event.getClickCount() >= 2) { + //convert to DateTimes, using max/min if null(off axis) + DateTime start = parseDateTime(getSpanStart()); + DateTime end = parseDateTime(getSpanEnd()); + + Interval i = adjustInterval(start.isBefore(end) ? new Interval(start, end) : new Interval(end, start)); + + controller.pushTimeRange(i); + } + } + }); + } + + /** + * + * @param i the interval represented by this selector + * + * @return a modified version of {@code i} adjusted to suite the needs + * of the concrete implementation + */ + protected abstract Interval adjustInterval(Interval i); + + /** format a string representation of the given x-axis value to use in + * the tooltip + * + * @param date a x-axis value of type X + * + * @return a string representation of the given x-axis value */ + protected abstract String formatSpan(final X date); + + /** parse an x-axis value to a {@link DateTime} + * + * @param date a x-axis value of type X + * + * @return a {@link DateTime} corresponding to the given x-axis value */ + protected abstract DateTime parseDateTime(X date); + + private void setTooltip() { + final X start = getSpanStart(); + final X end = getSpanEnd(); + Tooltip.uninstall(this, tooltip); + tooltip = new Tooltip("Double-click to zoom into range:\n" + formatSpan(start) + " to " + formatSpan(end) + "\nRight-click to clear."); + Tooltip.install(this, tooltip); + } + + /** @return the value along the x-axis corresponding to the left edge of + * the selector */ + public X getSpanEnd() { + return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getBoundsInParent().getMaxX(), 0).getX()); + } + + /** @return the value along the x-axis corresponding to the right edge + * of the selector */ + public X getSpanStart() { + return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getBoundsInParent().getMinX(), 0).getX()); + } + + /** enum to represent whether the drag is a left/right-edge modification + * or a horizontal slide triggered by dragging the center */ + private enum DragPosition { + + LEFT, CENTER, RIGHT + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java new file mode 100644 index 0000000000000000000000000000000000000000..2700f86aace31cf9ba4b563c693bda4ba3a1fd39 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java @@ -0,0 +1,120 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import java.util.HashSet; +import java.util.Set; +import javafx.beans.Observable; +import javax.swing.SwingUtilities; +import org.joda.time.format.DateTimeFormatter; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.explorernodes.EventRootNode; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent; +import org.sleuthkit.autopsy.corecomponents.DataResultPanel; + +/** + * Since it was too hard to derive from {@link DataResultPanel}, this class + * implements {@link TimeLineView}, listens to the events/state of a the + * assigned {@link FilteredEventsModel} and acts appropriately on its + * {@link DataResultPanel}. That is, this class acts as a sort of + * bridge/adapter between a FilteredEventsModel instance and a + * DataResultPanel instance. + */ +public class TimeLineResultView implements TimeLineView { + + /** the {@link DataResultPanel} that is the real view proxied by this + * class */ + private final DataResultPanel dataResultPanel; + + private TimeLineController controller; + + private FilteredEventsModel filteredEvents; + + private Set<Long> selectedEventIDs = new HashSet<>(); + + public DataResultPanel getDataResultPanel() { + return dataResultPanel; + } + + public TimeLineResultView(DataContent dataContent) { + dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, dataContent); + } + + /** Set the Controller for this class. Also sets the model provided by the + * controller as the model for this view. + * + * @param controller + */ + @Override + public void setController(TimeLineController controller) { + this.controller = controller; + + //set up listeners on relevant properties + TimeLineController.getTimeZone().addListener((Observable observable) -> { + dataResultPanel.setPath(getSummaryString()); + }); + + controller.getSelectedEventIDs().addListener((Observable o) -> { + refresh(); + }); + + setModel(controller.getEventsModel()); + } + + /** Set the Model for this View + * + * @param filteredEvents */ + @Override + synchronized public void setModel(final FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + } + + /** @return a String representation of all the Events displayed */ + private String getSummaryString() { + if (controller.getSelectedTimeRange().get() != null) { + final DateTimeFormatter zonedFormatter = TimeLineController.getZonedFormatter(); + return controller.getSelectedTimeRange().get().getStart().withZone(TimeLineController.getJodaTimeZone()).toString(zonedFormatter) + + " to " + + controller.getSelectedTimeRange().get().getEnd().withZone(TimeLineController.getJodaTimeZone()).toString(zonedFormatter); + } + return ""; + } + + /** refresh this view with the events selected in the controller */ + public final void refresh() { + + Set<Long> newSelectedEventIDs = new HashSet<>(controller.getSelectedEventIDs()); + if (selectedEventIDs.equals(newSelectedEventIDs) == false) { + selectedEventIDs = newSelectedEventIDs; + final EventRootNode root = new EventRootNode( + NbBundle.getMessage(this.getClass(), "AdvTimeline.node.root"), selectedEventIDs, + filteredEvents); + + //this must be in edt or exception is thrown + SwingUtilities.invokeLater(() -> { + dataResultPanel.setPath(getSummaryString()); + dataResultPanel.setNode(root); + }); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ae444c2817d38250e959c8900cce86c0076d32fd --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.fxml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + +<fx:root alignment="TOP_LEFT" animated="false" collapsible="false" contentDisplay="RIGHT" minHeight="-Infinity" minWidth="-Infinity" text="Display Times In:" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<graphic> +<HBox spacing="5.0"> +<children><RadioButton fx:id="localRadio" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" selected="true" text="Local Time Zone"> +<HBox.margin> +<Insets /> +</HBox.margin> +<toggleGroup> +<ToggleGroup fx:id="localOtherGroup" /> +</toggleGroup></RadioButton><RadioButton fx:id="otherRadio" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" text="GMT / UTC" toggleGroup="$localOtherGroup"> +<HBox.margin> +<Insets /> +</HBox.margin></RadioButton> +</children> +<padding> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</padding> +</HBox> +</graphic> +</fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java new file mode 100644 index 0000000000000000000000000000000000000000..6882781b64d57acc0a728bd7cbd87cc7b1b0e3f9 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/TimeZonePanel.java @@ -0,0 +1,70 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import java.time.ZoneOffset; +import java.util.Date; +import java.util.TimeZone; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * FXML Controller class for timezone picker ui + * + */ +public class TimeZonePanel extends TitledPane { + + @FXML + private RadioButton localRadio; + + @FXML + private RadioButton otherRadio; + + @FXML + private ToggleGroup localOtherGroup; + + static private String getTimeZoneString(final TimeZone timeZone) { + final String id = ZoneOffset.ofTotalSeconds(timeZone.getOffset(System.currentTimeMillis()) / 1000).getId(); + final String timeZoneString = "(GMT" + ("Z".equals(id) ? "+00:00" : id) + ") " + timeZone.getID() + " [" + timeZone.getDisplayName(timeZone.observesDaylightTime() && timeZone.inDaylightTime(new Date()), TimeZone.SHORT) + "]"; + return timeZoneString; + } + + @FXML + public void initialize() { + +// localRadio.setText("Local Time Zone: " + getTimeZoneString(TimeZone.getDefault())); + localOtherGroup.selectedToggleProperty().addListener((ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) -> { + if (newValue == localRadio) { + TimeLineController.setTimeZone(TimeZone.getDefault()); + } else { + TimeLineController.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); + } + }); + } + + public TimeZonePanel() { + FXMLConstructor.construct(this, "TimeZonePanel.fxml"); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..7aa00b91b4f8adcf4f91e593427c552f96c0bcef --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import jfxtras.scene.control.*?> +<?import org.controlsfx.control.*?> + +<fx:root prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <top> + <ToolBar fx:id="toolBar" prefWidth="200.0" BorderPane.alignment="CENTER"> + <items> + <HBox alignment="CENTER" BorderPane.alignment="CENTER"> + <children> + <Label text="Mode:"> + <HBox.margin> + <Insets right="5.0" /> + </HBox.margin> + </Label> + + <org.controlsfx.control.SegmentedButton maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"> + <buttons> + <ToggleButton fx:id="countsToggle" alignment="TOP_LEFT" mnemonicParsing="false" selected="true" text="Counts"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/chart_bar.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + <ToggleButton fx:id="detailsToggle" alignment="CENTER_RIGHT" layoutX="74.0" mnemonicParsing="false" selected="false" text="Details"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" rotate="0.0" smooth="true" style="-fx-background-color:white;" x="2.0" y="1.0"> + <image> + <Image url="@../images/20140521121247760_easyicon_net_32_colorized.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + </buttons> + + </org.controlsfx.control.SegmentedButton> + </children> + <padding> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </padding> + <BorderPane.margin> + <Insets left="10.0" /> + </BorderPane.margin> + </HBox> + <Separator orientation="VERTICAL" /> + <Button fx:id="snapShotButton" mnemonicParsing="false" text="snapshot"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/image.png" /> + </image> + </ImageView> + </graphic> + </Button> + <Separator orientation="VERTICAL" /> + </items> + </ToolBar> + </top> + <bottom> + <VBox maxHeight="-Infinity"> + <children> + <HBox fillHeight="false" BorderPane.alignment="CENTER"> + <children> + <Region fx:id="spacer" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="5.0" prefWidth="20.0" /> + <VBox fillWidth="false"> + <children> + <Pane fx:id="partPane" /> + <Pane fx:id="contextPane" /> + </children> + </VBox> + </children> + </HBox> + <Separator /> + <Separator /> + <StackPane fx:id="rangeHistogramStack" maxHeight="-Infinity" BorderPane.alignment="CENTER"> + <children> + <HBox fx:id="histogramBox" alignment="BOTTOM_LEFT" fillHeight="false" maxHeight="-Infinity" minHeight="-Infinity" prefHeight="32.0" StackPane.alignment="BOTTOM_CENTER" /> + </children> + </StackPane> + <Separator /> + <ToolBar> + <items> + <Label contentDisplay="RIGHT" minWidth="-Infinity" text="Start:"> + <graphic> + <LocalDateTimeTextField fx:id="startPicker" minWidth="200.0" prefWidth="200.0"> + <padding> + <Insets top="3.0" /> + </padding> + </LocalDateTimeTextField> + </graphic> + </Label> + <Separator fx:id="leftSeperator" halignment="LEFT" maxWidth="1.7976931348623157E308" minWidth="-Infinity" orientation="VERTICAL" HBox.hgrow="ALWAYS"> + <HBox.margin> + <Insets left="10.0" right="10.0" /> + </HBox.margin> + </Separator> + <HBox> + <children> + <Button fx:id="zoomOutButton" mnemonicParsing="false"> + <graphic> + <ImageView pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/magnifier-zoom-out-red.png" /> + </image> + </ImageView> + </graphic> + <HBox.margin> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </HBox.margin> + </Button> + <Button fx:id="zoomInButton" mnemonicParsing="false"> + <graphic> + <ImageView pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/magnifier-zoom-in-green.png" /> + </image> + </ImageView> + </graphic> + <HBox.margin> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </HBox.margin> + </Button> + </children> + </HBox> + <MenuButton fx:id="zoomMenuButton" mnemonicParsing="false" text="Zoom in/out to"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/magnifier-left.png" /> + </image> + </ImageView> + </graphic> + <HBox.margin> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </HBox.margin> + </MenuButton> + <Separator fx:id="rightSeperator" halignment="RIGHT" maxWidth="1.7976931348623157E308" minWidth="-Infinity" orientation="VERTICAL" HBox.hgrow="ALWAYS"> + <HBox.margin> + <Insets left="10.0" right="10.0" /> + </HBox.margin> + </Separator> + <Label contentDisplay="RIGHT" minWidth="-Infinity" text="End:"> + <graphic> + <LocalDateTimeTextField fx:id="endPicker" minWidth="200.0" nodeOrientation="LEFT_TO_RIGHT" prefWidth="200.0"> + <padding> + <Insets top="3.0" /> + </padding> + </LocalDateTimeTextField> + </graphic> + </Label> + </items> + </ToolBar> + </children> + </VBox> + </bottom> + </fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..e13f7624a12c198390631a45f8edfdbeb203b9e7 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -0,0 +1,583 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import impl.org.controlsfx.skin.RangeSliderSkin; +import java.net.URL; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Rectangle2D; +import javafx.scene.Node; +import javafx.scene.SnapshotParameters; +import javafx.scene.control.Button; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Separator; +import javafx.scene.control.Skin; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.Lighting; +import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javax.annotation.concurrent.GuardedBy; +import jfxtras.scene.control.LocalDateTimeTextField; +import org.controlsfx.control.RangeSlider; +import org.controlsfx.control.action.AbstractAction; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.LoggedTask; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.VisualizationMode; +import org.sleuthkit.autopsy.timeline.actions.DefaultFilters; +import org.sleuthkit.autopsy.timeline.actions.SaveSnapshot; +import org.sleuthkit.autopsy.timeline.actions.ZoomOut; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; +import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavPanel; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; + +/** A Container for an {@link AbstractVisualization}, has a toolbar on top to + * hold settings widgets supplied by contained {@link AbstractVisualization}, + * and the histogram / timeselection on bottom. Also supplies containers for + * replacement axis to contained {@link AbstractVisualization} + * + * TODO: refactor common code out of histogram and CountsView? -jm + */ +public class VisualizationPanel extends BorderPane implements TimeLineView { + + @GuardedBy("this") + private LoggedTask<Void> histogramTask; + + private static final Logger LOGGER = Logger.getLogger(VisualizationPanel.class.getName()); + + private final NavPanel navPanel; + + private AbstractVisualization<?, ?, ?, ?> visualization; + + @FXML // ResourceBundle that was given to the FXMLLoader + private ResourceBundle resources; + + @FXML // URL location of the FXML file that was given to the FXMLLoader + private URL location; + + //// range slider and histogram componenets + @FXML // fx:id="histogramBox" + protected HBox histogramBox; // Value injected by FXMLLoader + + @FXML // fx:id="rangeHistogramStack" + protected StackPane rangeHistogramStack; // Value injected by FXMLLoader + + private final RangeSlider rangeSlider = new RangeSlider(0, 1.0, .25, .75); + + //// time range selection componenets + @FXML + protected MenuButton zoomMenuButton; + + @FXML + private Separator rightSeperator; + + @FXML + private Separator leftSeperator; + + @FXML + protected Button zoomOutButton; + + @FXML + protected Button zoomInButton; + + @FXML + protected LocalDateTimeTextField startPicker; + + @FXML + protected LocalDateTimeTextField endPicker; + + //// replacemetn axis label componenets + @FXML + protected Pane partPane; + + @FXML + protected Pane contextPane; + + @FXML + protected Region spacer; + + //// header toolbar componenets + @FXML + private ToolBar toolBar; + + @FXML + private ToggleButton countsToggle; + + @FXML + private ToggleButton detailsToggle; + + @FXML + private Button snapShotButton; + + private double preDragPos; + + protected TimeLineController controller; + + protected FilteredEventsModel filteredEvents; + + private final ChangeListener<Object> rangeSliderListener + = (observable1, oldValue, newValue) -> { + if (rangeSlider.isHighValueChanging() == false && rangeSlider.isLowValueChanging() == false) { + Long minTime = filteredEvents.getMinTime() * 1000; + controller.pushTimeRange(new Interval( + new Double(rangeSlider.getLowValue() + minTime).longValue(), + new Double(rangeSlider.getHighValue() + minTime).longValue(), + DateTimeZone.UTC)); + } + }; + + private final InvalidationListener endListener = (Observable observable) -> { + if (endPicker.getLocalDateTime() != null) { + controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRange().get().withEndMillis( + ZonedDateTime.of(endPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli())); + } + }; + + private final InvalidationListener startListener = (Observable observable) -> { + if (startPicker.getLocalDateTime() != null) { + controller.pushTimeRange(VisualizationPanel.this.filteredEvents.timeRange().get().withStartMillis( + ZonedDateTime.of(startPicker.getLocalDateTime(), TimeLineController.getTimeZoneID()).toInstant().toEpochMilli())); + } + }; + + static private final Background background = new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)); + + static private final Lighting lighting = new Lighting(); + + public VisualizationPanel(NavPanel navPanel) { + this.navPanel = navPanel; + FXMLConstructor.construct(this, "VisualizationPanel.fxml"); + } + + @FXML // This method is called by the FXMLLoader when initialization is complete + protected void initialize() { + assert endPicker != null : "fx:id=\"endPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; + assert histogramBox != null : "fx:id=\"histogramBox\" was not injected: check your FXML file 'ViewWrapper.fxml'."; + assert startPicker != null : "fx:id=\"startPicker\" was not injected: check your FXML file 'ViewWrapper.fxml'."; + assert rangeHistogramStack != null : "fx:id=\"rangeHistogramStack\" was not injected: check your FXML file 'ViewWrapper.fxml'."; + assert countsToggle != null : "fx:id=\"countsToggle\" was not injected: check your FXML file 'VisToggle.fxml'."; + assert detailsToggle != null : "fx:id=\"eventsToggle\" was not injected: check your FXML file 'VisToggle.fxml'."; + + HBox.setHgrow(leftSeperator, Priority.ALWAYS); + HBox.setHgrow(rightSeperator, Priority.ALWAYS); + ChangeListener<Toggle> toggleListener = (ObservableValue<? extends Toggle> observable, + Toggle oldValue, + Toggle newValue) -> { + if (newValue == null) { + countsToggle.getToggleGroup().selectToggle(oldValue != null ? oldValue : countsToggle); + } else if (newValue == countsToggle && oldValue != null) { + controller.setViewMode(VisualizationMode.COUNTS); + } else if (newValue == detailsToggle && oldValue != null) { + controller.setViewMode(VisualizationMode.DETAIL); + } + }; + + if (countsToggle.getToggleGroup() != null) { + countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); + } else { + countsToggle.toggleGroupProperty().addListener((Observable observable) -> { + countsToggle.getToggleGroup().selectedToggleProperty().addListener(toggleListener); + }); + } + + //setup rangeslider + rangeSlider.setOpacity(.7); + rangeSlider.setMin(0); + + /** this is still needed to not get swamped by low/high value changes. + * https://bitbucket.org/controlsfx/controlsfx/issue/241/rangeslider-high-low-properties + * TODO: committ an appropriate version of this fix to the ControlsFX + * repo on bitbucket, remove this after next release -jm */ + Skin<?> skin = rangeSlider.getSkin(); + if (skin != null) { + attachDragListener((RangeSliderSkin) skin); + } else { + rangeSlider.skinProperty().addListener((Observable observable) -> { + RangeSliderSkin skin1 = (RangeSliderSkin) rangeSlider.getSkin(); + attachDragListener(skin1); + }); + } + + rangeSlider.setBlockIncrement(1); + + rangeHistogramStack.getChildren().add(rangeSlider); + + /* this padding attempts to compensates for the fact that the + * rangeslider track doesn't extend to edge of node,and so the + * histrogram doesn't quite line up with the rangeslider */ + histogramBox.setStyle(" -fx-padding: 0,0.5em,0,.5em; "); + + zoomMenuButton.getItems().clear(); + for (ZoomRanges b : ZoomRanges.values()) { + + MenuItem menuItem = new MenuItem(b.getDisplayName()); + menuItem.setOnAction((event) -> { + if (b != ZoomRanges.ALL) { + controller.pushPeriod(b.getPeriod()); + } else { + controller.showFullRange(); + } + }); + zoomMenuButton.getItems().add(menuItem); + } + + zoomOutButton.setOnAction(e -> { + controller.pushZoomOutTime(); + }); + zoomInButton.setOnAction(e -> { + controller.pushZoomInTime(); + }); + + snapShotButton.setOnAction((ActionEvent event) -> { + //take snapshot + final SnapshotParameters snapshotParameters = new SnapshotParameters(); + snapshotParameters.setViewport(new Rectangle2D(visualization.getBoundsInParent().getMinX(), visualization.getBoundsInParent().getMinY(), + visualization.getBoundsInParent().getWidth(), + contextPane.getLayoutBounds().getHeight() + visualization.getLayoutBounds().getHeight() + partPane.getLayoutBounds().getHeight() + )); + WritableImage snapshot = this.snapshot(snapshotParameters, null); + //pass snapshot to save action + new SaveSnapshot(controller, snapshot).handle(event); + }); + } + + /** + * TODO: committed an appropriate version of this fix to the ControlsFX repo + * on bitbucket, remove this after next release -jm + * + * @param skin + */ + private void attachDragListener(RangeSliderSkin skin) { + if (skin != null) { + for (Node n : skin.getChildren()) { + if (n.getStyleClass().contains("track")) { + n.setOpacity(.3); + } + if (n.getStyleClass().contains("range-bar")) { + StackPane rangeBar = (StackPane) n; + rangeBar.setOnMousePressed((MouseEvent e) -> { + rangeBar.requestFocus(); + preDragPos = e.getX(); + }); + + //don't mark as not changing until mouse is released + rangeBar.setOnMouseReleased((MouseEvent event) -> { + rangeSlider.setLowValueChanging(false); + rangeSlider.setHighValueChanging(false); + }); + rangeBar.setOnMouseDragged((MouseEvent event) -> { + final double min = rangeSlider.getMin(); + final double max = rangeSlider.getMax(); + + ///!!! compensate for range and width so that rangebar actualy stays with the slider + double delta = (event.getX() - preDragPos) * (max - min) / rangeSlider. + getWidth(); + //////////////////////////////////////////////////// + + final double lowValue = rangeSlider.getLowValue(); + final double newLowValue = Math.min(Math.max(min, lowValue + delta), + max); + final double highValue = rangeSlider.getHighValue(); + final double newHighValue = Math.min(Math.max(min, highValue + delta), + max); + + if (newLowValue <= min || newHighValue >= max) { + return; + } + + rangeSlider.setLowValueChanging(true); + rangeSlider.setHighValueChanging(true); + rangeSlider.setLowValue(newLowValue); + rangeSlider.setHighValue(newHighValue); + }); + } + } + } + } + + @Override + public synchronized void setController(TimeLineController controller) { + this.controller = controller; + setModel(controller.getEventsModel()); + + setViewMode(controller.getViewMode().get()); + + controller.getNeedsHistogramRebuild().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { + if (newValue) { + refreshHistorgram(); + } + }); + + controller.getViewMode().addListener((ObservableValue<? extends VisualizationMode> ov, VisualizationMode t, VisualizationMode t1) -> { + setViewMode(t1); + }); + refreshHistorgram(); + } + + private void setViewMode(VisualizationMode visualizationMode) { + switch (visualizationMode) { + case COUNTS: + setVisualization(new CountsViewPane(partPane, contextPane, spacer)); + countsToggle.setSelected(true); + break; + case DETAIL: + setVisualization(new DetailViewPane(partPane, contextPane, spacer)); + detailsToggle.setSelected(true); + break; + } + } + + synchronized void setVisualization(final AbstractVisualization<?, ?, ?, ?> newViz) { + Platform.runLater(() -> { + synchronized (VisualizationPanel.this) { + if (visualization != null) { + toolBar.getItems().removeAll(visualization.getSettingsNodes()); + visualization.dispose(); + } + + visualization = newViz; + toolBar.getItems().addAll(newViz.getSettingsNodes()); + + visualization.setController(controller); + setCenter(visualization); + if (visualization instanceof DetailViewPane) { + navPanel.setChart((DetailViewPane) visualization); + } + visualization.hasEvents.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { + if (newValue == false) { + + setCenter(new StackPane(visualization, new Region() { + { + setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); + setOpacity(.3); + } + }, new NoEventsDialog(() -> { + setCenter(visualization); + }))); + } else { + setCenter(visualization); + } + }); + } + }); + } + + synchronized private void refreshHistorgram() { + + if (histogramTask != null) { + histogramTask.cancel(true); + } + + histogramTask = new LoggedTask<Void>("Rebuild Histogram", true) { + + @Override + protected Void call() throws Exception { + + updateMessage("preparing"); + + long max = 0; + final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); + final long lowerBound = rangeInfo.getLowerBound(); + final long upperBound = rangeInfo.getUpperBound(); + Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); + + //extend range to block bounderies (ie day, month, year) + int p = 0; // progress counter + + //clear old data, and reset ranges and series + Platform.runLater(() -> { + updateMessage("resetting ui"); + + }); + + ArrayList<Long> bins = new ArrayList<>(); + + DateTime start = timeRange.getStart(); + while (timeRange.contains(start)) { + if (isCancelled()) { + return null; + } + DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); + final Interval interval = new Interval(start, end); + //increment for next iteration + + start = end; + + updateMessage("querying db"); + //query for current range + long count = filteredEvents.getEventCounts(interval).values().stream().mapToLong(Long::valueOf).sum(); + bins.add(count); + + max = Math.max(count, max); + + final double fMax = Math.log(max); + final ArrayList<Long> fbins = new ArrayList<>(bins); + Platform.runLater(() -> { + updateMessage("updating ui"); + + histogramBox.getChildren().clear(); + + for (Long bin : fbins) { + if (isCancelled()) { + break; + } + Region bar = new Region(); + //scale them to fit in histogram height + bar.prefHeightProperty().bind(histogramBox.heightProperty().multiply(Math.log(bin)).divide(fMax)); + bar.setMaxHeight(USE_PREF_SIZE); + bar.setMinHeight(USE_PREF_SIZE); + bar.setBackground(background); + bar.setOnMouseEntered((MouseEvent event) -> { + Tooltip.install(bar, new Tooltip(bin.toString())); + }); + bar.setEffect(lighting); + //they each get equal width to fill the histogram horizontaly + HBox.setHgrow(bar, Priority.ALWAYS); + histogramBox.getChildren().add(bar); + } + }); + } + return null; + } + + }; + new Thread(histogramTask).start(); + controller.monitorTask(histogramTask); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + + refreshTimeUI(filteredEvents.timeRange().get()); + this.filteredEvents.timeRange().addListener((Observable observable) -> { + refreshTimeUI(filteredEvents.timeRange().get()); + }); + TimeLineController.getTimeZone().addListener((Observable observable) -> { + refreshTimeUI(filteredEvents.timeRange().get()); + }); + } + + private void refreshTimeUI(Interval interval) { + RangeDivisionInfo rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.getSpanningInterval()); + + final Long minTime = rangeDivisionInfo.getLowerBound(); + final long maxTime = rangeDivisionInfo.getUpperBound(); + + if (minTime > 0 && maxTime > minTime) { + + Platform.runLater(() -> { + startPicker.localDateTimeProperty().removeListener(startListener); + endPicker.localDateTimeProperty().removeListener(endListener); + rangeSlider.highValueChangingProperty().removeListener(rangeSliderListener); + rangeSlider.lowValueChangingProperty().removeListener(rangeSliderListener); + + rangeSlider.setMax((Long) (maxTime - minTime)); + rangeSlider.setHighValue(interval.getEndMillis() - minTime); + rangeSlider.setLowValue(interval.getStartMillis() - minTime); + endPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getEndMillis()), TimeLineController.getTimeZoneID())); + startPicker.setLocalDateTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(interval.getStartMillis()), TimeLineController.getTimeZoneID())); + + rangeSlider.highValueChangingProperty().addListener(rangeSliderListener); + rangeSlider.lowValueChangingProperty().addListener(rangeSliderListener); + startPicker.localDateTimeProperty().addListener(startListener); + endPicker.localDateTimeProperty().addListener(endListener); + }); + } + } + + private class NoEventsDialog extends TitledPane { + + private final Runnable closeCallback; + + @FXML // ResourceBundle that was given to the FXMLLoader + private ResourceBundle resources; + + @FXML // URL location of the FXML file that was given to the FXMLLoader + private URL location; + + @FXML + private Button resetFiltersButton; + + @FXML + private Button dismissButton; + + @FXML + private Button zoomButton; + + public NoEventsDialog(Runnable closeCallback) { + this.closeCallback = closeCallback; + FXMLConstructor.construct(this, "NoEventsDialog.fxml"); + + } + + @FXML + void initialize() { + assert resetFiltersButton != null : "fx:id=\"resetFiltersButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; + assert dismissButton != null : "fx:id=\"dismissButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; + assert zoomButton != null : "fx:id=\"zoomButton\" was not injected: check your FXML file 'NoEventsDialog.fxml'."; + + AbstractAction zoomOutAction = new ZoomOut(controller); + zoomButton.setOnAction(zoomOutAction); + zoomButton.disableProperty().bind(zoomOutAction.disabledProperty()); + + dismissButton.setOnAction(e -> { + closeCallback.run(); + }); + AbstractAction defaultFiltersAction = new DefaultFilters(controller); + resetFiltersButton.setOnAction(defaultFiltersAction); + resetFiltersButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java new file mode 100644 index 0000000000000000000000000000000000000000..30b9f2fd1bfe47698455860e5c663b7bf7331fe6 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java @@ -0,0 +1,51 @@ +package org.sleuthkit.autopsy.timeline.ui; + +import org.joda.time.Days; +import org.joda.time.Hours; +import org.joda.time.Minutes; +import org.joda.time.Months; +import org.joda.time.ReadablePeriod; +import org.joda.time.Weeks; +import org.joda.time.Years; + +/** + * + */ +public enum ZoomRanges { + + ONE_MINUTE("One Minute", Minutes.ONE), + FIFTEEN_MINUTES("Fifteen Minutes", Minutes.minutes(15)), + ONE_HOUR("One Hour", Hours.ONE), + SIX_HOURS("Six Hours", Hours.SIX), + TWELVE_HOURS("Twelve Hours", Hours.hours(12)), + ONE_DAY("One Day", Days.ONE), + THREE_DAYS("Three Days", Days.THREE), + ONE_WEEK("One Week", Weeks.ONE), + TWO_WEEK("Two Weeks", Weeks.TWO), + ONE_MONTH("One Month", Months.ONE), + THREE_MONTHS("Three Months", Months.THREE), + SIX_MONTHS("Six Months", Months.SIX), + ONE_YEAR("One Year", Years.ONE), + THREE_YEARS("Three Years", Years.THREE), + FIVE_YEARS("Five Years", Years.years(5)), + TEN_YEARS("Ten Years", Years.years(10)), + ALL("All", Minutes.ONE); + + private ZoomRanges(String displayName, ReadablePeriod period) { + this.displayName = displayName; + this.period = period; + } + + + private String displayName; + private ReadablePeriod period; + + String getDisplayName() { + return displayName; + } + + ReadablePeriod getPeriod() { + return period; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties new file mode 100644 index 0000000000000000000000000000000000000000..284a121fc9668d7ed625e5a0674f883d6491f1d0 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/Bundle.properties @@ -0,0 +1,19 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +CountsChartPane.numberOfEvents=Number of Events diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java new file mode 100644 index 0000000000000000000000000000000000000000..5d9f60120272b7d521f0cc2250d7e0058c21da0f --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewPane.java @@ -0,0 +1,520 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.countsview; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.StackedBarChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.RadioButton; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.DropShadow; +import javafx.scene.effect.Effect; +import javafx.scene.effect.Lighting; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import org.controlsfx.control.action.ActionGroup; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Seconds; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.LoggedTask; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.actions.Back; +import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.events.type.RootEventType; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization; +import org.sleuthkit.autopsy.timeline.utils.ColorUtilities; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * FXML Controller class for a {@link StackedBarChart<String,Number>} based + * implementation of a {@link TimeLineView}. + * + * This class listens to changes in the assigned {@link FilteredEventsModel} and + * updates the internal {@link StackedBarChart} to reflect the currently + * requested events. + * + * This class captures input from the user in the form of mouse clicks on graph + * bars, and forwards them to the assigned {@link TimeLineController} * + * + * Concurrency Policy: Access to the private members stackedBarChart, countAxis, + * dateAxis, EventTypeMap, and dataSets affects the stackedBarChart so + * they all must only be manipulated on the JavaFx thread (through {@link Platform#runLater(java.lang.Runnable)} + * + * {@link CountsChartPane#filteredEvents} should encapsulate all need + * synchronization internally. + * + * TODO: refactor common code out of this class and ClusterChartPane into + * AbstractChartView + */ +public class CountsViewPane extends AbstractVisualization<String, Number, Node, EventCountsChart> { + + private static final Effect SELECTED_NODE_EFFECT = new Lighting(); + + private static final Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); + + private final NumberAxis countAxis = new NumberAxis(); + + private final CategoryAxis dateAxis = new CategoryAxis(FXCollections.<String>observableArrayList()); + + private final SimpleObjectProperty<ScaleType> scale = new SimpleObjectProperty<>(ScaleType.LINEAR); + + //private access to barchart data + private final Map<EventType, XYChart.Series<String, Number>> eventTypeMap = new ConcurrentHashMap<>(); + + @Override + protected String getTickMarkLabel(String labelValueString) { + return labelValueString; + } + + @Override + protected Boolean isTickBold(String value) { + return dataSets.stream().flatMap((series) -> series.getData().stream()) + .anyMatch((data) -> data.getXValue().equals(value) && data.getYValue().intValue() > 0); + } + + private ContextMenu getContextMenu() { + + ContextMenu chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new ActionGroup("Zoom History", new Back(controller), new Forward(controller)))); + chartContextMenu.setAutoHide(true); + return chartContextMenu; + } + + @Override + protected Task<Boolean> getUpdateTask() { + return new LoggedTask<Boolean>("Updating Counts Graph", true) { + + @Override + protected Boolean call() throws Exception { + if (isCancelled()) { + return null; + } + updateProgress(-1, 1); + updateMessage("preparing update"); + Platform.runLater(() -> { + setCursor(Cursor.WAIT); + }); + + final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.timeRange().get()); + chart.setRangeInfo(rangeInfo); + //extend range to block bounderies (ie day, month, year) + final long lowerBound = rangeInfo.getLowerBound(); + final long upperBound = rangeInfo.getUpperBound(); + final Interval timeRange = new Interval(new DateTime(lowerBound, TimeLineController.getJodaTimeZone()), new DateTime(upperBound, TimeLineController.getJodaTimeZone())); + + int max = 0; + int p = 0; // progress counter + + //clear old data, and reset ranges and series + Platform.runLater(() -> { + updateMessage("resetting ui"); + eventTypeMap.clear(); + dataSets.clear(); + dateAxis.getCategories().clear(); + + DateTime start = timeRange.getStart(); + while (timeRange.contains(start)) { + //add bar/'category' label for the current interval + final String dateString = start.toString(rangeInfo.getTickFormatter()); + dateAxis.getCategories().add(dateString); + + //increment for next iteration + start = start.plus(rangeInfo.getPeriodSize().getPeriod()); + } + + //make all series to ensure they get created in consistent order + EventType.allTypes.forEach(CountsViewPane.this::getSeries); + }); + + DateTime start = timeRange.getStart(); + while (timeRange.contains(start)) { + + final String dateString = start.toString(rangeInfo.getTickFormatter()); + DateTime end = start.plus(rangeInfo.getPeriodSize().getPeriod()); + final Interval interval = new Interval(start, end); + + //query for current range + Map<EventType, Long> eventCounts = filteredEvents.getEventCounts(interval); + + //increment for next iteration + start = end; + + int dateMax = 0; //used in max tracking + + //for each type add data to graph + for (final EventType et : eventCounts.keySet()) { + if (isCancelled()) { + return null; + } + + final Long count = eventCounts.get(et); + final int fp = p++; + if (count > 0) { + final double adjustedCount = count == 0 ? 0 : scale.get().adjust(count); + + dateMax += adjustedCount; + final XYChart.Data<String, Number> xyData = new BarChart.Data<>(dateString, adjustedCount); + + xyData.nodeProperty().addListener((Observable o) -> { + final Node node = xyData.getNode(); + if (node != null) { + node.setStyle("-fx-border-width: 2; -fx-border-color: " + ColorUtilities.getRGBCode(et.getSuperType().getColor()) + "; -fx-bar-fill: " + ColorUtilities.getRGBCode(et.getColor())); + node.setCursor(Cursor.HAND); + + node.setOnMouseEntered((MouseEvent event) -> { + //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart + final Tooltip tooltip = new Tooltip(count + " " + et.getDisplayName() + " events\n" + + "between " + dateString + "\n" + + "and " + + interval.getEnd().toString(rangeInfo.getTickFormatter())); + tooltip.setGraphic(new ImageView(et.getFXImage())); + Tooltip.install(node, tooltip); + node.setEffect(new DropShadow(10, et.getColor())); + }); + node.setOnMouseExited((MouseEvent event) -> { + if (selectedNodes.contains(node)) { + node.setEffect(SELECTED_NODE_EFFECT); + } else { + node.setEffect(null); + } + }); + + node.addEventHandler(MouseEvent.MOUSE_CLICKED, new BarClickHandler(node, dateString, interval, et)); + } + }); + + max = Math.max(max, dateMax); + + final double fmax = max; + + Platform.runLater(() -> { + updateMessage("updating counts"); + getSeries(et).getData().add(xyData); + if (scale.get().equals(ScaleType.LINEAR)) { + countAxis.setTickUnit(Math.pow(10, Math.max(0, Math.floor(Math.log10(fmax)) - 1))); + } else { + countAxis.setTickUnit(Double.MAX_VALUE); + } + countAxis.setUpperBound(1 + fmax * 1.2); + layoutDateLabels(); + updateProgress(fp, rangeInfo.getPeriodsInRange()); + }); + } else { + final double fmax = max; + + Platform.runLater(() -> { + updateMessage("updating counts"); + updateProgress(fp, rangeInfo.getPeriodsInRange()); + }); + } + } + } + + Platform.runLater(() -> { + updateMessage("wrapping up"); + updateProgress(1, 1); + layoutDateLabels(); + setCursor(Cursor.NONE); + }); + + return max > 0; + } + }; + } + + public CountsViewPane(Pane partPane, Pane contextPane, Region spacer) { + super(partPane, contextPane, spacer); + chart = new EventCountsChart(dateAxis, countAxis); + setChartClickHandler(); + chart.setData(dataSets); + setCenter(chart); + + settingsNodes = new ArrayList<>(new CountsViewSettingsPane().getChildrenUnmodifiable()); + + dateAxis.getTickMarks().addListener((Observable observable) -> { + layoutDateLabels(); + }); + dateAxis.categorySpacingProperty().addListener((Observable observable) -> { + layoutDateLabels(); + }); + dateAxis.getCategories().addListener((Observable observable) -> { + layoutDateLabels(); + }); + + spacer.minWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2))); + spacer.prefWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2))); + spacer.maxWidthProperty().bind(countAxis.widthProperty().add(countAxis.tickLengthProperty()).add(dateAxis.startMarginProperty().multiply(2))); + + scale.addListener(o -> { + countAxis.tickLabelsVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR)); + countAxis.tickMarkVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR)); + countAxis.minorTickVisibleProperty().bind(scale.isEqualTo(ScaleType.LINEAR)); + update(); + }); + } + + @Override + protected NumberAxis getYAxis() { + return countAxis; + } + + @Override + protected CategoryAxis getXAxis() { + return dateAxis; + } + + @Override + protected double getTickSpacing() { + return dateAxis.getCategorySpacing(); + } + + @Override + protected Effect getSelectionEffect() { + return SELECTED_NODE_EFFECT; + } + + @Override + protected void applySelectionEffect(Node c1, Boolean applied) { + if (applied) { + c1.setEffect(getSelectionEffect()); + } else { + c1.setEffect(null); + } + } + + /** + * NOTE: Because this method modifies data directly used by the chart, this + * method should only be called from JavaFX thread! + * + * @param et the EventType to get the series for + * + * @return a Series object to contain all the events with the given + * EventType + */ + private XYChart.Series<String, Number> getSeries(final EventType et) { + XYChart.Series<String, Number> series = eventTypeMap.get(et); + if (series == null) { + series = new XYChart.Series<>(); + series.setName(et.getDisplayName()); + eventTypeMap.put(et, series); + + dataSets.add(series); + } + return series; + + } + + /** + * EventHandler for click events on nodes representing a bar(segment) in the + * stacked bar chart. + * + * Concurrency Policy: This only accesses immutable state or javafx nodes + * (from the jfx thread) and the internally synchronized + * {@link TimeLineController} + * + * TODO: review for thread safety -jm + */ + private class BarClickHandler implements EventHandler<MouseEvent> { + + private ContextMenu barContextMenu; + + private final Interval interval; + + private final EventType type; + + private final Node node; + + private final String startDateString; + + public BarClickHandler(Node node, String dateString, Interval countInterval, EventType type) { + this.interval = countInterval; + this.type = type; + this.node = node; + this.startDateString = dateString; + } + + @Override + public void handle(final MouseEvent e) { + e.consume(); + if (e.getClickCount() == 1) { //single click => selection + if (e.getButton().equals(MouseButton.PRIMARY)) { + controller.selectTimeAndType(interval, type); + selectedNodes.setAll(node); + } else if (e.getButton().equals(MouseButton.SECONDARY)) { + Platform.runLater(() -> { + chart.getContextMenu().hide(); + + if (barContextMenu == null) { + barContextMenu = new ContextMenu(); + barContextMenu.setAutoHide(true); + barContextMenu.getItems().addAll( + new MenuItem("Select Time Range") { + { + setOnAction((ActionEvent t) -> { + controller.selectTimeAndType(interval, RootEventType.getInstance()); + + selectedNodes.clear(); + for (XYChart.Series<String, Number> s : dataSets) { + s.getData().forEach((XYChart.Data<String, Number> d) -> { + if (startDateString.contains(d.getXValue())) { + selectedNodes.add(d.getNode()); + } + }); + } + }); + } + }, + new MenuItem("Select Event Type") { + { + setOnAction((ActionEvent t) -> { + controller.selectTimeAndType(filteredEvents.getSpanningInterval(), type); + + selectedNodes.clear(); + eventTypeMap.get(type).getData().forEach((d) -> { + selectedNodes.add(d.getNode()); + + }); + }); + } + }, + new MenuItem("Select Time and Type") { + { + setOnAction((ActionEvent t) -> { + controller.selectTimeAndType(interval, type); + selectedNodes.setAll(node); + }); + } + }, + new SeparatorMenuItem(), + new MenuItem("Zoom into Time Range") { + { + setOnAction((ActionEvent t) -> { + if (interval.toDuration().isShorterThan(Seconds.ONE.toStandardDuration()) == false) { + controller.pushTimeRange(interval); + } + }); + } + }); + barContextMenu.getItems().addAll(getContextMenu().getItems()); + } + + barContextMenu.show(node, e.getScreenX(), e.getScreenY()); + }); + + } + } else if (e.getClickCount() >= 2) { //double-click => zoom in time + if (interval.toDuration().isLongerThan(Seconds.ONE.toStandardDuration())) { + controller.pushTimeRange(interval); + } + } + } + + } + + class CountsViewSettingsPane extends HBox { + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private RadioButton logRadio; + + @FXML + private RadioButton sqrtRadio; + + @FXML + private RadioButton linearRadio; + + @FXML + private ToggleGroup scaleGroup; + + @FXML + void initialize() { + assert logRadio != null : "fx:id=\"logRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; + assert sqrtRadio != null : "fx:id=\"sqrtRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; + assert linearRadio != null : "fx:id=\"linearRadio\" was not injected: check your FXML file 'CountsViewSettingsPane.fxml'."; + linearRadio.setSelected(true); + scaleGroup.selectedToggleProperty().addListener(observable -> { + if (scaleGroup.getSelectedToggle() == linearRadio) { + scale.set(ScaleType.LINEAR); + } else if (scaleGroup.getSelectedToggle() == sqrtRadio) { + scale.set(ScaleType.SQUARE_ROOT); + } else if (scaleGroup.getSelectedToggle() == logRadio) { + scale.set(ScaleType.LOGARITHMIC); + } + }); + } + + public CountsViewSettingsPane() { + FXMLConstructor.construct(this, "CountsViewSettingsPane.fxml"); + } + } + + private static enum ScaleType { + + LINEAR(t -> t.doubleValue()), + SQUARE_ROOT(t -> Math.sqrt(t)), + LOGARITHMIC(t -> Math.log10(t) + 1); + + private final Function<Long, Double> func; + + ScaleType(Function<Long, Double> func) { + this.func = func; + } + + double adjust(Long c) { + return func.apply(c); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..533eff15aae377fa704efc89b0e144af91963a6c --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/CountsViewSettingsPane.fxml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + +<fx:root alignment="CENTER_LEFT" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" type="HBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<children><FlowPane alignment="CENTER_LEFT" hgap="5.0" prefWrapLength="250.0" vgap="5.0" HBox.hgrow="ALWAYS"> +<children><Label minHeight="-Infinity" minWidth="-Infinity" text="Scale: "> +<HBox.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</HBox.margin></Label><FlowPane alignment="CENTER_LEFT" hgap="5.0" prefWrapLength="250.0" vgap="5.0"> +<children><RadioButton fx:id="linearRadio" mnemonicParsing="false" selected="true" text="Linear"> +<toggleGroup> +<ToggleGroup fx:id="scaleGroup" /> +</toggleGroup></RadioButton><RadioButton fx:id="sqrtRadio" mnemonicParsing="false" text="Square Root" toggleGroup="$scaleGroup" /><RadioButton fx:id="logRadio" mnemonicParsing="false" text="Logarithmic" toggleGroup="$scaleGroup" /> +</children> +<HBox.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</HBox.margin> +<FlowPane.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</FlowPane.margin></FlowPane> +</children> +<HBox.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</HBox.margin></FlowPane> +</children></fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java new file mode 100644 index 0000000000000000000000000000000000000000..4ee393cdf4fb8ce58736add83a546dc6a28a4de3 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/countsview/EventCountsChart.java @@ -0,0 +1,196 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.countsview; + +import java.util.Arrays; +import java.util.Collections; +import javafx.scene.chart.Axis; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.StackedBarChart; +import javafx.scene.control.ContextMenu; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.util.StringConverter; +import org.controlsfx.control.action.ActionGroup; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.actions.Back; +import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; + +/** Customized {@link StackedBarChart<String, Number>} used to display the event + * counts in {@link CountsViewPane} */ +class EventCountsChart extends StackedBarChart<String, Number> implements TimeLineChart<String> { + + private ContextMenu contextMenu; + + private TimeLineController controller; + + private IntervalSelector<? extends String> intervalSelector; + + /** * the RangeDivisionInfo for the currently displayed time range, + * used to correct the interval provided + * {@link EventCountsChart#intervalSelector} by padding the end with one + * 'period' */ + private RangeDivisionInfo rangeInfo; + + EventCountsChart(CategoryAxis dateAxis, NumberAxis countAxis) { + super(dateAxis, countAxis); + //configure constant properties on axes and chart + dateAxis.setAnimated(true); + dateAxis.setLabel(null); + dateAxis.setTickLabelsVisible(false); + dateAxis.setTickLabelGap(0); + + countAxis.setLabel(NbBundle.getMessage(CountsViewPane.class, "CountsChartPane.numberOfEvents")); + countAxis.setAutoRanging(false); + countAxis.setLowerBound(0); + countAxis.setAnimated(true); + countAxis.setMinorTickCount(0); + countAxis.setTickLabelFormatter(new IntegerOnlyStringConverter()); + + setAlternativeRowFillVisible(true); + setCategoryGap(2); + setLegendVisible(false); + setAnimated(true); + setTitle(null); + + //use one handler with an if chain because it maintains state + ChartDragHandler<String, EventCountsChart> dragHandler = new ChartDragHandler<>(this, getXAxis()); + setOnMousePressed(dragHandler); + setOnMouseReleased(dragHandler); + setOnMouseDragged(dragHandler); + + setOnMouseClicked((MouseEvent clickEvent) -> { + contextMenu.hide(); + if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { + contextMenu.show(EventCountsChart.this, clickEvent.getScreenX(), clickEvent.getScreenY()); + clickEvent.consume(); + } + }); + } + + @Override + public void clearIntervalSelector() { + getChartChildren().remove(intervalSelector); + intervalSelector = null; + } + + @Override + public final synchronized void setController(TimeLineController controller) { + this.controller = controller; + setModel(this.controller.getEventsModel()); + //we have defered creating context menu until control is available + contextMenu = ActionUtils.createContextMenu( + Arrays.asList(new ActionGroup("Zoom History", new Back(controller), new Forward(controller)))); + contextMenu.setAutoHide(true); + } + + @Override + public IntervalSelector<? extends String> getIntervalSelector() { + return intervalSelector; + } + + @Override + public void setIntervalSelector(IntervalSelector<? extends String> newIntervalSelector) { + intervalSelector = newIntervalSelector; + getChartChildren().add(getIntervalSelector()); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + filteredEvents.getRequestedZoomParamters().addListener(o -> { + clearIntervalSelector(); + controller.selectEventIDs(Collections.emptyList()); + }); + } + + @Override + public CountsIntervalSelector newIntervalSelector(double x, Axis<String> dateAxis) { + return new CountsIntervalSelector(x, getHeight() - dateAxis.getHeight() - dateAxis.getTickLength(), dateAxis, controller); + } + + /** used by {@link CountsViewPane#BarClickHandler} to close the context menu + * when the bar menu is requested + * + * @return the context menu for this chart */ + ContextMenu getContextMenu() { + return contextMenu; + } + + void setRangeInfo(RangeDivisionInfo rangeInfo) { + this.rangeInfo = rangeInfo; + } + + /** + * StringConvereter used to 'format' vertical axis labels + */ + private static class IntegerOnlyStringConverter extends StringConverter<Number> { + + @Override + public String toString(Number n) { + //suppress non-integer values + return n.intValue() == n.doubleValue() + ? Integer.toString(n.intValue()) : ""; + } + + @Override + public Number fromString(String string) { + //this is unused but here for symmetry + return Double.valueOf(string).intValue(); + } + } + + /** Interval Selector for the counts chart, adjusts interval based on + * rangeInfo to include final period */ + private class CountsIntervalSelector extends IntervalSelector<String> { + + public CountsIntervalSelector(double x, double height, Axis<String> axis, TimeLineController controller) { + super(x, height, axis, controller); + } + + @Override + protected String formatSpan(String date) { + return date; + } + + @Override + protected Interval adjustInterval(Interval i) { + //extend range to block bounderies (ie day, month, year) + RangeDivisionInfo iInfo = RangeDivisionInfo.getRangeDivisionInfo(i); + final long lowerBound = iInfo.getLowerBound(); + final long upperBound = iInfo.getUpperBound(); + final DateTime lowerDate = new DateTime(lowerBound, TimeLineController.getJodaTimeZone()); + final DateTime upperDate = new DateTime(upperBound, TimeLineController.getJodaTimeZone()); + //add extra block to end that gets cut of by conversion from string/category. + return new Interval(lowerDate, upperDate.plus(rangeInfo.getPeriodSize().getPeriod())); + } + + @Override + protected DateTime parseDateTime(String date) { + return date == null ? new DateTime(rangeInfo.getLowerBound()) : rangeInfo.getTickFormatter().parseDateTime(date); + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java new file mode 100644 index 0000000000000000000000000000000000000000..edcb8ef407fb2b8b9416fcc828277a199276b681 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java @@ -0,0 +1,266 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.utils.ColorUtilities; + +/** Represents an {@link AggregateEvent} in a {@link EventDetailChart}. */ +public class AggregateEventNode extends StackPane { + + private static final CornerRadii CORNER_RADII = new CornerRadii(3); + + /** the border to apply when this node is 'selected' */ + private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + + /** The event this AggregateEventNode represents visually */ + private final AggregateEvent event; + + private final AggregateEventNode parentEventNode; + + /** the region that represents the time span of this node's event */ + private final Region spanRegion = new Region(); + + /** The label used to display this node's event's description */ + private final Label descrLabel = new Label(); + + /** The IamgeView used to show the icon for this node's event's type */ + private final ImageView eventTypeImageView = new ImageView(); + + /** Pane that contains AggregateEventNodes of any 'subevents' if they are + * displayed + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart */ + private final Pane subNodePane = new Pane(); + + /** the context menu that with the slider that controls subnode/event + * display + * + * //TODO: move more of the control of subnodes/events here and out + * of EventDetail Chart */ + private final SimpleObjectProperty<ContextMenu> contextMenu = new SimpleObjectProperty<>(); + + /** the Background used to fill the spanRegion, this varies epending on the + * selected/highlighted state of this node in its parent EventDetailChart */ + private Background spanFill; + + public AggregateEventNode(final AggregateEvent event, AggregateEventNode parentEventNode) { + this.event = event; + this.parentEventNode = parentEventNode; + //set initial properties + getChildren().addAll(spanRegion, subNodePane, descrLabel); + + setAlignment(Pos.TOP_LEFT); + setMinHeight(24); + minWidthProperty().bind(spanRegion.widthProperty()); + setPrefHeight(USE_COMPUTED_SIZE); + setMaxHeight(USE_PREF_SIZE); + setMargin(descrLabel, new Insets(2, 5, 2, 5)); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMinHeight(USE_PREF_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + subNodePane.setPickOnBounds(false); + + //setup description label + eventTypeImageView.setImage(event.getType().getFXImage()); + descrLabel.setGraphic(eventTypeImageView); + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + + descrLabel.setMouseTransparent(true); + setDescriptionVisibility(DescriptionVisibility.SHOWN); + + //setup backgrounds + final Color evtColor = event.getType().getColor(); + spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setCursor(Cursor.HAND); + spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); + spanRegion.setBackground(spanFill); + + //set up mouse hover effect and tooltip + setOnMouseEntered((MouseEvent e) -> { + //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart + installTooltip(); + spanRegion.setEffect(new DropShadow(10, evtColor)); + }); + + setOnMouseExited((MouseEvent e) -> { + spanRegion.setEffect(null); + }); + + } + + private void installTooltip() { + Tooltip.install(AggregateEventNode.this, new Tooltip(getEvent().getEventIDs().size() + " " + getEvent().getType() + " events\n" + + getEvent().getDescription() + + "\nbetween " + getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()) + + "\nand " + getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter()) + + "\nright-click to adjust local description zoom.")); + } + + public Pane getSubNodePane() { + return subNodePane; + } + + public AggregateEvent getEvent() { + return event; + } + + /** + * sets the width of the {@link Region} with border and background used to + * indicate the temporal span of this aggregate event + * + * @param w + */ + public void setSpanWidth(double w) { + spanRegion.setPrefWidth(w); + spanRegion.setMaxWidth(w); + spanRegion.setMinWidth(Math.max(2, w)); + } + + /** + * + * @param w the maximum width the description label should have + */ + public void setDescriptionLabelMaxWidth(double w) { + descrLabel.setMaxWidth(w); + } + + /** @param descrVis the level of description that should be displayed */ + final public void setDescriptionVisibility(DescriptionVisibility descrVis) { + switch (descrVis) { + case SHOWN: + descrLabel.setText(event.getDescription() + " (" + event.getEventIDs().size() + ")"); + break; + case COUNT_ONLY: + descrLabel.setText("(" + event.getEventIDs().size() + ")"); + break; + case HIDDEN: + descrLabel.setText(""); + break; + } + } + + /** apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + void applySelectionEffect(final boolean applied) { + Platform.runLater(() -> { + if (applied) { + setBorder(selectionBorder); + } else { + setBorder(null); + } + }); + } + + /** apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + void applyHighlightEffect(boolean applied) { + + if (applied) { + descrLabel.setStyle("-fx-font-weight: bold;"); + spanFill = new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .5), CORNER_RADII, Insets.EMPTY)); + spanRegion.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); + } else { + descrLabel.setStyle("-fx-font-weight: normal;"); + spanFill = new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); + spanRegion.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(getEvent().getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + } + } + + /** set the span background and description label visible or not + * (the span background is not visible when this node is displaying + * subnodes) + * //TODO: move more of the control of subnodes/events here and out + * of EventDetail Chart + * + * @param applied true to set the span fill visible, false to hide it + */ + void setEventDetailsVisible(boolean b) { + if (b) { + spanRegion.setBackground(spanFill); + } else { + spanRegion.setBackground(null); + } + descrLabel.setVisible(b); + } + + String getDisplayedDescription() { + return descrLabel.getText(); + } + + double getLayoutXCompensation() { + if (parentEventNode != null) { + return parentEventNode.getLayoutXCompensation() + getBoundsInParent().getMinX(); + } else { + return getBoundsInParent().getMinX(); + } + } + + /** + * @return the contextMenu + */ + public ContextMenu getContextMenu() { + return contextMenu.get(); + } + + /** + * @param contextMenu the contextMenu to set + */ + public void setContextMenu(ContextMenu contextMenu) { + this.contextMenu.set(contextMenu); + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java new file mode 100644 index 0000000000000000000000000000000000000000..df9092c794ad115ba053e65045bebc664bddb180 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DateAxis.java @@ -0,0 +1,338 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013, Christian Schudt + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * + * + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.scene.chart.Axis; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; + +/** + * from <a + * href="https://bitbucket.org/sco0ter/extfx/src/a61710e99c63bfa288672f0d99861c8fe8571293/src/main/java/extfx/scene/chart/DateAxis.java?at=0.3" + * >here</a> + * + * <strong> with extreme modifications.</strong> + * + * @author Christian Schudt + * @author Diego Cirujano + */ +final class DateAxis extends Axis<DateTime> { + + private ObjectProperty<DateTime> lowerBound = new ObjectPropertyBase<DateTime>() { + @Override + protected void invalidated() { + if (!isAutoRanging()) { + invalidateRange(); + requestAxisLayout(); + } + } + + @Override + public Object getBean() { + return DateAxis.this; + } + + @Override + public String getName() { + return "lowerBound"; + } + }; + + /** + * Stores the min and max date of the list of dates which is used. If + * {@link #autoRanging} is true, these values are used as lower and upper + * bounds. + */ + private DateTime maxDate; + + /** + * Stores the min and max date of the list of dates which is used. If + * {@link #autoRanging} is true, these values are used as lower and upper + * bounds. + */ + private DateTime minDate; + + private RangeDivisionInfo rangeDivisionInfo; + + private final ReadOnlyDoubleWrapper tickSpacing = new ReadOnlyDoubleWrapper(); + + private final ObjectProperty<DateTime> upperBound = new ObjectPropertyBase<DateTime>() { + @Override + protected void invalidated() { + if (!isAutoRanging()) { + invalidateRange(); + requestAxisLayout(); + } + } + + @Override + public Object getBean() { + return DateAxis.this; + } + + @Override + public String getName() { + return "upperBound"; + } + }; + + /** Default constructor. By default the lower and upper bound are calculated + * by the data. */ + DateAxis() { + setAutoRanging(false); + } + + @Override + public double getDisplayPosition(DateTime date) { + final double length = -200 + (getSide().isHorizontal() ? getWidth() : getHeight()); + + // Get the difference between the max and min date. + double diff = getUpperBound().getMillis() - getLowerBound().getMillis(); + + // Get the actual range of the visible area. + // The minimal date should start at the zero position, that's why we subtract it. + double range = length - getZeroPosition(); + + // Then get the difference from the actual date to the min date and divide it by the total difference. + // We get a value between 0 and 1, if the date is within the min and max date. + double d = (date.getMillis() - getLowerBound().getMillis()) / diff; + + // Multiply this percent value with the range and add the zero offset. + if (getSide().isVertical()) { + return getHeight() - d * range + getZeroPosition(); + } else { + return d * range + getZeroPosition(); + } + } + + /** + * Gets the lower bound of the axis. + * + * @return The lower bound. + * + * @see #lowerBoundProperty() + */ + public final DateTime getLowerBound() { + return lowerBound.get(); + } + + /** + * Sets the lower bound of the axis. + * + * @param date The lower bound date. + * + * @see #lowerBoundProperty() + */ + public final void setLowerBound(DateTime date) { + lowerBound.set(date); + } + + /** + * Gets the upper bound of the axis. + * + * @return The upper bound. + * + * @see #upperBoundProperty() + */ + public final DateTime getUpperBound() { + return upperBound.get(); + } + + /** + * Sets the upper bound of the axis. + * + * @param date The upper bound date. + * + * @see #upperBoundProperty() () + */ + public final void setUpperBound(DateTime date) { + upperBound.set(date); + } + + @Override + public DateTime getValueForDisplay(double displayPosition) { + final double length = - 200 + (getSide().isHorizontal() ? getWidth() : getHeight()); + + // Get the difference between the max and min date. + double diff = getUpperBound().getMillis() - getLowerBound().getMillis(); + + // Get the actual range of the visible area. + // The minimal date should start at the zero position, that's why we subtract it. + double range = length - getZeroPosition(); + + if (getSide().isVertical()) { + // displayPosition = getHeight() - ((date - lowerBound) / diff) * range + getZero + // date = displayPosition - getZero - getHeight())/range * diff + lowerBound + return new DateTime((long) ((displayPosition - getZeroPosition() - getHeight()) / -range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone()); + } else { + // displayPosition = ((date - lowerBound) / diff) * range + getZero + // date = displayPosition - getZero)/range * diff + lowerBound + return new DateTime((long) ((displayPosition - getZeroPosition()) / range * diff + getLowerBound().getMillis()), TimeLineController.getJodaTimeZone()); + } + } + + @Override + public double getZeroPosition() { + return 0; + } + + @Override + public void invalidateRange(List<DateTime> list) { + super.invalidateRange(list); + + Collections.sort(list); + if (list.isEmpty()) { + minDate = maxDate = new DateTime(); + } else if (list.size() == 1) { + minDate = maxDate = list.get(0); + } else if (list.size() > 1) { + minDate = list.get(0); + maxDate = list.get(list.size() - 1); + } + } + + @Override + public boolean isValueOnAxis(DateTime date) { + return date.getMillis() > getLowerBound().getMillis() && date.getMillis() < getUpperBound().getMillis(); + } + + @Override + public double toNumericValue(DateTime date) { + return date.getMillis(); + } + + @Override + public DateTime toRealValue(double v) { + return new DateTime((long) v); + } + + @Override + protected Object autoRange(double length) { + if (isAutoRanging()) { + return new Interval(minDate, maxDate); + } else { + if (getLowerBound() == null || getUpperBound() == null) { + return null; + } + return getRange(); + } + } + + @Override + protected List<DateTime> calculateTickValues(double length, Object range) { + List<DateTime> tickDates = new ArrayList<>(); + if (range == null) { + return tickDates; + } + rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range); + final DateTime lowerBound1 = getLowerBound(); + final DateTime upperBound1 = getUpperBound(); + + if (lowerBound1 == null || upperBound1 == null) { + return tickDates; + } + DateTime lower = lowerBound1.withZone(TimeLineController.getJodaTimeZone()); + DateTime upper = upperBound1.withZone(TimeLineController.getJodaTimeZone()); + + DateTime current = lower; + // Loop as long we exceeded the upper bound. + while (current.isBefore(upper)) { + tickDates.add(current); + current = current.plus(rangeDivisionInfo.getPeriodSize().getPeriod());//.add(interval.interval, interval.amount); + } + + // At last add the upper bound. + tickDates.add(upper); + + // If there are at least three dates, check if the gap between the lower date and the second date is at least half the gap of the second and third date. + // Do the same for the upper bound. + // If gaps between dates are to small, remove one of them. + // This can occur, e.g. if the lower bound is 25.12.2013 and years are shown. Then the next year shown would be 2014 (01.01.2014) which would be too narrow to 25.12.2013. + if (tickDates.size() > 2) { + DateTime secondDate = tickDates.get(1); + DateTime thirdDate = tickDates.get(2); + DateTime lastDate = tickDates.get(tickDates.size() - 2); + DateTime previousLastDate = tickDates.get(tickDates.size() - 3); + + // If the second date is too near by the lower bound, remove it. + if (secondDate.getMillis() - lower.getMillis() < (thirdDate.getMillis() - secondDate.getMillis()) / 2) { + tickDates.remove(lower); + } + + // If difference from the upper bound to the last date is less than the half of the difference of the previous two dates, + // we better remove the last date, as it comes to close to the upper bound. + if (upper.getMillis() - lastDate.getMillis() < (lastDate.getMillis() - previousLastDate.getMillis()) / 2) { + tickDates.remove(lastDate); + } + } + + if (tickDates.size() >= 2) { + tickSpacing.set(getDisplayPosition(tickDates.get(1)) - getDisplayPosition(tickDates.get(0))); + } else if (tickDates.size() >= 4) { + tickSpacing.set(getDisplayPosition(tickDates.get(2)) - getDisplayPosition(tickDates.get(1))); + } + return tickDates; + } + + @Override + protected Object getRange() { + + return new Interval(getLowerBound(), getUpperBound()); + } + + @Override + protected String getTickMarkLabel(DateTime date) { + return rangeDivisionInfo.getTickFormatter().print(date); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + } + + @Override + protected void setRange(Object range, boolean animating) { + + rangeDivisionInfo = RangeDivisionInfo.getRangeDivisionInfo((Interval) range); + + setLowerBound(new DateTime(rangeDivisionInfo.getLowerBound())); + setUpperBound(new DateTime(rangeDivisionInfo.getUpperBound())); + } + + ReadOnlyDoubleProperty getTickSpacing() { + return tickSpacing.getReadOnlyProperty(); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java new file mode 100644 index 0000000000000000000000000000000000000000..e398c68070a42f72858d2484d3c1e84e4f778b9e --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java @@ -0,0 +1,27 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +/** Level of description shown in UI + * NOTE: this is a separate concept form {@link DescriptionLOD} */ +enum DescriptionVisibility { + + HIDDEN, COUNT_ONLY, SHOWN; + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java new file mode 100644 index 0000000000000000000000000000000000000000..3eceb5d9197874a0e607f4562ccb192674a67997 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -0,0 +1,428 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.geometry.Orientation; +import javafx.scene.Cursor; +import javafx.scene.chart.Axis; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.CheckBox; +import javafx.scene.control.MultipleSelectionModel; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.Slider; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.TreeItem; +import javafx.scene.effect.Effect; +import static javafx.scene.input.KeyCode.DOWN; +import static javafx.scene.input.KeyCode.KP_DOWN; +import static javafx.scene.input.KeyCode.KP_UP; +import static javafx.scene.input.KeyCode.PAGE_DOWN; +import static javafx.scene.input.KeyCode.PAGE_UP; +import static javafx.scene.input.KeyCode.UP; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ScrollEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.LoggedTask; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization; +import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; +import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavTreeNode; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * FXML Controller class for a {@link EventDetailChart} based implementation of + * a TimeLineView. + * + * This class listens to changes in the assigned {@link FilteredEventsModel} and + * updates the internal {@link EventDetailChart} to reflect the currently + * requested events. + * + * This class captures input from the user in the form of mouse clicks on graph + * bars, and forwards them to the assigned {@link TimeLineController} + * + * Concurrency Policy: Access to the private members clusterChart, dateAxis, + * EventTypeMap, and dataSets is all linked directly to the ClusterChart which + * must only be manipulated on the JavaFx thread (through {@link Platform#runLater(java.lang.Runnable) + * } + * + * {@link CountsChartPane#filteredEvents} should encapsulate all needed + * synchronization internally. + * + * TODO: refactor common code out of this class and CountsChartPane into + * {@link AbstractVisualization} + */ +public class DetailViewPane extends AbstractVisualization<DateTime, AggregateEvent, AggregateEventNode, EventDetailChart> { + + private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); + + private MultipleSelectionModel<TreeItem<NavTreeNode>> treeSelectionModel; + + @FXML + protected ResourceBundle resources; + + @FXML + protected URL location; + + //these three could be injected from fxml but it was causing npe's + private final DateAxis dateAxis = new DateAxis(); + + private final Axis<AggregateEvent> verticalAxis = new EventAxis(); + + //private access to barchart data + private final Map<EventType, XYChart.Series<DateTime, AggregateEvent>> eventTypeToSeriesMap = new ConcurrentHashMap<>(); + + private final ScrollBar vertScrollBar = new ScrollBar(); + + private final Region region = new Region(); + + private final ObservableList<AggregateEvent> aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + + private final ObservableList<AggregateEventNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + + public ObservableList<AggregateEvent> getAggregatedEvents() { + return aggregatedEvents; + } + + public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) { + super(partPane, contextPane, spacer); + chart = new EventDetailChart(dateAxis, verticalAxis, selectedNodes); + setChartClickHandler(); + chart.setData(dataSets); + setCenter(chart); + + chart.setPrefHeight(USE_COMPUTED_SIZE); + + settingsNodes = new ArrayList<>(new DetailViewSettingsPane().getChildrenUnmodifiable()); + + vertScrollBar.setOrientation(Orientation.VERTICAL); + VBox vBox = new VBox(); + VBox.setVgrow(vertScrollBar, Priority.ALWAYS); + vBox.getChildren().add(vertScrollBar); + vBox.getChildren().add(region); + setRight(vBox); + + dateAxis.setAutoRanging(false); + region.minHeightProperty().bind(dateAxis.heightProperty()); + vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.getMaxVScroll())); + requestLayout(); + + highlightedNodes.addListener((ListChangeListener.Change<? extends AggregateEventNode> change) -> { + while (change.next()) { + change.getAddedSubList().forEach(aeNode -> { + aeNode.applyHighlightEffect(true); + }); + change.getRemoved().forEach(aeNode -> { + aeNode.applyHighlightEffect(false); + }); + } + }); + //request focus for keyboard scrolling + setOnMouseClicked((MouseEvent t) -> { + requestFocus(); + }); + + //These scroll related handlers don't affect any other view or the model, so they are handled internally + //mouse wheel scroll handler + this.onScrollProperty().set((EventHandler<ScrollEvent>) (ScrollEvent t) -> { + vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0))); + }); + + this.setOnKeyPressed((KeyEvent t) -> { + switch (t.getCode()) { + case PAGE_UP: + incrementScrollValue(-70); + break; + case PAGE_DOWN: + incrementScrollValue(70); + break; + case KP_UP: + case UP: + incrementScrollValue(-10); + break; + case KP_DOWN: + case DOWN: + incrementScrollValue(10); + break; + } + t.consume(); + }); + + //scrollbar handler + this.vertScrollBar.valueProperty().addListener((o, oldValue, newValue) -> { + chart.setVScroll(newValue.doubleValue() / 100.0); + }); + spacer.minWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + spacer.prefWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + spacer.maxWidthProperty().bind(verticalAxis.widthProperty().add(verticalAxis.tickLengthProperty())); + + dateAxis.setTickLabelsVisible(false); + + dateAxis.getTickMarks().addListener((Observable observable) -> { + layoutDateLabels(); + }); + dateAxis.getTickSpacing().addListener((Observable observable) -> { + layoutDateLabels(); + }); + + dateAxis.setTickLabelGap(0); + + selectedNodes.addListener((Observable observable) -> { + highlightedNodes.clear(); + selectedNodes.stream().forEach((tn) -> { + for (AggregateEventNode n : chart.getNodes(( + AggregateEventNode t) -> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) { + highlightedNodes.add(n); + } + }); + }); + + } + + private void incrementScrollValue(int factor) { + vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.getMaxVScroll().get())))); + } + + public void setSelectionModel(MultipleSelectionModel<TreeItem<NavTreeNode>> selectionModel) { + this.treeSelectionModel = selectionModel; + + treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { + highlightedNodes.clear(); + for (TreeItem<NavTreeNode> tn : treeSelectionModel.getSelectedItems()) { + for (AggregateEventNode n : chart.getNodes(( + AggregateEventNode t) + -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { + highlightedNodes.add(n); + } + } + }); + } + + @Override + protected Boolean isTickBold(DateTime value) { + return false; + } + + @Override + protected Axis<AggregateEvent> getYAxis() { + return verticalAxis; + } + + @Override + protected Axis<DateTime> getXAxis() { + return dateAxis; + } + + @Override + protected double getTickSpacing() { + return dateAxis.getTickSpacing().get(); + } + + @Override + protected String getTickMarkLabel(DateTime value) { + return dateAxis.getTickMarkLabel(value); + } + + /** NOTE: Because this method modifies data directly used by the chart, + * this method should only be called from JavaFX thread! + * + * @param et the EventType to get the series for + * + * @return a Series object to contain all the events with the given + * EventType */ + private XYChart.Series<DateTime, AggregateEvent> getSeries(final EventType et) { + XYChart.Series<DateTime, AggregateEvent> series = eventTypeToSeriesMap.get(et); + if (series == null) { + series = new XYChart.Series<>(); + series.setName(et.getDisplayName()); + eventTypeToSeriesMap.put(et, series); + dataSets.add(series); + } + return series; + } + + @Override + protected Task<Boolean> getUpdateTask() { + + return new LoggedTask<Boolean>("Update Details", true) { + + @Override + protected Boolean call() throws Exception { + if (isCancelled()) { + return null; + } + Platform.runLater(() -> { + if (isCancelled() == false) { + setCursor(Cursor.WAIT); + } + }); + + updateProgress(-1, 1); + updateMessage("preparing"); + + final RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(filteredEvents.timeRange().get()); + final long lowerBound = rangeInfo.getLowerBound(); + final long upperBound = rangeInfo.getUpperBound(); + + updateMessage("querying db"); + aggregatedEvents.setAll(filteredEvents.getAggregatedEvents()); + + Platform.runLater(() -> { + if (isCancelled()) { + return; + } + dateAxis.setLowerBound(new DateTime(lowerBound, TimeLineController.getJodaTimeZone())); + dateAxis.setUpperBound(new DateTime(upperBound, TimeLineController.getJodaTimeZone())); +// if (chart == null) { +// initializeClusterChart(); +// } + vertScrollBar.setValue(0); + eventTypeToSeriesMap.clear(); + dataSets.clear(); + }); + final int size = aggregatedEvents.size(); + int i = 0; + for (final AggregateEvent e : aggregatedEvents) { + if (isCancelled()) { + break; + } + updateProgress(i++, size); + updateMessage("updating ui"); + final XYChart.Data<DateTime, AggregateEvent> xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e); + + Platform.runLater(() -> { + if (isCancelled() == false) { + getSeries(e.getType()).getData().add(xyData); + } + }); + } + + Platform.runLater(() -> { + setCursor(Cursor.NONE); + layoutDateLabels(); + updateProgress(1, 1); + }); + return aggregatedEvents.isEmpty() == false; + } + }; + } + + @Override + protected Effect getSelectionEffect() { + return null; + } + + @Override + protected void applySelectionEffect(AggregateEventNode c1, Boolean applied) { + c1.applySelectionEffect(applied); + } + + class DetailViewSettingsPane extends HBox { + + @FXML + private RadioButton hiddenRadio; + + @FXML + private RadioButton showRadio; + + @FXML + private ToggleGroup descrVisibility; + + @FXML + private RadioButton countsRadio; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private CheckBox bandByTypeBox; + + @FXML + private CheckBox oneEventPerRowBox; + + @FXML + private CheckBox truncateAllBox; + + @FXML + private Slider truncateWidthSlider; + + public DetailViewSettingsPane() { + FXMLConstructor.construct(this, "DetailViewSettingsPane.fxml"); + } + + @FXML + void initialize() { + assert bandByTypeBox != null : "fx:id=\"bandByTypeBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; + assert oneEventPerRowBox != null : "fx:id=\"oneEventPerRowBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; + assert truncateAllBox != null : "fx:id=\"truncateAllBox\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; + assert truncateWidthSlider != null : "fx:id=\"truncateAllSlider\" was not injected: check your FXML file 'DetailViewSettings.fxml'."; + bandByTypeBox.selectedProperty().bindBidirectional(chart.getBandByType()); + truncateAllBox.selectedProperty().bindBidirectional(chart.getTruncateAll()); + oneEventPerRowBox.selectedProperty().bindBidirectional(chart.getOneEventPerRow()); + truncateWidthSlider.disableProperty().bind(truncateAllBox.selectedProperty().not()); + final InvalidationListener sliderListener = o -> { + if (truncateWidthSlider.isValueChanging() == false) { + chart.getTruncateWidth().set(truncateWidthSlider.getValue()); + } + }; + truncateWidthSlider.valueProperty().addListener(sliderListener); + truncateWidthSlider.valueChangingProperty().addListener(sliderListener); + + descrVisibility.selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> { + if (newToggle == countsRadio) { + chart.getDescrVisibility().set(DescriptionVisibility.COUNT_ONLY); + } else if (newToggle == showRadio) { + chart.getDescrVisibility().set(DescriptionVisibility.SHOWN); + } else if (newToggle == hiddenRadio) { + chart.getDescrVisibility().set(DescriptionVisibility.HIDDEN); + } + }); + } + + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..47b444c4e550c9e318c9ef584aa01e1deeb1526c --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + +<fx:root alignment="CENTER_LEFT" spacing="5.0" type="javafx.scene.layout.HBox" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <children> + <FlowPane alignment="CENTER_LEFT" hgap="5.0" prefHeight="-1.0" prefWidth="-1.0" prefWrapLength="220.0" vgap="5.0" HBox.hgrow="ALWAYS"> + <children><Label text="Layout Options:" /> +<HBox spacing="5.0"> +<children> + <CheckBox fx:id="bandByTypeBox" mnemonicParsing="false" text="Band by Type" /> + <CheckBox fx:id="oneEventPerRowBox" mnemonicParsing="false" text="One Event Per Row" /> +</children> +<FlowPane.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</FlowPane.margin> +</HBox> + </children> +<HBox.margin> +<Insets bottom="5.0" left="5.0" right="5.0" /> +</HBox.margin> + </FlowPane> + <Separator orientation="VERTICAL" prefHeight="-1.0" /> + <FlowPane columnHalignment="CENTER" hgap="5.0" prefHeight="-1.0" prefWidth="-1.0" prefWrapLength="350.0" vgap="5.0" HBox.hgrow="ALWAYS"> + <children> + <CheckBox fx:id="truncateAllBox" mnemonicParsing="false" text="Truncate Descriptions to (px):" /> + <Slider id="truncateAllSlider" fx:id="truncateWidthSlider" blockIncrement="50.0" disable="false" majorTickUnit="150.0" max="500.0" min="50.0" minorTickCount="0" prefHeight="33.0" prefWidth="146.0" showTickLabels="true" showTickMarks="false" value="200.0"> +<FlowPane.margin> +<Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> +</FlowPane.margin></Slider> + </children> + </FlowPane> + <Separator orientation="VERTICAL" prefHeight="-1.0" /><FlowPane alignment="CENTER_LEFT" hgap="5.0" minHeight="-Infinity" minWidth="-Infinity" prefWrapLength="200.0" vgap="5.0" HBox.hgrow="ALWAYS"> +<children><Label text="Description Visbility:" /> +<HBox spacing="5.0"> +<children><RadioButton fx:id="showRadio" mnemonicParsing="false" selected="true" text="Show"> +<toggleGroup> +<ToggleGroup fx:id="descrVisibility" /> +</toggleGroup></RadioButton><RadioButton fx:id="countsRadio" mnemonicParsing="false" text="Counts Only" toggleGroup="$descrVisibility" /><RadioButton fx:id="hiddenRadio" mnemonicParsing="false" text="Hide" toggleGroup="$descrVisibility" /> +</children> +<FlowPane.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</FlowPane.margin> +</HBox> +</children> +<HBox.margin> +<Insets /> +</HBox.margin></FlowPane> + </children> + <padding> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </padding> +</fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java new file mode 100644 index 0000000000000000000000000000000000000000..78235664c6e9808a752f4f1e682dbebcefe5ddec --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.util.Collections; +import java.util.List; +import javafx.scene.chart.Axis; +import javafx.scene.chart.XYChart; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; + +/** No-Op axis that doesn't do anything usefull but is necessary to pass + * AggregateEvent as the second member of {@link XYChart.Data} objects */ +class EventAxis extends Axis<AggregateEvent> { + + @Override + public double getDisplayPosition(AggregateEvent value) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public AggregateEvent getValueForDisplay(double displayPosition) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public double getZeroPosition() { + return 0; + } + + @Override + public boolean isValueOnAxis(AggregateEvent value) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public double toNumericValue(AggregateEvent value) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public AggregateEvent toRealValue(double value) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected Object autoRange(double length) { + return null; + } + + @Override + protected List<AggregateEvent> calculateTickValues(double length, Object range) { + return Collections.emptyList(); + } + + @Override + protected Object getRange() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected String getTickMarkLabel(AggregateEvent value) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected void setRange(Object range, boolean animate) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java new file mode 100644 index 0000000000000000000000000000000000000000..5f0c8e7584be19da3f37c179d654ba06f39226ff --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -0,0 +1,841 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import com.google.common.collect.Collections2; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.chart.Axis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Slider; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.shape.Line; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.text.Text; +import javafx.util.Duration; +import javafx.util.StringConverter; +import javax.annotation.concurrent.GuardedBy; +import org.controlsfx.control.action.AbstractAction; +import org.controlsfx.control.action.ActionGroup; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.timeline.LoggedTask; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.actions.Back; +import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.filters.Filter; +import org.sleuthkit.autopsy.timeline.filters.TextFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; + +/** + * Custom implementation of {@link XYChart} to graph events on a horizontal + * timeline. + * + * The horizontal {@link DateAxis} controls the tick-marks and the horizontal + * layout of the nodes representing events. The vertical {@link NumberAxis} does + * nothing (although a custom implementation could help with the vertical + * layout?) + * + * Series help organize events for the banding by event type, we could add a + * node to contain each band if we need a place for per band controls. + * + * //TODO: refactor the projected lines to a separate class. -jm */ +public class EventDetailChart extends XYChart<DateTime, AggregateEvent> implements TimeLineChart<DateTime> { + + private static final int PROJECTED_LINE_Y_OFFSET = 5; + + private static final int PROJECTED_LINE_STROKE_WIDTH = 5; + + /** true == layout each event type in its own band, false == mix all the + * events together during layout */ + private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); + + private ContextMenu chartContextMenu; + + private TimeLineController controller; + + /** how much detail of the description to show in the ui */ + private final SimpleObjectProperty<DescriptionVisibility> descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + + private FilteredEventsModel filteredEvents; + + /** a user position-able vertical line to help the compare events */ + private Line guideLine; + + /** * the user can drag out a time range to zoom into and this + * {@link IntervalSelector} is the visual representation of it while + * the user is dragging */ + private IntervalSelector<? extends DateTime> intervalSelector; + + /** listener that triggers layout pass */ + private final InvalidationListener layoutInvalidationListener = ( + Observable o) -> { + synchronized (EventDetailChart.this) { + requiresLayout = true; + requestChartLayout(); + } + }; + + /** the maximum y value used so far during the most recent layout pass */ + private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + + /** + * the group that all event nodes are added to. This facilitates scrolling + * by allowing a single translation of this group. + */ + private final Group nodeGroup = new Group(); + + /** map from event to node */ + private final Map<AggregateEvent, AggregateEventNode> nodeMap = new TreeMap<>(( + AggregateEvent o1, + AggregateEvent o2) -> { + int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis()); + if (comp != 0) { + return comp; + } else { + return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2); + } + }); + + /** true == enforce that no two events can share the same 'row', leading to + * sparser but possibly clearer layout. false == put unrelated events in the + * same 'row', creating a denser more compact layout */ + private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); + + private final ObservableMap<AggregateEventNode, Line> projectionMap = FXCollections.observableHashMap(); + + /** flag indicating whether this chart actually needs a layout pass */ + @GuardedBy(value = "this") + private boolean requiresLayout = true; + + private final ObservableList<AggregateEventNode> selectedNodes; + + /** + * list of series of data added to this chart TODO: replace this with a map + * from name to series? -jm + */ + private final ObservableList<Series<DateTime, AggregateEvent>> seriesList + = FXCollections.<Series<DateTime, AggregateEvent>>observableArrayList(); + + private final ObservableList<Series<DateTime, AggregateEvent>> sortedSeriesList = seriesList + .sorted((s1, s2) -> { + final List<String> collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); + return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); + }); + + /** true == truncate all the labels to the greater of the size of their + * timespan indicator or the value of truncateWidth. false == don't truncate + * the labels, alow them to extend past the timespan indicator and off the + * edge of the screen */ + private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); + + /** the width to truncate all labels to if truncateAll is true. adjustable + * via slider if truncateAll is true */ + private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + + EventDetailChart(DateAxis dateAxis, final Axis<AggregateEvent> verticalAxis, ObservableList<AggregateEventNode> selectedNodes) { + super(dateAxis, verticalAxis); + dateAxis.setAutoRanging(false); + + //yAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm + verticalAxis.setTickLabelsVisible(false); + verticalAxis.setTickMarkVisible(false); + + setLegendVisible(false); + setPadding(Insets.EMPTY); + setAlternativeColumnFillVisible(true); + + //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly + getPlotChildren().add(nodeGroup); + + //bind listener to events that should trigger layout + widthProperty().addListener(layoutInvalidationListener); + heightProperty().addListener(layoutInvalidationListener); +// boundsInLocalProperty().addListener(layoutInvalidationListener); + bandByType.addListener(layoutInvalidationListener); + oneEventPerRow.addListener(layoutInvalidationListener); + truncateAll.addListener(layoutInvalidationListener); + truncateWidth.addListener(layoutInvalidationListener); + descrVisibility.addListener(layoutInvalidationListener); + + //this is needed to allow non circular binding of the guideline and timerangRect heights to the height of the chart + boundsInLocalProperty().addListener((Observable observable) -> { + setPrefHeight(boundsInLocalProperty().get().getHeight()); + }); + + //set up mouse listeners + final EventHandler<MouseEvent> clickHandler = (MouseEvent clickEvent) -> { + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { + + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new AbstractAction("Place Marker") { + { + setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); + } + + @Override + public void handle(ActionEvent ae) { +// + if (guideLine == null) { + guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis); + guideLine.relocate(clickEvent.getX(), 0); + guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty()))); + + getChartChildren().add(guideLine); + + guideLine.setOnMouseClicked((MouseEvent event) -> { + if (event.getButton() == MouseButton.SECONDARY) { + clearGuideLine(); + event.consume(); + } + }); + +// + } else { + + guideLine.relocate(clickEvent.getX(), 0); + } + } + }, new ActionGroup("Zoom History", new Back(controller), + new Forward(controller)))); + chartContextMenu.setAutoHide(true); + chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY()); + clickEvent.consume(); + } + }; + + setOnMouseClicked(clickHandler); + + //use one handler with an if chain because it maintains state + final ChartDragHandler<DateTime, EventDetailChart> dragHandler = new ChartDragHandler<>(this, getXAxis()); + setOnMousePressed(dragHandler); + setOnMouseReleased(dragHandler); + setOnMouseDragged(dragHandler); + + projectionMap.addListener((MapChangeListener.Change<? extends AggregateEventNode, ? extends Line> change) -> { + final Line valueRemoved = change.getValueRemoved(); + if (valueRemoved != null) { + getChartChildren().removeAll(valueRemoved); + } + final Line valueAdded = change.getValueAdded(); + if (valueAdded != null) { + getChartChildren().add(valueAdded); + } + }); + + this.selectedNodes = selectedNodes; + this.selectedNodes.addListener(( + ListChangeListener.Change<? extends AggregateEventNode> c) -> { + while (c.next()) { + c.getRemoved().forEach((AggregateEventNode t) -> { + projectionMap.remove(t); + }); + c.getAddedSubList().forEach((AggregateEventNode t) -> { + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + ); + line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5)); + line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); + line.setStrokeLineCap(StrokeLineCap.ROUND); + projectionMap.put(t, line); + }); + + } + + this.controller.selectEventIDs(selectedNodes.stream() + .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream()) + .collect(Collectors.toList())); + }); + + requestChartLayout(); + } + + @Override + public void clearIntervalSelector() { + getChartChildren().remove(intervalSelector); + intervalSelector = null; + } + + public synchronized SimpleBooleanProperty getBandByType() { + return bandByType; + } + + @Override + public final synchronized void setController(TimeLineController controller) { + this.controller = controller; + setModel(this.controller.getEventsModel()); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + filteredEvents.getRequestedZoomParamters().addListener(o -> { + clearGuideLine(); + clearIntervalSelector(); + + selectedNodes.clear(); + projectionMap.clear(); + controller.selectEventIDs(Collections.emptyList()); + }); + } + + @Override + public IntervalSelector<DateTime> newIntervalSelector(double x, Axis<DateTime> axis) { + return new DetailIntervalSelector(x, getHeight() - axis.getHeight() - axis.getTickLength(), axis, controller); + } + + synchronized void setBandByType(Boolean t1) { + bandByType.set(t1); + } + + /** get the DateTime along the x-axis that corresponds to the given + * x-coordinate in the coordinate system of this {@link EventDetailChart} + * + * @param x a x-coordinate in the space of this {@link EventDetailChart} + * + * @return the DateTime along the x-axis corresponding to the given x value + * (in the space of this {@link EventDetailChart} + */ + public final DateTime getDateTimeForPosition(double x) { + return getXAxis().getValueForDisplay(getXAxis().parentToLocal(x, 0).getX()); + } + + @Override + public IntervalSelector<? extends DateTime> getIntervalSelector() { + return intervalSelector; + } + + @Override + public void setIntervalSelector(IntervalSelector<? extends DateTime> newIntervalSelector) { + intervalSelector = newIntervalSelector; + getChartChildren().add(getIntervalSelector()); + } + + public synchronized SimpleBooleanProperty getOneEventPerRow() { + return oneEventPerRow; + } + + public synchronized SimpleBooleanProperty getTruncateAll() { + return truncateAll; + } + + synchronized void setEventOnePerRow(Boolean t1) { + oneEventPerRow.set(t1); + } + + synchronized void setTruncateAll(Boolean t1) { + truncateAll.set(t1); + + } + + @Override + protected synchronized void dataItemAdded(Series<DateTime, AggregateEvent> series, int i, Data<DateTime, AggregateEvent> data) { + final AggregateEvent aggEvent = data.getYValue(); + AggregateEventNode eventNode = nodeMap.get(aggEvent); + if (eventNode == null) { + eventNode = new AggregateEventNode(aggEvent, null); + eventNode.setOnMouseClicked(new EventMouseHandler(eventNode)); + + eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); + data.setNode(eventNode); + nodeMap.put(aggEvent, eventNode); + nodeGroup.getChildren().add(eventNode); + requiresLayout = true; + } + } + + @Override + protected synchronized void dataItemChanged(Data<DateTime, AggregateEvent> data) { + //TODO: can we use this to help with local detail level adjustment -jm + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected synchronized void dataItemRemoved(Data<DateTime, AggregateEvent> data, Series<DateTime, AggregateEvent> series) { + nodeMap.remove(data.getYValue()); + nodeGroup.getChildren().remove(data.getNode()); + data.setNode(null); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + } + + /** + * Layout the nodes representing events via the following algorithm. + * + * we start with a list of nodes (each representing an event) - sort the + * list of nodes by span start time of the underlying event - initialize + * empty map (maxXatY) from y-position to max used x-value - for each + * node: -- autosize the node (based on text label) -- get the event's start + * and end positions from the dateaxis -- size the capsule representing + * event duration -- starting from the top of the chart: --- (1)check if + * maxXatY is to the left of the start position: -------if maxXatY less than + * start position , good, put the current node here, mark end + * position as maxXatY, go to next node -------if maxXatY greater than start + * position, increment y position, do -------------check(1) again until + * maxXatY less than start position + */ + @Override + protected synchronized void layoutPlotChildren() { + + if (requiresLayout) { + setCursor(Cursor.WAIT); + double minY = 0; + + maxY.set(0.0); + + if (bandByType.get() == false) { + + ObservableList<Node> nodes = FXCollections.observableArrayList(nodeMap.values()); + FXCollections.sort(nodes, new StartTimeComparator()); + layoutNodes(nodes, minY, 0); +// layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0); + } else { + for (Series<DateTime, AggregateEvent> s : sortedSeriesList) { + ObservableList<Node> nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode)); + + FXCollections.sort(nodes, new StartTimeComparator()); + layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0); + minY = maxY.get(); + } + } + setCursor(null); + requiresLayout = false; + } + layoutProjectionMap(); + } + + @Override + protected synchronized void seriesAdded(Series<DateTime, AggregateEvent> series, int i) { + for (int j = 0; j < series.getData().size(); j++) { + dataItemAdded(series, j, series.getData().get(j)); + } + seriesList.add(series); + requiresLayout = true; + } + + @Override + protected synchronized void seriesRemoved(Series<DateTime, AggregateEvent> series) { + for (int j = 0; j < series.getData().size(); j++) { + dataItemRemoved(series.getData().get(j), series); + } + seriesList.remove(series); + requiresLayout = true; + } + + synchronized SimpleObjectProperty<DescriptionVisibility> getDescrVisibility() { + return descrVisibility; + } + + synchronized ReadOnlyDoubleProperty getMaxVScroll() { + return maxY.getReadOnlyProperty(); + } + + Iterable<AggregateEventNode> getNodes(Predicate<AggregateEventNode> p) { + List<AggregateEventNode> nodes = new ArrayList<>(); + + for (AggregateEventNode node : nodeMap.values()) { + checkNode(node, p, nodes); + } + + return nodes; + } + + synchronized SimpleDoubleProperty getTruncateWidth() { + return truncateWidth; + } + + synchronized void setVScroll(double d) { + final double h = maxY.get() - (getHeight() * .9); + nodeGroup.setTranslateY(-d * h); + } + + private void checkNode(AggregateEventNode node, Predicate<AggregateEventNode> p, List<AggregateEventNode> nodes) { + if (node != null) { + AggregateEvent event = node.getEvent(); + if (p.test(node)) { + nodes.add(node); + } + for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { + checkNode((AggregateEventNode) n, p, nodes); + } + } + } + + private void clearGuideLine() { + getChartChildren().remove(guideLine); + guideLine = null; + } + + /** + * layout the nodes in the given list, starting form the given minimum y + * coordinate. + * + * @param nodes + * @param minY + */ + private synchronized double layoutNodes(final List<Node> nodes, final double minY, final double xOffset) { + //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot + Map<Integer, Double> maxXatY = new HashMap<>(); + double localMax = minY; + //for each node lay size it and position it in first available slot + for (Node n : nodes) { + final AggregateEventNode tlNode = (AggregateEventNode) n; + tlNode.setDescriptionVisibility(descrVisibility.get()); + + AggregateEvent ie = tlNode.getEvent(); + final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis())); + //position of start and end according to range of axis + double xPos = rawDisplayPosition - xOffset; + double layoutNodesResultHeight = 0; + if (tlNode.getSubNodePane().getChildren().isEmpty() == false) { + FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator()); + layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition); + } + double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset; + double span = xPos2 - xPos; + + //size timespan border + tlNode.setSpanWidth(span); + if (truncateAll.get()) { //if truncate option is selected limit width of description label + tlNode.setDescriptionLabelMaxWidth(Math.max(span, truncateWidth.get())); + } else { //else set it unbounded + + tlNode.setDescriptionLabelMaxWidth(20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + } + tlNode.autosize(); //compute size of tlNode based on constraints and event data + + //get position of right edge of node ( influenced by description label) + double xRight = xPos + tlNode.getWidth(); + + //get the height of the node + final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight; + //initial test position + double yPos = minY; + + double yPos2 = yPos + h; + + if (oneEventPerRow.get()) { + // if onePerRow, just put it at end + yPos = (localMax + 2); + yPos2 = yPos + h; + + } else {//else + + boolean overlapping = true; + while (overlapping) { + //loop through y values looking for available slot. + + overlapping = false; + //check each pixel from bottom to top. + for (double y = yPos2; y >= yPos; y--) { + final Double maxX = maxXatY.get((int) y); + if (maxX != null && maxX >= xPos - 4) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yPos = y + 4; + yPos2 = yPos + h; + break; + } + } + } + //mark used y values + for (double y = yPos; y <= yPos2; y++) { + maxXatY.put((int) y, xRight); + } + } + localMax = Math.max(yPos2, localMax); + + Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), + new KeyValue(tlNode.layoutXProperty(), xPos), + new KeyValue(tlNode.layoutYProperty(), yPos))); + + tm.play(); +// tlNode.relocate(xPos, yPos); + } + maxY.set(Math.max(maxY.get(), localMax)); + return localMax - minY; + } + + private void layoutProjectionMap() { + for (final Map.Entry<AggregateEventNode, Line> entry : projectionMap.entrySet()) { + final AggregateEventNode aggNode = entry.getKey(); + final Line line = entry.getValue(); + + line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone()))); + line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone()))); + line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); + line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); + } + } + + private double getParentXForValue(DateTime dt) { + return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX(); + } + + private static final class DescriptionLODConverter extends StringConverter<Double> { + + @Override + public String toString(Double value) { + return value == -1 ? "None" + : DescriptionLOD.values()[value.intValue()].getDisplayName(); + } + + @Override + public Double fromString(String string) { + //we never convert from string to double (slider position) + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + } + + private static class StartTimeComparator implements Comparator<Node> { + + @Override + public int compare(Node n1, Node n2) { + + if (n1 == null) { + return 1; + } else if (n2 == null) { + return -1; + } else { + + return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(), + (((AggregateEventNode) n2).getEvent().getSpan().getStartMillis())); + } + } + + } + + private class DetailIntervalSelector extends IntervalSelector<DateTime> { + + public DetailIntervalSelector(double x, double height, Axis<DateTime> axis, TimeLineController controller) { + super(x, height, axis, controller); + } + + @Override + protected String formatSpan(DateTime date) { + return date.toString(TimeLineController.getZonedFormatter()); + } + + @Override + protected Interval adjustInterval(Interval i) { + return i; + } + + @Override + protected DateTime parseDateTime(DateTime date) { + return date; + } + + } + + /** event handler used for mouse events on {@link AggregateEventNode}s + * //TODO: refactor this to put more of the state(slider)in the node */ + private class EventMouseHandler implements EventHandler<MouseEvent> { + + private final AggregateEventNode aggNode; + + private final AggregateEvent aggEvent; + + private final Slider slider; + + public EventMouseHandler(AggregateEventNode aggNode) { + this.aggNode = aggNode; + this.aggEvent = aggNode.getEvent(); + + //configure slider + this.slider = new Slider(-1, 2, -1); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setSnapToTicks(true); + slider.setMajorTickUnit(1); + slider.setMinorTickCount(0); + slider.setBlockIncrement(1); + slider.setLabelFormatter(new DescriptionLODConverter()); + + //on slider change, reload subnodes + InvalidationListener invalidationListener = o -> { + if (slider.isValueChanging() == false) { + reloadSubNodes(); + } + }; + slider.valueProperty().addListener(invalidationListener); + slider.valueChangingProperty().addListener(invalidationListener); + } + + private void reloadSubNodes() { + final int value = Math.round(slider.valueProperty().floatValue()); + aggNode.getSubNodePane().getChildren().clear(); + aggNode.setEventDetailsVisible(true); + if (value == -1) { + aggNode.getSubNodePane().getChildren().clear(); + aggNode.setEventDetailsVisible(true); + synchronized (EventDetailChart.this) { + requiresLayout = true; + } + requestChartLayout(); + } else { + final DescriptionLOD newLOD = DescriptionLOD.values()[value]; + + final Filter combinedFilter = Filter.intersect(new Filter[]{new TextFilter(aggEvent.getDescription()), + new TypeFilter(aggEvent.getType()), + filteredEvents.filter().get()}); + final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000); + LoggedTask<List<AggregateEventNode>> loggedTask = new LoggedTask<List<AggregateEventNode>>("Load sub events", true) { + + @Override + protected List<AggregateEventNode> call() throws Exception { + + List<AggregateEvent> aggregatedEvents = filteredEvents.getAggregatedEvents(new ZoomParams(span, + filteredEvents.eventTypeZoom().get(), + combinedFilter, + newLOD)); + return aggregatedEvents.stream().map((AggregateEvent t) -> { + AggregateEventNode subNode = new AggregateEventNode(t, aggNode); + subNode.setOnMouseClicked(new EventMouseHandler(subNode)); + subNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(t.getSpan().getStartMillis())) - aggNode.getLayoutXCompensation()); + return subNode; + }).collect(Collectors.toList()); + } + + @Override + protected void succeeded() { + try { + if (get().size() > 1) { + setCursor(Cursor.WAIT); + aggNode.setEventDetailsVisible(false); + aggNode.getSubNodePane().getChildren().setAll(get()); + synchronized (EventDetailChart.this) { + requiresLayout = true; + } + requestChartLayout(); + setCursor(null); + } + } catch (InterruptedException | ExecutionException ex) { + Exceptions.printStackTrace(ex); + } + } + }; + controller.monitorTask(loggedTask); + } + } + + @Override + public void handle(MouseEvent t) { + t.consume(); + if (t.getButton() == MouseButton.PRIMARY) { + if (t.isShiftDown()) { + if (selectedNodes.contains(aggNode) == false) { + selectedNodes.add(aggNode); + } + } else if (t.isShortcutDown()) { + selectedNodes.removeAll(aggNode); + } else if (t.getClickCount() > 1) { + slider.increment(); + } else { + selectedNodes.setAll(aggNode); + } + } else if (t.getButton() == MouseButton.SECONDARY) { + + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + //we use a per node menu to remember the slider position + ContextMenu nodeContextMenu = aggNode.getContextMenu(); + if (nodeContextMenu == null) { + nodeContextMenu = builContextMenu(); + aggNode.setContextMenu(nodeContextMenu); + } + nodeContextMenu.show(aggNode, t.getScreenX(), t.getScreenY()); + } + } + + private ContextMenu builContextMenu() { + //should we include a label to remind uer of what group this is for + //final MenuItem headingItem = new CustomMenuItem(new Label(aggEvent.getDescription()), false); + //headingItem.getStyleClass().remove("menu-item"); + final Label sliderLabel = new Label("Nested Detail:", slider); + sliderLabel.setContentDisplay(ContentDisplay.RIGHT); + final MenuItem detailSliderItem = new CustomMenuItem(sliderLabel, false); + detailSliderItem.getStyleClass().remove("menu-item"); + ContextMenu contextMenu = new ContextMenu(detailSliderItem); + //we don't reuse item from chartContextMenu because 'place marker' is location specific. + //TODO: refactor this so we can reuse chartContextMenu items + contextMenu.getItems().addAll(ActionUtils.createContextMenu( + Arrays.asList(new ActionGroup("Zoom History", new Back(controller), + new Forward(controller)))).getItems()); + //TODO: add tagging actions here + contextMenu.setAutoHide(true); + return contextMenu; + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java new file mode 100644 index 0000000000000000000000000000000000000000..febf055aada0cd8468682df3207396a5807d1a9c --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import javafx.scene.Cursor; +import javafx.scene.control.Tooltip; +import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Color; +import javafx.scene.shape.Line; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.timeline.TimeLineController; + +/** + * + */ +class GuideLine extends Line { + + private final DateAxis dateAxis; + + private double startLayoutX; + + protected Tooltip tooltip; + + private double dragStartX = 0; + + GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) { + super(startX, startY, endX, endY); + dateAxis = axis; + setCursor(Cursor.E_RESIZE); + getStrokeDashArray().setAll(5.0, 5.0); + setStroke(Color.RED); + setOpacity(.5); + setStrokeWidth(3); + + setOnMouseEntered((MouseEvent event) -> { + setTooltip(); + }); + + setOnMousePressed((MouseEvent event) -> { + startLayoutX = getLayoutX(); + dragStartX = event.getScreenX(); + }); + setOnMouseDragged((MouseEvent event) -> { + double dX = event.getScreenX() - dragStartX; + + relocate(startLayoutX + dX, 0); + }); + } + + private void setTooltip() { + Tooltip.uninstall(this, tooltip); + tooltip = new Tooltip(formatSpan(getDateTime()) + + "\nRight-click to remove." + + "\nRight-drag to reposition."); + Tooltip.install(this, tooltip); + } + + private String formatSpan(DateTime date) { + return date.toString(TimeLineController.getZonedFormatter()); + } + + private DateTime getDateTime() { + return dateAxis.getValueForDisplay(dateAxis.parentToLocal(getLayoutX(), 0).getX()); + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..7a662c28cd3611cebf29172f66a177f50d591ee5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.util.Comparator; +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; + +/** + * + */ +class EventDescriptionTreeItem extends NavTreeItem { + + public EventDescriptionTreeItem(AggregateEvent g) { + setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size())); + } + + @Override + public int getCount() { + return getValue().getCount(); + } + + @Override + public void insert(AggregateEvent g) { + NavTreeNode value = getValue(); + if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { + throw new IllegalArgumentException(); + } + + setValue(new NavTreeNode(value.getType().getBaseType(), value.getDescription(), value.getCount() + g.getEventIDs().size())); + } + + @Override + public void resort(Comparator<TreeItem<NavTreeNode>> comp) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) { + if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { + return this; + } + return null; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..ba2367e3ea23cba319d13b710bad8ff901b38867 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -0,0 +1,94 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; + +class EventTypeTreeItem extends NavTreeItem { + + /** maps a description to the child item of this item with that description */ + private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>(); + + private final Comparator<TreeItem<NavTreeNode>> comparator = TreeComparator.Description; + + EventTypeTreeItem(AggregateEvent g) { + setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0)); + } + + @Override + public int getCount() { + return getValue().getCount(); + } + + /** Recursive method to add a grouping at a given path. + * + * @param path Full path (or subset not yet added) to add + * @param g Group to add + * @param tree True if it is part of a tree (versus a list) + */ + @Override + public void insert(AggregateEvent g) { + + EventDescriptionTreeItem treeItem = childMap.get(g.getDescription()); + if (treeItem == null) { + final EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(g); + newTreeItem.setExpanded(true); + childMap.put(g.getDescription(), newTreeItem); + + Platform.runLater(() -> { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + FXCollections.sort(getChildren(), comparator); + } + }); + } else { + treeItem.insert(g); + } + Platform.runLater(() -> { + NavTreeNode value1 = getValue(); + setValue(new NavTreeNode(value1.getType().getBaseType(), value1.getType().getBaseType().getDisplayName(), childMap.values().stream().mapToInt(EventDescriptionTreeItem::getCount).sum())); + }); + + } + + @Override + public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) { + if (t.getType().getBaseType() == getValue().getType().getBaseType()) { + + for (TreeItem<NavTreeNode> child : getChildren()) { + final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + if (findTreeItemForEvent != null) { + return findTreeItemForEvent; + } + } + } + return null; + } + + @Override + public void resort(Comparator<TreeItem<NavTreeNode>> comp) { + FXCollections.sort(getChildren(), comp); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..a1457451eba5613a21a1fa43655614092ac94181 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.fxml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.collections.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> + + + <fx:root prefHeight="-1.0" prefWidth="-1.0" type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <center> + <TreeView fx:id="eventsTree" prefHeight="-1.0" prefWidth="-1.0" /> + </center> + <top> + <ToolBar> + <items> + <Label text="Sort By:" /> + <ComboBox fx:id="sortByBox"> + <items> + <FXCollections fx:factory="observableArrayList"> + <String fx:value="Item 1" /> + <String fx:value="Item 2" /> + <String fx:value="Item 3" /> + </FXCollections> + </items> + </ComboBox> + </items> + </ToolBar> + </top> + </fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..dd368a5257318bba27804eb3adaf4d292d141f91 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -0,0 +1,161 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.net.URL; +import java.util.Arrays; +import java.util.Comparator; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.shape.Rectangle; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; + +/** + * Display two trees. one shows all folders (groups) and calls out folders with + * images. the user can select folders with images to see them in the main + * GroupListPane The other shows folders with hash set hits. + */ +public class NavPanel extends BorderPane implements TimeLineView { + + private TimeLineController controller; + + private FilteredEventsModel filteredEvents; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + private DetailViewPane detailViewPane; + + /** + * TreeView for folders with hash hits + */ + @FXML + private TreeView< NavTreeNode> eventsTree; + + @FXML + private ComboBox<Comparator<TreeItem<NavTreeNode>>> sortByBox; + + public NavPanel() { + + FXMLConstructor.construct(this, "NavPanel.fxml"); + } + + public void setChart(DetailViewPane detailViewPane) { + this.detailViewPane = detailViewPane; + detailViewPane.setSelectionModel(eventsTree.getSelectionModel()); + setRoot(); + detailViewPane.getAggregatedEvents().addListener((Observable observable) -> { + setRoot(); + }); + detailViewPane.getSelectedNodes().addListener((Observable observable) -> { + eventsTree.getSelectionModel().clearSelection(); + detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> { + eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent())); + }); + }); + + } + + private void setRoot() { + RootItem root = new RootItem(); + final ObservableList<AggregateEvent> aggregatedEvents = detailViewPane.getAggregatedEvents(); + + synchronized (aggregatedEvents) { + for (AggregateEvent agg : aggregatedEvents) { + root.insert(agg); + } + } + Platform.runLater(() -> { + eventsTree.setRoot(root); + }); + } + + @Override + public void setController(TimeLineController controller) { + this.controller = controller; + setModel(controller.getEventsModel()); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + + } + + @FXML + void initialize() { + assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'."; + + sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count)); + sortByBox.getSelectionModel().select(TreeComparator.Description); + sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { + ((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem()); + }); + eventsTree.setShowRoot(false); + eventsTree.setCellFactory((TreeView<NavTreeNode> p) -> new EventTreeCell()); + eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + } + + /** A tree cell to display {@link NavTreeNode}s. Shows the description, and + * count, as well a a "legend icon" for the event type. */ + private static class EventTreeCell extends TreeCell<NavTreeNode> { + + @Override + protected void updateItem(NavTreeNode item, boolean empty) { + super.updateItem(item, empty); + if (item != null) { + final String text = item.getDescription() + " (" + item.getCount() + ")"; + setText(text); + setTooltip(new Tooltip(text)); + Rectangle rect = new Rectangle(24, 24); + rect.setArcHeight(5); + rect.setArcWidth(5); + rect.setStrokeWidth(2); + rect.setStroke(item.getType().getColor()); + rect.setFill(item.getType().getColor().deriveColor(0, 1, 1, 0.1)); + setGraphic(new StackPane(rect, new ImageView(item.getType().getFXImage()))); + } else { + setText(null); + setTooltip(null); + setGraphic(null); + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..c880c29b36799a796bc5a6d43ad0adc95cdf0e73 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -0,0 +1,39 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.util.Comparator; +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; + +/** A node in the nav tree. Manages inserts and resorts. Has parents + * and children. Does not have graphical properties these are configured in + * {@link EventTreeCell}. Each GroupTreeItem has a NavTreeNode which has a type, + * description , and count */ +abstract class NavTreeItem extends TreeItem<NavTreeNode> { + + abstract void insert(AggregateEvent g); + + abstract int getCount(); + + abstract void resort(Comparator<TreeItem<NavTreeNode>> comp); + + abstract TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t); + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java new file mode 100644 index 0000000000000000000000000000000000000000..46f7689654c8d468878efc50a32bec3b047ea8c7 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.timeline.events.type.EventType; + +/** The data item for the nav tree. Represents a combination of type and + * description, as well as the corresponding number of events */ +@Immutable +public class NavTreeNode { + + final private EventType type; + + final private String Description; + + final private int count; + + public EventType getType() { + return type; + } + + public String getDescription() { + return Description; + } + + public int getCount() { + return count; + } + + public NavTreeNode(EventType type, String Description, int count) { + this.type = type; + this.Description = Description; + this.count = count; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java new file mode 100644 index 0000000000000000000000000000000000000000..f15f66f31b1c25e03ddcf963e9516526726b933e --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -0,0 +1,92 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.events.type.EventType; +import org.sleuthkit.autopsy.timeline.events.AggregateEvent; + +/** + * + */ +class RootItem extends NavTreeItem { + + /** maps a description to the child item of this item with that description */ + private final Map<EventType, EventTypeTreeItem> childMap = new HashMap<>(); + + /** the comparator if any used to sort the children of this item */ +// private TreeNodeComparators comp; + RootItem() { + + } + + @Override + public int getCount() { + return getValue().getCount(); + } + + /** Recursive method to add a grouping at a given path. + * + * @param g Group to add + */ + @Override + public void insert(AggregateEvent g) { + + EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType()); + if (treeItem == null) { + final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); + newTreeItem.setExpanded(true); + childMap.put(g.getType().getBaseType(), newTreeItem); + newTreeItem.insert(g); + + Platform.runLater(() -> { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + + FXCollections.sort(getChildren(), TreeComparator.Type); + } + }); + } else { + treeItem.insert(g); + } + } + + @Override + public void resort(Comparator<TreeItem<NavTreeNode>> comp) { + childMap.values().forEach((ti) -> { + ti.resort(comp); + }); + } + + @Override + public TreeItem<NavTreeNode> findTreeItemForEvent(AggregateEvent t) { + for (TreeItem<NavTreeNode> child : getChildren()) { + final TreeItem<NavTreeNode> findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + if (findTreeItemForEvent != null) { + return findTreeItemForEvent; + } + } + return null; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java new file mode 100644 index 0000000000000000000000000000000000000000..5da9a49d4251acd52a7938f369bb7c1f3c6048bd --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -0,0 +1,47 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview.tree; + +import java.util.Comparator; +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.events.type.EventType; + +enum TreeComparator implements Comparator<TreeItem<NavTreeNode>> { + + Description { + @Override + public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) { + return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); + } + }, + Count { + @Override + public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) { + + return -Integer.compare(o1.getValue().getCount(), o2.getValue().getCount()); + } + }, + Type { + @Override + public int compare(TreeItem<NavTreeNode> o1, TreeItem<NavTreeNode> o2) { + return EventType.getComparator().compare(o1.getValue().getType(), o2.getValue().getType()); + } + }; + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java new file mode 100644 index 0000000000000000000000000000000000000000..026ca01b89d472fdd67bca2649cd164f24441ad3 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.filtering; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.TreeTableCell; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; + +/** + * + */ +class FilterCheckBoxCell extends TreeTableCell<AbstractFilter, AbstractFilter> { + + private CheckBox checkBox; + + public FilterCheckBoxCell() { + } + + @Override + protected void updateItem(AbstractFilter item, boolean empty) { + super.updateItem(item, empty); + + if (item == null) { + setText(null); + setGraphic(null); + checkBox = null; + } else { + setText(item.getDisplayName()); + + checkBox = new CheckBox(); + checkBox.selectedProperty().bindBidirectional(item.getActiveProperty()); + checkBox.disableProperty().bind(item.getDisabledProperty()); + setGraphic(checkBox); + } + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..e3e1c13947802436319bf5819517d6217914cf09 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> +<top><ToolBar prefWidth="200.0" BorderPane.alignment="CENTER"> +<items> + <Button fx:id="applyButton" mnemonicParsing="false" text="Apply"> +<HBox.margin> +<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> +</HBox.margin> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../../images/tick.png" /> +</image></ImageView> +</graphic></Button><Button fx:id="defaultButton" mnemonicParsing="false" text="Default"> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../../images/arrow-circle-double-135.png" /> +</image></ImageView> +</graphic></Button> +</items> +</ToolBar> +</top> +<center><TreeTableView fx:id="filterTreeTable" editable="true" minWidth="-Infinity" showRoot="false" BorderPane.alignment="CENTER"> + <columns> + <TreeTableColumn fx:id="treeColumn" minWidth="100.0" prefWidth="200.0" sortable="false" text="C1" /> + <TreeTableColumn fx:id="legendColumn" editable="false" minWidth="50.0" prefWidth="50.0" sortable="false" text="C2" /> + </columns> +<columnResizePolicy> +<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" /> +</columnResizePolicy> +</TreeTableView> +</center></fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..89f379f80f53969716b9b0bfe7fb7e36e825856e --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -0,0 +1,172 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.filtering; + +import java.net.URL; +import java.util.ResourceBundle; +import javafx.beans.Observable; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeTableColumn; +import javafx.scene.control.TreeTableRow; +import javafx.scene.control.TreeTableView; +import javafx.scene.layout.BorderPane; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.actions.DefaultFilters; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.Filter; + +/** The FXML controller for the filter ui. + * + * This also implements {@link TimeLineView} since it dynamically updates its + * filters based on the contents of a {@link FilteredEventsModel} + */ +public class FilterSetPanel extends BorderPane implements TimeLineView { + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private Button applyButton; + + @FXML + private Button defaultButton; + + @FXML + private TreeTableView<Filter> filterTreeTable; + + @FXML + private TreeTableColumn<AbstractFilter, AbstractFilter> treeColumn; + + @FXML + private TreeTableColumn<AbstractFilter, AbstractFilter> legendColumn; + + private FilteredEventsModel filteredEvents; + + private TimeLineController controller; + + @FXML + void initialize() { + assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; + + applyButton.setOnAction(e -> { + controller.pushFilters(filterTreeTable.getRoot().getValue().copyOf()); + }); + + //remove column headers via css. + filterTreeTable.getStylesheets().addAll(getClass().getResource("FilterTable.css").toExternalForm()); + + //use row factory as hook to attach context menus to. + filterTreeTable.setRowFactory((TreeTableView<Filter> param) -> { + final TreeTableRow<Filter> row = new TreeTableRow<>(); + + MenuItem all = new MenuItem("all"); + all.setOnAction(e -> { + row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> { + t.getValue().setActive(Boolean.TRUE); + }); + }); + MenuItem none = new MenuItem("none"); + none.setOnAction(e -> { + row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> { + t.getValue().setActive(Boolean.FALSE); + }); + }); + + MenuItem only = new MenuItem("only"); + only.setOnAction(e -> { + row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> { + if (t == row.getTreeItem()) { + t.getValue().setActive(Boolean.TRUE); + } else { + t.getValue().setActive(Boolean.FALSE); + } + }); + }); + MenuItem others = new MenuItem("others"); + others.setOnAction(e -> { + row.getTreeItem().getParent().getChildren().forEach((TreeItem<Filter> t) -> { + if (t == row.getTreeItem()) { + t.getValue().setActive(Boolean.FALSE); + } else { + t.getValue().setActive(Boolean.TRUE); + } + }); + }); + final ContextMenu rowMenu = new ContextMenu(); + Menu select = new Menu("select"); + select.setOnAction(e -> { + row.getItem().setActive(!row.getItem().isActive()); + }); + select.getItems().addAll(all, none, only, others); + rowMenu.getItems().addAll(select); + row.setContextMenu(rowMenu); + + return row; + }); + + //configure tree column to show name of filter and checkbox + treeColumn.setCellValueFactory(param -> param.getValue().valueProperty()); + treeColumn.setCellFactory(col -> new FilterCheckBoxCell()); + + //configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter) + legendColumn.setCellValueFactory(param -> param.getValue().valueProperty()); + legendColumn.setCellFactory(col -> new LegendCell(this.controller)); + } + + public FilterSetPanel() { + FXMLConstructor.construct(this, "FilterSetPanel.fxml"); + } + + @Override + public void setController(TimeLineController timeLineController) { + this.controller = timeLineController; + AbstractAction defaultFiltersAction = new DefaultFilters(controller); + defaultButton.setOnAction(defaultFiltersAction); + defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); + this.setModel(timeLineController.getEventsModel()); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + + refresh(); + + this.filteredEvents.filter().addListener((Observable o) -> { + refresh(); + }); + + } + + public void refresh() { + filterTreeTable.setRoot(new FilterTreeItem(this.filteredEvents.filter().get().copyOf())); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css new file mode 100644 index 0000000000000000000000000000000000000000..fa3c5a76dbc43e0c298330ae1c379de8e9d31cef --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTable.css @@ -0,0 +1,3 @@ + + +.column-header-background { visibility: hidden; -fx-padding: -1em; } diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..2302099939f36fc1ae3a4ae17f4e82172cc7e5a1 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java @@ -0,0 +1,31 @@ +package org.sleuthkit.autopsy.timeline.ui.filtering; + +import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.filters.CompoundFilter; +import org.sleuthkit.autopsy.timeline.filters.Filter; + +/** A TreeItem for a filter. */ +public class FilterTreeItem extends TreeItem<Filter> { + + /** + * recursively construct a tree of treeitems to parallel the filter tree of + * the given filter + * + * + * @param f the filter for this item. if f has sub-filters, tree items will + * be made for them added added to the children of this + * FilterTreeItem + */ + public FilterTreeItem(Filter f) { + super(f); + setExpanded(true); + + if (f instanceof CompoundFilter) { + CompoundFilter cf = (CompoundFilter) f; + + for (Filter af : cf.getSubFilters()) { + getChildren().add(new FilterTreeItem(af)); + } + } + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java new file mode 100644 index 0000000000000000000000000000000000000000..23e6f25055f66fec680444a5a912b9472f229bf0 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/ui/filtering/LegendCell.java @@ -0,0 +1,135 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui.filtering; + +import javafx.application.Platform; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Pos; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.TextField; +import javafx.scene.control.TreeTableCell; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.TextFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; + +/** + * A TreeTableCell that shows an icon and color corresponding to the + * represented filter + */ +class LegendCell extends TreeTableCell<AbstractFilter, AbstractFilter> implements TimeLineView { + + private static final Color CLEAR = Color.rgb(0, 0, 0, 0); + + private TimeLineController controller; + + private FilteredEventsModel filteredEvents; + + //We need a controller so we can listen to changes in EventTypeZoom to show/hide legends + public LegendCell(TimeLineController controller) { + setEditable(false); + setController(controller); + } + + @Override + public void updateItem(AbstractFilter item, boolean empty) { + super.updateItem(item, empty); + if (item == null) { + Platform.runLater(() -> { + setGraphic(null); + setBackground(null); + }); + } else { + + //TODO: have the filter return an appropriate node, rather than use instanceof + if (item instanceof TypeFilter) { + TypeFilter filter = (TypeFilter) item; + Rectangle rect = new Rectangle(20, 20); + + rect.setArcHeight(5); + rect.setArcWidth(5); + rect.setStrokeWidth(3); + setLegendColor(filter, rect, this.filteredEvents.getEventTypeZoom()); + this.filteredEvents.eventTypeZoom().addListener(( + ObservableValue<? extends EventTypeZoomLevel> observable, + EventTypeZoomLevel oldValue, + EventTypeZoomLevel newValue) -> { + setLegendColor(filter, rect, newValue); + }); + + HBox hBox = new HBox(new Rectangle(filter.getEventType().getZoomLevel().ordinal() * 10, 5, CLEAR), + new ImageView(((TypeFilter) item).getFXImage()), rect + ); + hBox.setAlignment(Pos.CENTER); + Platform.runLater(() -> { + setGraphic(hBox); + setContentDisplay(ContentDisplay.CENTER); + }); + + } else if (item instanceof TextFilter) { + TextFilter f = (TextFilter) item; + TextField textField = new TextField(); + textField.setPromptText("enter filter string"); + textField.textProperty().bindBidirectional(f.textProperty()); + Platform.runLater(() -> { + setGraphic(textField); + }); + + } else { + Platform.runLater(() -> { + setGraphic(null); + setBackground(null); + }); + } + } + } + + private void setLegendColor(TypeFilter filter, Rectangle rect, EventTypeZoomLevel eventTypeZoom) { + //only show legend color if filter is of the same zoomlevel as requested in filteredEvents + if (eventTypeZoom.equals(filter.getEventType().getZoomLevel())) { + Platform.runLater(() -> { + rect.setStroke(filter.getEventType().getSuperType().getColor()); + rect.setFill(filter.getColor()); + }); + } else { + Platform.runLater(() -> { + rect.setStroke(CLEAR); + rect.setFill(CLEAR); + }); + } + } + + @Override + synchronized public final void setController(TimeLineController controller) { + this.controller = controller; + setModel(this.controller.getEventsModel()); + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java new file mode 100644 index 0000000000000000000000000000000000000000..8a7e614f7d35ef9ca960815f91b3cc7c7f0c7085 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ColorUtilities.java @@ -0,0 +1,36 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.utils; + +import javafx.scene.paint.Color; + +/** + * + */ +public class ColorUtilities { + + + public static String getRGBCode(Color color) { + return String.format("#%02X%02X%02X%02X", + (int) (color.getRed() * 255), + (int) (color.getGreen() * 255), + (int) (color.getBlue() * 255), + (int) (color.getOpacity() * 255)); + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..ab03330b2c7f57eee4af9750d9008d10d84085df --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java @@ -0,0 +1,83 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.utils; + +import java.util.Collection; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.joda.time.ReadablePeriod; +import org.sleuthkit.autopsy.timeline.zooming.TimeUnits; + +/** + * + */ +public class IntervalUtils { + + static public Interval getSpanningInterval(Collection<DateTime> times) { + Interval trange = null; + for (DateTime t : times) { + if (trange == null) { + trange = new Interval(t.getMillis(), t.getMillis() + 1000,DateTimeZone.UTC); + } else { + trange = extendInterval(trange, t.getMillis()); + } + } + return trange; + } + + static public Interval span(Interval range, final Interval range2) { + return new Interval(Math.min(range.getStartMillis(), range2.getStartMillis()), Math.max(range.getEndMillis(), range2.getEndMillis()),DateTimeZone.UTC); + } + + static public Interval extendInterval(Interval range, final Long eventTime) { + return new Interval(Math.min(range.getStartMillis(), eventTime), Math.max(range.getEndMillis(), eventTime + 1),DateTimeZone.UTC); + } + + /** + * @param timeRange + * + * @return + * + * @deprecated Moved to + * {@link org.sleuthkit.autopsy.timeline.visualization.RangeDivisionInfo#getRangeDivisionInfo} + */ + @Deprecated + public static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange) { + return RangeDivisionInfo.getRangeDivisionInfo(timeRange); + } + + public static DateTime middleOf(Interval interval) { + return new DateTime((interval.getStartMillis() + interval.getEndMillis()) / 2); + } + + public static Interval getAdjustedInterval(Interval oldInterval, TimeUnits requestedUnit) { + return getIntervalAround(middleOf(oldInterval), requestedUnit.getPeriod()); + } + + static public Interval getIntervalAround(DateTime aroundInstant, ReadablePeriod period) { + DateTime start = aroundInstant.minus(period); + DateTime end = aroundInstant.plus(period); + Interval range = new Interval(start, end); + DateTime middleOf = IntervalUtils.middleOf(range); + long halfRange = range.toDurationMillis() / 4; + final Interval newInterval = new Interval(middleOf.minus(halfRange), middleOf.plus(halfRange)); + return newInterval; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java new file mode 100644 index 0000000000000000000000000000000000000000..142434d5ac428ade4eb11954c8173501a0d528c5 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/ObservableStack.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.utils; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +/** + * + */ +public class ObservableStack<T> extends SimpleListProperty<T> { + + public ObservableStack() { + super(FXCollections.synchronizedObservableList(FXCollections.observableArrayList())); + } + + public void push(T item) { + synchronized (this) { + add(0, item); + } + } + + public T pop() { + synchronized (this) { + if (isEmpty()) { + return null; + } else { + return remove(0); + } + } + } + + public T peek() { + synchronized (this) { + if (isEmpty()) { + return null; + } else { + return get(0); + } + } + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..5be09e376dc1b9dafa1f059ff1459bb8f61002ce --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/utils/RangeDivisionInfo.java @@ -0,0 +1,151 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.utils; + +import javax.annotation.concurrent.Immutable; +import org.joda.time.DateTime; +import org.joda.time.DateTimeFieldType; +import org.joda.time.Days; +import org.joda.time.Hours; +import org.joda.time.Interval; +import org.joda.time.Minutes; +import org.joda.time.Months; +import org.joda.time.Seconds; +import org.joda.time.Years; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.zooming.TimeUnits; + +/** * bundles up the results of analyzing a time range for the appropriate + * {@link TimeUnits} to use to visualize it. Partly, this class exists + * so I don't have to have more member variables in other places , and partly + * because I can only return a single value from a function. This might only be + * a temporary design but is working well for now. */ +@Immutable +public class RangeDivisionInfo { + + /** the size of the periods we should divide the interval into */ + private final TimeUnits blockSize; + + /** The number of Blocks we are going to divide the interval into. */ + private final int numberOfBlocks; + + /** a {@link DateTimeFormatter} corresponding to the block size for the + * tick marks on the date axis of the graph */ + private final DateTimeFormatter tickFormatter; + + /** an adjusted lower bound for the range such that is lines up with a block + * boundary before or at the start of the timerange */ + private final long lowerBound; + + /** an adjusted upper bound for the range such that is lines up with a block + * boundary at or after the end of the timerange */ + private final long upperBound; + + /** the time range this {@link RangeDivisionInfo} describes */ + private final Interval timeRange; + + public Interval getTimeRange() { + return timeRange; + } + + private RangeDivisionInfo(Interval timeRange, int periodsInRange, TimeUnits periodSize, DateTimeFormatter tickformatter, long lowerBound, long upperBound) { + super(); + this.numberOfBlocks = periodsInRange; + this.blockSize = periodSize; + this.tickFormatter = tickformatter; + + this.lowerBound = lowerBound; + this.upperBound = upperBound; + this.timeRange = timeRange; + } + + /** + * Static factory method. + * + * Determine the period size, number of periods, whole period bounds, and + * formatters to use to visualize the given timerange. + * + * @param timeRange + * + * @return + */ + public static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange) { + //Check from largest to smallest unit + + //TODO: make this more generic... reduce code duplication -jm + DateTimeFieldType timeUnit; + final DateTime startWithZone = timeRange.getStart().withZone(TimeLineController.getJodaTimeZone()); + final DateTime endWithZone = timeRange.getEnd().withZone(TimeLineController.getJodaTimeZone()); + + if (Years.yearsIn(timeRange).isGreaterThan(Years.THREE)) { + timeUnit = DateTimeFieldType.year(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Years.yearsIn(timeRange).get(timeUnit.getDurationType()) + 1, TimeUnits.YEARS, ISODateTimeFormat.year(), lower, upper); + } else if (Months.monthsIn(timeRange).isGreaterThan(Months.THREE)) { + timeUnit = DateTimeFieldType.monthOfYear(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Months.monthsIn(timeRange).getMonths() + 1, TimeUnits.MONTHS, DateTimeFormat.forPattern("YYYY'-'MMMM"), lower, upper); + } else if (Days.daysIn(timeRange).isGreaterThan(Days.THREE)) { + timeUnit = DateTimeFieldType.dayOfMonth(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Days.daysIn(timeRange).getDays() + 1, TimeUnits.DAYS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd"), lower, upper); + } else if (Hours.hoursIn(timeRange).isGreaterThan(Hours.THREE)) { + timeUnit = DateTimeFieldType.hourOfDay(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Hours.hoursIn(timeRange).getHours() + 1, TimeUnits.HOURS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH"), lower, upper); + } else if (Minutes.minutesIn(timeRange).isGreaterThan(Minutes.THREE)) { + timeUnit = DateTimeFieldType.minuteOfHour(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Minutes.minutesIn(timeRange).getMinutes() + 1, TimeUnits.MINUTES, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm"), lower, upper); + } else { + timeUnit = DateTimeFieldType.secondOfMinute(); + long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis(); + long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis(); + return new RangeDivisionInfo(timeRange, Seconds.secondsIn(timeRange).getSeconds() + 1, TimeUnits.SECONDS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm':'ss"), lower, upper); + } + } + + public DateTimeFormatter getTickFormatter() { + return tickFormatter; + } + + public int getPeriodsInRange() { + return numberOfBlocks; + } + + public TimeUnits getPeriodSize() { + return blockSize; + } + + public long getUpperBound() { + return upperBound; + } + + public long getLowerBound() { + return lowerBound; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java new file mode 100644 index 0000000000000000000000000000000000000000..be12be9825b3c33642debcd2645bf9f47bdd9046 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java @@ -0,0 +1,37 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.zooming; + +/** + * + */ +public enum DescriptionLOD { + + SHORT("Short"), MEDIUM("Medium"), FULL("Full"); + + private final String displayName; + + public String getDisplayName() { + return displayName; + } + + private DescriptionLOD(String displayName) { + this.displayName = displayName; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..854f7d73abd7daac3c7a3eb069283d4eead97961 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/EventTypeZoomLevel.java @@ -0,0 +1,38 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.zooming; + +/** + * + * + */ +public enum EventTypeZoomLevel { + + ROOT_TYPE("Root Type"), BASE_TYPE("Base Type"), SUB_TYPE("Sub Type"); + + public String getDisplayName() { + return displayName; + } + + private final String displayName; + + private EventTypeZoomLevel(String displayName) { + this.displayName = displayName; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java new file mode 100644 index 0000000000000000000000000000000000000000..91c97cc981570e5c41fa1e298755fffcc16a8a38 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/TimeUnits.java @@ -0,0 +1,88 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.zooming; + +import java.time.temporal.ChronoUnit; +import org.joda.time.Days; +import org.joda.time.Hours; +import org.joda.time.Minutes; +import org.joda.time.Months; +import org.joda.time.Period; +import org.joda.time.Seconds; +import org.joda.time.Years; + +/** predefined units of time for use in choosing axis labels and sub intervals. */ +public enum TimeUnits { + + FOREVER(null, ChronoUnit.FOREVER), + YEARS(Years.ONE.toPeriod(), ChronoUnit.YEARS), + MONTHS(Months.ONE.toPeriod(), ChronoUnit.MONTHS), + DAYS(Days.ONE.toPeriod(), ChronoUnit.DAYS), + HOURS(Hours.ONE.toPeriod(), ChronoUnit.HOURS), + MINUTES(Minutes.ONE.toPeriod(), ChronoUnit.MINUTES), + SECONDS(Seconds.ONE.toPeriod(), ChronoUnit.SECONDS); + + public static TimeUnits fromChronoUnit(ChronoUnit chronoUnit) { + switch (chronoUnit) { + + case FOREVER: + return FOREVER; + case ERAS: + case MILLENNIA: + case CENTURIES: + case DECADES: + case YEARS: + return YEARS; + case MONTHS: + return MONTHS; + case WEEKS: + case DAYS: + return DAYS; + case HOURS: + case HALF_DAYS: + return HOURS; + case MINUTES: + return MINUTES; + case SECONDS: + case MILLIS: + case MICROS: + case NANOS: + return SECONDS; + default: + return YEARS; + } + } + + private final Period p; + + private final ChronoUnit cu; + + public Period getPeriod() { + return p; + } + + public ChronoUnit getChronoUnit() { + return cu; + } + + private TimeUnits(Period p, ChronoUnit cu) { + this.p = p; + this.cu = cu; + } +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java new file mode 100644 index 0000000000000000000000000000000000000000..a0e523769778124b66239fc1f7d9c6018c7a8b05 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java @@ -0,0 +1,158 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.zooming; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.filters.Filter; + +/** + * This class encapsulates all the zoom(and filter) parameters into one object + * for passing around and as a memento of the zoom/filter state. + */ +public class ZoomParams { + + private final Interval timeRange; + + private final EventTypeZoomLevel typeZoomLevel; + + private final Filter filter; + + private final DescriptionLOD descrLOD; + + private final Set<Field> changedFields; + + public Set<Field> getChangedFields() { + return Collections.unmodifiableSet(changedFields); + } + + public enum Field { + + TIME, EVENT_TYPE_ZOOM, FILTER, DESCRIPTION_LOD; + } + + public Interval getTimeRange() { + return timeRange; + } + + public EventTypeZoomLevel getTypeZoomLevel() { + return typeZoomLevel; + } + + public Filter getFilter() { + return filter; + } + + public DescriptionLOD getDescrLOD() { + return descrLOD; + } + + public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, Filter filter, DescriptionLOD descrLOD) { + this.timeRange = timeRange; + this.typeZoomLevel = zoomLevel; + this.filter = filter; + this.descrLOD = descrLOD; + changedFields = EnumSet.allOf(Field.class); + } + + public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, Filter filter, DescriptionLOD descrLOD, EnumSet<Field> changed) { + this.timeRange = timeRange; + this.typeZoomLevel = zoomLevel; + this.filter = filter; + this.descrLOD = descrLOD; + changedFields = changed; + } + + public ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel) { + return new ZoomParams(timeRange, zoomLevel, filter, descrLOD, EnumSet.of(Field.TIME, Field.EVENT_TYPE_ZOOM)); + } + + public ZoomParams withTypeZoomLevel(EventTypeZoomLevel zoomLevel) { + return new ZoomParams(timeRange, zoomLevel, filter, descrLOD, EnumSet.of(Field.EVENT_TYPE_ZOOM)); + } + + public ZoomParams withTimeRange(Interval timeRange) { + return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.TIME)); + } + + public ZoomParams withDescrLOD(DescriptionLOD descrLOD) { + return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.DESCRIPTION_LOD)); + } + + public ZoomParams withFilter(Filter filter) { + return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD, EnumSet.of(Field.FILTER)); + } + + public boolean hasFilter(Filter filterSet) { + return this.filter.equals(filterSet); + } + + public boolean hasTypeZoomLevel(EventTypeZoomLevel typeZoom) { + return this.typeZoomLevel.equals(typeZoom); + } + + public boolean hasTimeRange(Interval timeRange) { + return this.timeRange == null ? false : this.timeRange.equals(timeRange); + } + + public boolean hasDescrLOD(DescriptionLOD newLOD) { + return this.descrLOD.equals(newLOD); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 97 * hash + Objects.hashCode(this.timeRange.getStartMillis()); + hash = 97 * hash + Objects.hashCode(this.timeRange.getEndMillis()); + hash = 97 * hash + Objects.hashCode(this.typeZoomLevel); + hash = 97 * hash + Objects.hashCode(this.filter.isActive()); + hash = 97 * hash + Objects.hashCode(this.descrLOD); + + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ZoomParams other = (ZoomParams) obj; + if (!Objects.equals(this.timeRange, other.timeRange)) { + return false; + } + if (this.typeZoomLevel != other.typeZoomLevel) { + return false; + } + if (this.filter.equals(other.filter) == false) { + return false; + } + if (this.descrLOD != other.descrLOD) { + return false; + } + + return true; + } + +} diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..894e35fd4a3590e7c1575a8fbf1c362884d8f610 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.fxml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<fx:root alignment="TOP_LEFT" collapsible="false" contentDisplay="RIGHT" minHeight="-Infinity" minWidth="-Infinity" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <content> + <AnchorPane minHeight="-Infinity" minWidth="-Infinity"> + <children> + <GridPane alignment="CENTER" hgap="5.0" minHeight="-Infinity" minWidth="-Infinity" vgap="5.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="0.0"> + <children> + <Label text="Description Detail:" GridPane.rowIndex="2"> + <GridPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </GridPane.margin> + </Label> + <Slider fx:id="descrLODSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1" GridPane.rowIndex="2"> + <GridPane.margin> + <Insets /> + </GridPane.margin> + </Slider> + <Label text="Event Type: " GridPane.rowIndex="1"> + <GridPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </GridPane.margin> + </Label> + <Slider fx:id="typeZoomSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1" GridPane.rowIndex="1"> + <GridPane.margin> + <Insets /> + </GridPane.margin> + </Slider> + <Label text="Time Units:" GridPane.valignment="CENTER"> + <GridPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </GridPane.margin> + </Label> + <Slider fx:id="timeUnitSlider" blockIncrement="1.0" majorTickUnit="1.0" max="10.0" minorTickCount="0" showTickLabels="true" showTickMarks="true" snapToTicks="true" GridPane.columnIndex="1"> + <GridPane.margin> + <Insets /> + </GridPane.margin> + </Slider> + </children> + <columnConstraints> + <ColumnConstraints fillWidth="false" halignment="LEFT" hgrow="NEVER" minWidth="-Infinity" /> + <ColumnConstraints halignment="CENTER" hgrow="SOMETIMES" minWidth="-Infinity" /> + </columnConstraints> + <rowConstraints> + <RowConstraints minHeight="-Infinity" vgrow="NEVER" /> + <RowConstraints minHeight="-Infinity" vgrow="NEVER" /> + <RowConstraints minHeight="-Infinity" vgrow="NEVER" /> + <RowConstraints /> + </rowConstraints> + </GridPane> + </children> + <padding> + <Insets right="5.0" /> + </padding> + </AnchorPane> + </content> + <graphic> + <BorderPane> + <left> + <HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" spacing="5.0" BorderPane.alignment="CENTER_LEFT"> + <children> + <Label text="Zoom "> + <font> + <Font name="System Bold" size="12.0" /> + </font> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" preserveRatio="true"> + <image> + <Image url="@../images/magnifier-zoom.png" /> + </image> + </ImageView> + </graphic> + </Label> + <Region maxWidth="1.7976931348623157E308" minWidth="60.0" HBox.hgrow="ALWAYS" /> + <Label text="History: " /> + <Button fx:id="backButton" mnemonicParsing="false" HBox.hgrow="SOMETIMES"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow-180.png" /> + </image> + </ImageView> + </graphic> + </Button> + <Button fx:id="forwardButton" mnemonicParsing="false" HBox.hgrow="SOMETIMES"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow.png" /> + </image> + </ImageView> + </graphic> + </Button> + </children> + <BorderPane.margin> + <Insets top="3.0" /> + </BorderPane.margin> + <padding> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </padding> + </HBox> + </left> + </BorderPane> + </graphic> +</fx:root> diff --git a/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java new file mode 100644 index 0000000000000000000000000000000000000000..f07e113461ce78e50d4544d251d98a3c08a1cd63 --- /dev/null +++ b/Timeline/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java @@ -0,0 +1,227 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.zooming; + +import java.net.URL; +import java.time.temporal.ChronoUnit; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Slider; +import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; +import javafx.util.StringConverter; +import org.sleuthkit.autopsy.timeline.FXMLConstructor; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.VisualizationMode; +import org.sleuthkit.autopsy.timeline.actions.Back; +import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.events.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; +import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; + +/** FXML Controller class for the ZoomSettingsPane.fxml + * + * has sliders to provide context/control over three axes of zooming (timescale, + * event hierarchy, and description detail). */ +public class ZoomSettingsPane extends TitledPane implements TimeLineView { + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private Button backButton; + + @FXML + private Button forwardButton; + + @FXML + private Slider descrLODSlider; + + @FXML + private Slider typeZoomSlider; + + @FXML + private Slider timeUnitSlider; + + private TimeLineController controller; + + private FilteredEventsModel filteredEvents; + + /** + * Initializes the controller class. + */ + public void initialize() { + + timeUnitSlider.setMax(TimeUnits.values().length - 2); + timeUnitSlider.setLabelFormatter(new TimeUnitConverter()); + + typeZoomSlider.setMin(1); + typeZoomSlider.setMax(2); + typeZoomSlider.setLabelFormatter(new TypeZoomConverter()); + descrLODSlider.setMax(DescriptionLOD.values().length - 1); + descrLODSlider.setLabelFormatter(new DescrLODConverter()); + } + + public ZoomSettingsPane() { + FXMLConstructor.construct(this, "ZoomSettingsPane.fxml"); + } + + @Override + synchronized public void setController(TimeLineController controller) { + this.controller = controller; + setModel(controller.getEventsModel()); + descrLODSlider.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + Back back = new Back(controller); + backButton.disableProperty().bind(back.disabledProperty()); + backButton.setOnAction(back); + backButton.setTooltip(new Tooltip("Back: " + back.getAccelerator().getName())); + Forward forward = new Forward(controller); + forwardButton.disableProperty().bind(forward.disabledProperty()); + forwardButton.setOnAction(forward); + forwardButton.setTooltip(new Tooltip("Forward: " + forward.getAccelerator().getName())); + + } + + @Override + public void setModel(FilteredEventsModel filteredEvents) { + this.filteredEvents = filteredEvents; + + initializeSlider(timeUnitSlider, + () -> { + TimeUnits requestedUnit = TimeUnits.values()[new Double(timeUnitSlider.getValue()).intValue()]; + if (requestedUnit == TimeUnits.FOREVER) { + controller.showFullRange(); + } else { + controller.pushTimeRange(IntervalUtils.getIntervalAround(IntervalUtils.middleOf(ZoomSettingsPane.this.filteredEvents.timeRange().get()), requestedUnit.getPeriod())); + } + }, + this.filteredEvents.timeRange(), + () -> { + RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(this.filteredEvents.timeRange().get()); + ChronoUnit chronoUnit = rangeInfo.getPeriodSize().getChronoUnit(); + + timeUnitSlider.setValue(TimeUnits.fromChronoUnit(chronoUnit).ordinal() - 1); + }); + + initializeSlider(descrLODSlider, + () -> { + DescriptionLOD newLOD = DescriptionLOD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]; + controller.pushDescrLOD(newLOD); + }, this.filteredEvents.descriptionLOD(), + () -> { + descrLODSlider.setValue(this.filteredEvents.descriptionLOD().get().ordinal()); + }); + + initializeSlider(typeZoomSlider, + () -> { + EventTypeZoomLevel newZoomLevel = EventTypeZoomLevel.values()[Math.round(typeZoomSlider.valueProperty().floatValue())]; + controller.pushEventTypeZoom(newZoomLevel); + }, + this.filteredEvents.eventTypeZoom(), + () -> { + typeZoomSlider.setValue(this.filteredEvents.eventTypeZoom().get().ordinal()); + }); + } + + /** + * setup a slider that with a listener that is added and removed to avoid + * circular updates. + * + * @param <T> the type of the driving property + * @param slider the slider that will have its change handlers + * setup + * @param sliderChangeHandler the runnable that will be executed whenever + * the slider value has changed and is not + * currently changing + * @param driver the property that drives updates to this + * slider + * @param driverChangHandler the code to update the slider bases on the + * value of the driving property. This will be wrapped in a + * remove/add-listener pair to prevent circular updates. + */ + private <T> void initializeSlider(Slider slider, Runnable sliderChangeHandler, ReadOnlyObjectProperty<T> driver, Runnable driverChangHandler) { + final InvalidationListener sliderListener = observable -> { + if (slider.isValueChanging() == false) { + sliderChangeHandler.run(); + } + }; + slider.valueProperty().addListener(sliderListener); + slider.valueChangingProperty().addListener(sliderListener); + + Platform.runLater(driverChangHandler); + + driver.addListener(observable -> { + slider.valueProperty().removeListener(sliderListener); + slider.valueChangingProperty().removeListener(sliderListener); + + driverChangHandler.run(); + + slider.valueProperty().addListener(sliderListener); + slider.valueChangingProperty().addListener(sliderListener); + }); + } + + //Can these be abstracted to a sort of Enum converter for use in a potential enumslider + private static class TimeUnitConverter extends StringConverter<Double> { + + @Override + public String toString(Double object) { + return TimeUnits.values()[Math.min(TimeUnits.values().length - 1, object.intValue() + 1)].toString(); + } + + @Override + public Double fromString(String string) { + return new Integer(TimeUnits.valueOf(string).ordinal()).doubleValue(); + } + } + + private static class TypeZoomConverter extends StringConverter<Double> { + + @Override + public String toString(Double object) { + return EventTypeZoomLevel.values()[object.intValue()].getDisplayName(); + } + + @Override + public Double fromString(String string) { + return new Integer(EventTypeZoomLevel.valueOf(string).ordinal()).doubleValue(); + } + } + + private static class DescrLODConverter extends StringConverter<Double> { + + @Override + public String toString(Double object) { + return DescriptionLOD.values()[object.intValue()].getDisplayName(); + } + + @Override + public Double fromString(String string) { + return new Integer(DescriptionLOD.valueOf(string).ordinal()).doubleValue(); + } + } +} diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index 92f657c0b37ed56045a468ec3da39fc9bad4166f..df82a8680af661b7869ad26904949b5bd2cc359c 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Tue, 05 Aug 2014 14:10:24 -0400 +#Mon, 28 Apr 2014 01:45:18 -0400 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=288 SPLASH_WIDTH=538 @@ -8,4 +8,4 @@ SplashRunningTextBounds=5,266,530,17 SplashRunningTextColor=0x0 SplashRunningTextFontSize=18 -currentVersion=Autopsy 3.1.0_Beta +currentVersion=Autopsy 3.1.0 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index c9b7f9be63e51c9b1b23b7d62be081c323302f70..91c0d156ef0f725db9d322ab14350824da90fdb8 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script #Tue, 05 Aug 2014 14:10:24 -0400 -CTL_MainWindow_Title=Autopsy 3.1.0_Beta -CTL_MainWindow_Title_No_Project=Autopsy 3.1.0_Beta +CTL_MainWindow_Title=Autopsy 3.1.0 +CTL_MainWindow_Title_No_Project=Autopsy 3.1.0 diff --git a/nbproject/project.properties b/nbproject/project.properties index 55eff9f604bf9f88edff15c28f2699b181efdd95..b25757db069cf992e91231f783b23de9a649cc42 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -10,6 +10,7 @@ app.version=3.1.0 #build.type=RELEASE build.type=DEVELOPMENT project.org.sleuthkit.autopsy.imageanalyzer=ImageAnalyzer +project.org.sleuthkit.autopsy.advancedtimeline=Timeline update_versions=false #custom JVM options #Note: can be higher on 64 bit systems, should be in sync with build.xml @@ -29,7 +30,8 @@ modules=\ ${project.org.sleuthkit.autopsy.core}:\ ${project.org.sleuthkit.autopsy.corelibs}:\ ${project.org.sleuthkit.autopsy.scalpel}:\ - ${project.org.sleuthkit.autopsy.imageanalyzer} + ${project.org.sleuthkit.autopsy.imageanalyzer}:\ + ${project.org.sleuthkit.autopsy.advancedtimeline} project.org.sleuthkit.autopsy.core=Core project.org.sleuthkit.autopsy.corelibs=CoreLibs project.org.sleuthkit.autopsy.keywordsearch=KeywordSearch