diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java index 2111f3a554..28ce44b3c7 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2013-2015 Basis Technology Corp. + * + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.actions; import java.util.Collection; +import java.util.HashSet; import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; @@ -26,8 +27,8 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** @@ -38,7 +39,6 @@ public class AddBlackboardArtifactTagAction extends AddTagAction { // This class is a singleton to support multi-selection of nodes, since // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every // node in the array returns a reference to the same action object from Node.getActions(boolean). - private static AddBlackboardArtifactTagAction instance; public static synchronized AddBlackboardArtifactTagAction getInstance() { @@ -63,7 +63,14 @@ public class AddBlackboardArtifactTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - final Collection selectedArtifacts = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class); + /* + * The documentation for Lookup.lookupAll() explicitly says that the + * collection it returns may contain duplicates. Within this invocation + * of addTag(), we don't want to tag the same BlackboardArtifact more + * than once, so we dedupe the BlackboardArtifacts by stuffing them into + * a HashSet. + */ + final Collection selectedArtifacts = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class)); new Thread(() -> { for (BlackboardArtifact artifact : selectedArtifacts) { diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java index d504600446..e32f9df902 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2013-2015 Basis Technology Corp. + * + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,13 +19,13 @@ package org.sleuthkit.autopsy.actions; import java.util.Collection; +import java.util.HashSet; import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; @@ -40,7 +40,6 @@ public class AddContentTagAction extends AddTagAction { // This class is a singleton to support multi-selection of nodes, since // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every // node in the array returns a reference to the same action object from Node.getActions(boolean). - private static AddContentTagAction instance; public static synchronized AddContentTagAction getInstance() { @@ -63,7 +62,13 @@ public class AddContentTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - final Collection selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class); + /* + * The documentation for Lookup.lookupAll() explicitly says that the + * collection it returns may contain duplicates. Within this invocation + * of addTag(), we don't want to tag the same AbstractFile more than + * once, so we dedupe the AbstractFiles by stuffing them into a HashSet. + */ + final Collection selectedFiles = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); new Thread(() -> { for (AbstractFile file : selectedFiles) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties index d23cfbb773..5b74bea711 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/Bundle.properties @@ -9,7 +9,7 @@ Timeline.zoomOutButton.text=Zoom Out Timeline.goToButton.text=Go To\: Timeline.yearBarChart.x.years=Years Timeline.resultPanel.loading=Loading... -Timeline.node.root=Root + TimelineFrame.title=Timeline TimelinePanel.jButton1.text=6m TimelinePanel.jButton13.text=all @@ -24,4 +24,4 @@ TimelinePanel.jButton7.text=3d TimelinePanel.jButton2.text=1m TimelinePanel.jButton3.text=3m TimelinePanel.jButton4.text=2w -ProgressWindow.progressHeader.text=\ \ No newline at end of file +ProgressWindow.progressHeader.text=\ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties index ba1ee4f94a..17f945778a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/Bundle_ja.properties @@ -1,48 +1,48 @@ -CTL_MakeTimeline=\u300C\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u300D -CTL_TimeLineTopComponent=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30A6\u30A3\u30F3\u30C9\u30A6 -CTL_TimeLineTopComponentAction=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C8\u30C3\u30D7\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8 -HINT_TimeLineTopComponent=\u3053\u308C\u306F\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u3059 -OpenTimelineAction.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -Timeline.frameName.text={0} - Autopsy\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -Timeline.goToButton.text=\u4E0B\u8A18\u3078\u79FB\u52D5\uFF1A -Timeline.node.root=\u30EB\u30FC\u30C8 -Timeline.pushDescrLOD.confdlg.msg={0}\u30A4\u30D9\u30F3\u30C8\u306E\u8A73\u7D30\u304C\u8868\u793A\u53EF\u80FD\u3067\u3059\u3002\u3053\u306E\u51E6\u7406\u306F\u9577\u6642\u9593\u304B\u304B\u308B\u3082\u3057\u304F\u306FAutopsy\u3092\u30AF\u30E9\u30C3\u30B7\u30E5\u3059\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\n\n\u5B9F\u884C\u3057\u307E\u3059\u304B\uFF1F -Timeline.resultPanel.loading=\u30ED\u30FC\u30C9\u4E2D\u30FB\u30FB\u30FB -Timeline.resultsPanel.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u7D50\u679C -Timeline.runJavaFxThread.progress.creating=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u4F5C\u6210\u4E2D\u30FB\u30FB\u30FB -Timeline.zoomOutButton.text=\u30BA\u30FC\u30E0\u30A2\u30A6\u30C8 -TimelineFrame.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3 -TimeLineTopComponent.eventsTab.name=\u30A4\u30D9\u30F3\u30C8 -TimeLineTopComponent.filterTab.name=\u30D5\u30A3\u30EB\u30BF\u30FC -OpenTimeLineAction.msgdlg.text=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u4F5C\u6210\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u304C\u3042\u308A\u307E\u305B\u3093\u3002 -PrompDialogManager.buttonType.continueNoUpdate=\u66F4\u65B0\u305B\u305A\u6B21\u3078 -PrompDialogManager.buttonType.showTimeline=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u8868\u793A -PrompDialogManager.buttonType.update=\u66F4\u65B0 -PromptDialogManager.confirmDuringIngest.contentText=\u6B21\u3078\u9032\u307F\u307E\u3059\u304B\uFF1F -PromptDialogManager.confirmDuringIngest.headerText=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u304C\u5B8C\u4E86\u3059\u308B\u524D\u306B\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u3092\u8868\u793A\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307E\u3059\u3002\n\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u5B8C\u6210\u3057\u3066\u3044\u306A\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002 -PromptDialogManager.progressDialog.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C7\u30FC\u30BF\u3092\u5165\u529B\u4E2D -PromptDialogManager.rebuildPrompt.details=\u8A73\u7D30\uFF1A -PromptDialogManager.rebuildPrompt.headerText=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u4E0D\u5B8C\u5168\u307E\u305F\u306F\u6700\u65B0\u3067\u306F\u306A\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\n \u6B20\u843D\u3057\u3066\u3044\u308B\u307E\u305F\u306F\u4E0D\u6B63\u78BA\u306A\u30A4\u30D9\u30F3\u30C8\u304C\u4E00\u90E8\u3042\u308B\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002\u4E00\u90E8\u306E\u6A5F\u80FD\u304C\u5229\u7528\u3067\u304D\u306A\u3044\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002 -Timeline.confirmation.dialogs.title=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u66F4\u65B0\u3057\u307E\u3059\u304B\uFF1F -Timeline.pushDescrLOD.confdlg.title=\u8AAC\u660E\u306E\u8A18\u8FF0\u30EC\u30D9\u30EB\u3092\u5909\u66F4\u3057\u307E\u3059\u304B\uFF1F -TimeLineController.errorTitle=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30A8\u30E9\u30FC -TimeLineController.outOfDate.errorMessage=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u304C\u6700\u65B0\u304B\u78BA\u8A8D\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u66F4\u65B0\u304C\u5FC5\u8981\u3060\u3068\u60F3\u5B9A\u3057\u307E\u3059\u3002 -TimeLineController.rebuildReasons.incompleteOldSchema=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30A4\u30D9\u30F3\u30C8\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306B\u4E0D\u5B8C\u5168\u306A\u60C5\u5831\u304C\u4EE5\u524D\u5165\u529B\u3055\u308C\u3066\u3044\u307E\u3057\u305F\uFF1A\u30A4\u30D9\u30F3\u30C8\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u66F4\u65B0\u3057\u306A\u3044\u3068\u3001\u4E00\u90E8\u306E\u6A5F\u80FD\u304C\u5229\u7528\u3067\u304D\u306A\u3044\u3001\u307E\u305F\u306F\u6A5F\u80FD\u3057\u306A\u3044\u304B\u3082\u3057\u308C\u306A\u3044\u3067\u3059\u3002 -TimeLineController.rebuildReasons.ingestWasRunning=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u304C\u5B9F\u884C\u4E2D\u306B\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30A4\u30D9\u30F3\u30C8\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306B\u60C5\u5831\u304C\u5165\u529B\u3055\u308C\u3066\u3044\u307E\u3057\u305F\uFF1A\u30A4\u30D9\u30F3\u30C8\u304C\u6B20\u3051\u3066\u3044\u308B\u3001\u4E0D\u5B8C\u5168\u3001\u307E\u305F\u306F\u4E0D\u6B63\u78BA\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002 -TimeLineController.rebuildReasons.outOfDate=\u30A4\u30D9\u30F3\u30C8\u30C7\u30FC\u30BF\u304C\u6700\u65B0\u3067\u306F\u3042\u308A\u307E\u305B\u3093\uFF1A\u898B\u308C\u306A\u3044\u30A4\u30D9\u30F3\u30C8\u304C\u3042\u308A\u307E\u3059\u3002 -TimeLineController.rebuildReasons.outOfDateError=\u30BF\u30A4\u30E0\u30E9\u30A4\u30F3\u30C7\u30FC\u30BF\u304C\u6700\u65B0\u304B\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002 -TimeLinecontroller.updateNowQuestion=\u30A4\u30D9\u30F3\u30C8\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u4ECA\u66F4\u65B0\u3057\u307E\u3059\u304B\uFF1F +CTL_MakeTimeline=\u300c\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u300d +CTL_TimeLineTopComponent=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30a6\u30a3\u30f3\u30c9\u30a6 +CTL_TimeLineTopComponentAction=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30c8\u30c3\u30d7\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8 +HINT_TimeLineTopComponent=\u3053\u308c\u306f\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30a6\u30a3\u30f3\u30c9\u30a6\u3067\u3059 +OpenTimelineAction.title=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3 +Timeline.frameName.text={0} - Autopsy\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3 +Timeline.goToButton.text=\u4e0b\u8a18\u3078\u79fb\u52d5\uff1a +Timeline.pushDescrLOD.confdlg.msg={0}\u30a4\u30d9\u30f3\u30c8\u306e\u8a73\u7d30\u304c\u8868\u793a\u53ef\u80fd\u3067\u3059\u3002\u3053\u306e\u51e6\u7406\u306f\u9577\u6642\u9593\u304b\u304b\u308b\u3082\u3057\u304f\u306fAutopsy\u3092\u30af\u30e9\u30c3\u30b7\u30e5\u3059\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u5b9f\u884c\u3057\u307e\u3059\u304b\uff1f +Timeline.resultPanel.loading=\u30ed\u30fc\u30c9\u4e2d\u30fb\u30fb\u30fb +Timeline.resultsPanel.title=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u7d50\u679c +Timeline.runJavaFxThread.progress.creating=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u4f5c\u6210\u4e2d\u30fb\u30fb\u30fb +Timeline.zoomOutButton.text=\u30ba\u30fc\u30e0\u30a2\u30a6\u30c8 +TimelineFrame.title=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3 +TimeLineTopComponent.eventsTab.name=\u30a4\u30d9\u30f3\u30c8 +TimeLineTopComponent.filterTab.name=\u30d5\u30a3\u30eb\u30bf\u30fc +OpenTimeLineAction.msgdlg.text=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u304c\u3042\u308a\u307e\u305b\u3093\u3002 +PrompDialogManager.buttonType.continueNoUpdate=\u66f4\u65b0\u305b\u305a\u6b21\u3078 +PrompDialogManager.buttonType.showTimeline=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u3092\u8868\u793a +PrompDialogManager.buttonType.update=\u66f4\u65b0 +PromptDialogManager.confirmDuringIngest.contentText=\u6b21\u3078\u9032\u307f\u307e\u3059\u304b\uff1f +PromptDialogManager.confirmDuringIngest.headerText=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u304c\u5b8c\u4e86\u3059\u308b\u524d\u306b\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u3092\u8868\u793a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002\n\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u304c\u5b8c\u6210\u3057\u3066\u3044\u306a\u3044\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 +PromptDialogManager.progressDialog.title=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30c7\u30fc\u30bf\u3092\u5165\u529b\u4e2d +PromptDialogManager.rebuildPrompt.details=\u8a73\u7d30\uff1a +PromptDialogManager.rebuildPrompt.headerText=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u304c\u4e0d\u5b8c\u5168\u307e\u305f\u306f\u6700\u65b0\u3067\u306f\u306a\u3044\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\n \u6b20\u843d\u3057\u3066\u3044\u308b\u307e\u305f\u306f\u4e0d\u6b63\u78ba\u306a\u30a4\u30d9\u30f3\u30c8\u304c\u4e00\u90e8\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\u4e00\u90e8\u306e\u6a5f\u80fd\u304c\u5229\u7528\u3067\u304d\u306a\u3044\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 +Timeline.confirmation.dialogs.title=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u306e\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u66f4\u65b0\u3057\u307e\u3059\u304b\uff1f +Timeline.pushDescrLOD.confdlg.title=\u8aac\u660e\u306e\u8a18\u8ff0\u30ec\u30d9\u30eb\u3092\u5909\u66f4\u3057\u307e\u3059\u304b\uff1f +TimeLineController.errorTitle=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30a8\u30e9\u30fc +TimeLineController.outOfDate.errorMessage=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u304c\u6700\u65b0\u304b\u78ba\u8a8d\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u66f4\u65b0\u304c\u5fc5\u8981\u3060\u3068\u60f3\u5b9a\u3057\u307e\u3059\u3002 +TimeLineController.rebuildReasons.incompleteOldSchema=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30a4\u30d9\u30f3\u30c8\u306e\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u4e0d\u5b8c\u5168\u306a\u60c5\u5831\u304c\u4ee5\u524d\u5165\u529b\u3055\u308c\u3066\u3044\u307e\u3057\u305f\uff1a\u30a4\u30d9\u30f3\u30c8\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u66f4\u65b0\u3057\u306a\u3044\u3068\u3001\u4e00\u90e8\u306e\u6a5f\u80fd\u304c\u5229\u7528\u3067\u304d\u306a\u3044\u3001\u307e\u305f\u306f\u6a5f\u80fd\u3057\u306a\u3044\u304b\u3082\u3057\u308c\u306a\u3044\u3067\u3059\u3002 +TimeLineController.rebuildReasons.ingestWasRunning=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u304c\u5b9f\u884c\u4e2d\u306b\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30a4\u30d9\u30f3\u30c8\u306e\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u60c5\u5831\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u307e\u3057\u305f\uff1a\u30a4\u30d9\u30f3\u30c8\u304c\u6b20\u3051\u3066\u3044\u308b\u3001\u4e0d\u5b8c\u5168\u3001\u307e\u305f\u306f\u4e0d\u6b63\u78ba\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 +TimeLineController.rebuildReasons.outOfDate=\u30a4\u30d9\u30f3\u30c8\u30c7\u30fc\u30bf\u304c\u6700\u65b0\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff1a\u898b\u308c\u306a\u3044\u30a4\u30d9\u30f3\u30c8\u304c\u3042\u308a\u307e\u3059\u3002 +TimeLineController.rebuildReasons.outOfDateError=\u30bf\u30a4\u30e0\u30e9\u30a4\u30f3\u30c7\u30fc\u30bf\u304c\u6700\u65b0\u304b\u78ba\u8a8d\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 +TimeLinecontroller.updateNowQuestion=\u30a4\u30d9\u30f3\u30c8\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u4eca\u66f4\u65b0\u3057\u307e\u3059\u304b\uff1f TimelinePanel.jButton13.text=\u5168\u3066 -Timeline.yearBarChart.x.years=\u5E74 -TimelinePanel.jButton1.text=6\u30F6\u6708 +Timeline.yearBarChart.x.years=\u5e74 +TimelinePanel.jButton1.text=6\u30f6\u6708 TimelinePanel.jButton10.text=1\u6642\u9593 TimelinePanel.jButton9.text=12\u6642\u9593 -TimelinePanel.jButton11.text=5\u5E74 -TimelinePanel.jButton12.text=10\u5E74 +TimelinePanel.jButton11.text=5\u5e74 +TimelinePanel.jButton12.text=10\u5e74 TimelinePanel.jButton6.text=1\u9031\u9593 -TimelinePanel.jButton5.text=1\u5E74 -TimelinePanel.jButton8.text=1\u65E5 -TimelinePanel.jButton7.text=3\u65E5 -TimelinePanel.jButton2.text=1\u30F6\u6708 -TimelinePanel.jButton3.text=3\u30F6\u6708 -TimelinePanel.jButton4.text=2\u9031\u9593 \ No newline at end of file +TimelinePanel.jButton5.text=1\u5e74 +TimelinePanel.jButton8.text=1\u65e5 +TimelinePanel.jButton7.text=3\u65e5 +TimelinePanel.jButton2.text=1\u30f6\u6708 +TimelinePanel.jButton3.text=3\u30f6\u6708 +TimelinePanel.jButton4.text=2\u9031\u9593 +TimeLineResultView.startDateToEndDate.text={0}\u304b\u3089{1} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index bbc213de10..ed3f3b307a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.TimeZone; import java.util.concurrent.ExecutionException; @@ -188,7 +189,7 @@ public class TimeLineController { } @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - private TimeLineTopComponent mainFrame; + private TimeLineTopComponent topComponent; //are the listeners currently attached @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @@ -199,11 +200,7 @@ public class TimeLineController { private final PropertyChangeListener ingestModuleListener = new AutopsyIngestModuleListener(); @GuardedBy("this") - private final ReadOnlyObjectWrapper visualizationMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); - - synchronized public ReadOnlyObjectProperty visualizationModeProperty() { - return visualizationMode.getReadOnlyProperty(); - } + private final ReadOnlyObjectWrapper viewMode = new ReadOnlyObjectWrapper<>(ViewMode.COUNTS); @GuardedBy("filteredEvents") private final FilteredEventsModel filteredEvents; @@ -223,21 +220,38 @@ public class TimeLineController { @GuardedBy("this") private final ObservableList selectedEventIDs = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + @GuardedBy("this") + private final ReadOnlyObjectWrapper selectedTimeRange = new ReadOnlyObjectWrapper<>(); + + private final ReadOnlyBooleanWrapper eventsDBStale = new ReadOnlyBooleanWrapper(true); + + private final PromptDialogManager promptDialogManager = new PromptDialogManager(this); + /** - * @return A list of the selected event ids + * Get an ObservableList of selected event IDs + * + * @return A list of the selected event IDs */ synchronized public ObservableList getSelectedEventIDs() { return selectedEventIDs; } - @GuardedBy("this") - private final ReadOnlyObjectWrapper selectedTimeRange = new ReadOnlyObjectWrapper<>(); + /** + * Get a read only observable view of the selected time range. + * + * @return A read only view of the selected time range. + */ + synchronized public ReadOnlyObjectProperty selectedTimeRangeProperty() { + return selectedTimeRange.getReadOnlyProperty(); + } /** - * @return a read only view of the selected interval. + * Get the selected time range. + * + * @return The selected time range. */ - synchronized public ReadOnlyObjectProperty getSelectedTimeRange() { - return selectedTimeRange.getReadOnlyProperty(); + synchronized public Interval getSelectedTimeRange() { + return selectedTimeRange.get(); } public ReadOnlyBooleanProperty eventsDBStaleProperty() { @@ -282,9 +296,30 @@ public class TimeLineController { synchronized public ReadOnlyBooleanProperty canRetreatProperty() { return historyManager.getCanRetreat(); } - private final ReadOnlyBooleanWrapper eventsDBStale = new ReadOnlyBooleanWrapper(true); - private final PromptDialogManager promptDialogManager = new PromptDialogManager(this); + synchronized public ReadOnlyObjectProperty viewModeProperty() { + return viewMode.getReadOnlyProperty(); + } + + /** + * Set a new ViewMode as the active one. + * + * @param viewMode The new ViewMode to set. + */ + synchronized public void setViewMode(ViewMode viewMode) { + if (this.viewMode.get() != viewMode) { + this.viewMode.set(viewMode); + } + } + + /** + * Get the currently active ViewMode. + * + * @return The currently active ViewMode. + */ + synchronized public ViewMode getViewMode() { + return viewMode.get(); + } public TimeLineController(Case autoCase) throws IOException { this.autoCase = autoCase; @@ -310,6 +345,9 @@ public class TimeLineController { filteredEvents.filterProperty().get(), DescriptionLoD.SHORT); historyManager.advance(InitialZoomState); + + //clear the selected events when the view mode changes + viewMode.addListener(observable -> selectEventIDs(Collections.emptySet())); } /** @@ -449,9 +487,9 @@ public class TimeLineController { IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleListener); IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); Case.removePropertyChangeListener(caseListener); - if (mainFrame != null) { - mainFrame.close(); - mainFrame = null; + if (topComponent != null) { + topComponent.close(); + topComponent = null; } OpenTimelineAction.invalidateController(); } @@ -582,17 +620,6 @@ public class TimeLineController { pushTimeRange(new Interval(start, end)); } - /** - * Set a new Visualization mode as the active one. - * - * @param visualizationMode The new VisaualizationMode to set. - */ - synchronized public void setVisualizationMode(VisualizationMode visualizationMode) { - if (this.visualizationMode.get() != visualizationMode) { - this.visualizationMode.set(visualizationMode); - } - } - public void selectEventIDs(Collection events) { final LoggedTask selectEventIDsTask = new LoggedTask("Select Event IDs", true) { //NON-NLS @Override @@ -624,16 +651,16 @@ public class TimeLineController { */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) synchronized private void showWindow() { - if (mainFrame == null) { - mainFrame = new TimeLineTopComponent(this); + if (topComponent == null) { + topComponent = new TimeLineTopComponent(this); } - mainFrame.open(); - mainFrame.toFront(); + topComponent.open(); + topComponent.toFront(); /* * Make this top component active so its ExplorerManager's lookup gets * proxied in Utilities.actionsGlobalContext() */ - mainFrame.requestActive(); + topComponent.requestActive(); } synchronized public void pushEventTypeZoom(EventTypeZoomLevel typeZoomeLevel) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form index 1f0f367115..a05e7505ec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.form @@ -54,7 +54,7 @@ - + @@ -65,7 +65,7 @@ - + @@ -82,33 +82,45 @@ - - - - - - + - + + + + + + + + + + + + - - - - - - + - + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index f91d2e8bba..1ca594fa14 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,12 +18,14 @@ */ package org.sleuthkit.autopsy.timeline; -import java.awt.BorderLayout; +import java.beans.PropertyVetoException; import java.util.Collections; import java.util.List; +import java.util.logging.Level; import javafx.application.Platform; +import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.embed.swing.JFXPanel; +import javafx.collections.ObservableList; import javafx.scene.Scene; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; @@ -34,8 +36,15 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javax.swing.SwingUtilities; +import org.controlsfx.control.Notifications; +import org.joda.time.Interval; +import org.joda.time.format.DateTimeFormatter; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.windows.Mode; import org.openide.windows.TopComponent; @@ -44,19 +53,22 @@ 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.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Forward; +import org.sleuthkit.autopsy.timeline.explorernodes.EventNode; +import org.sleuthkit.autopsy.timeline.explorernodes.EventRootNode; import org.sleuthkit.autopsy.timeline.ui.HistoryToolBar; 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.ViewFrame; import org.sleuthkit.autopsy.timeline.ui.detailview.tree.EventsTree; import org.sleuthkit.autopsy.timeline.ui.filtering.FilterSetPanel; import org.sleuthkit.autopsy.timeline.zooming.ZoomSettingsPane; +import org.sleuthkit.datamodel.TskCoreException; /** - * TopComponent for the timeline feature. + * TopComponent for the Timeline feature. */ @TopComponent.Description( preferredID = "TimeLineTopComponent", @@ -67,91 +79,209 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer private static final Logger LOGGER = Logger.getLogger(TimeLineTopComponent.class.getName()); - private final DataContentPanel dataContentPanel; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final DataContentPanel contentViewerPanel; - private final TimeLineResultView tlrv; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private DataResultPanel dataResultPanel; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private final ExplorerManager em = new ExplorerManager(); private final TimeLineController controller; + /** + * Listener that drives the result viewer or content viewer (depending on + * view mode) according to the controller's selected event IDs + */ + @NbBundle.Messages({"TimelineTopComponent.selectedEventListener.errorMsg=There was a problem getting the content for the selected event."}) + private final InvalidationListener selectedEventsListener = new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + ObservableList selectedEventIDs = controller.getSelectedEventIDs(); + + //depending on the active view mode, we either update the dataResultPanel, or update the contentViewerPanel directly. + switch (controller.getViewMode()) { + case LIST: + + //make an array of EventNodes for the selected events + EventNode[] childArray = new EventNode[selectedEventIDs.size()]; + try { + for (int i = 0; i < selectedEventIDs.size(); i++) { + childArray[i] = EventNode.createEventNode(selectedEventIDs.get(i), controller.getEventsModel()); + } + Children children = new Children.Array(); + children.add(childArray); + + SwingUtilities.invokeLater(() -> { + //set generic container node as root context + em.setRootContext(new AbstractNode(children)); + try { + //set selected nodes for actions + em.setSelectedNodes(childArray); + } catch (PropertyVetoException ex) { + //I don't know why this would ever happen. + LOGGER.log(Level.SEVERE, "Selecting the event node was vetoed.", ex); // NON-NLS + } + //if there is only one event selected push it into content viewer. + if (selectedEventIDs.size() == 1) { + contentViewerPanel.setNode(childArray[0]); + } else { + contentViewerPanel.setNode(null); + } + }); + } catch (IllegalStateException ex) { + //Since the case is closed, the user probably doesn't care about this, just log it as a precaution. + LOGGER.log(Level.SEVERE, "There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + Platform.runLater(() -> { + Notifications.create() + .owner(jFXViewPanel.getScene().getWindow()) + .text(Bundle.TimelineTopComponent_selectedEventListener_errorMsg()) + .showError(); + }); + } + + break; + case COUNTS: + case DETAIL: + //make a root node with nodes for the selected events as children and push it to the result viewer. + EventRootNode rootNode = new EventRootNode(selectedEventIDs, controller.getEventsModel()); + SwingUtilities.invokeLater(() -> { + dataResultPanel.setPath(getResultViewerSummaryString()); + dataResultPanel.setNode(rootNode); + }); + break; + default: + throw new UnsupportedOperationException("Unknown view mode: " + controller.getViewMode()); + } + } + }; + + /** + * Constructor + * + * @param controller The TimeLineController for this topcomponent. + */ public TimeLineTopComponent(TimeLineController controller) { initComponents(); - this.controller = controller; associateLookup(ExplorerUtils.createLookup(em, getActionMap())); - setName(NbBundle.getMessage(TimeLineTopComponent.class, "CTL_TimeLineTopComponent")); setToolTipText(NbBundle.getMessage(TimeLineTopComponent.class, "HINT_TimeLineTopComponent")); setIcon(WindowManager.getDefault().getMainWindow().getIconImage()); //use the same icon as main application - dataContentPanel = DataContentPanel.createInstance(); - this.contentViewerContainerPanel.add(dataContentPanel, BorderLayout.CENTER); - tlrv = new TimeLineResultView(controller, dataContentPanel); - DataResultPanel dataResultPanel = tlrv.getDataResultPanel(); - this.resultContainerPanel.add(dataResultPanel, BorderLayout.CENTER); - dataResultPanel.open(); - customizeFXComponents(); - } + this.controller = controller; - @NbBundle.Messages({"TimeLineTopComponent.eventsTab.name=Events", - "TimeLineTopComponent.filterTab.name=Filters"}) - void customizeFXComponents() { - Platform.runLater(() -> { + //create linked result and content views + contentViewerPanel = DataContentPanel.createInstance(); + dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, contentViewerPanel); - //create and wire up jfx componenets that make up the interface - final Tab filterTab = new Tab(Bundle.TimeLineTopComponent_filterTab_name(), new FilterSetPanel(controller)); - filterTab.setClosable(false); - filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png")); // NON-NLS + //add them to bottom splitpane + horizontalSplitPane.setLeftComponent(dataResultPanel); + horizontalSplitPane.setRightComponent(contentViewerPanel); - final EventsTree eventsTree = new EventsTree(controller); - final VisualizationPanel visualizationPanel = new VisualizationPanel(controller, eventsTree); - final Tab eventsTreeTab = new Tab(Bundle.TimeLineTopComponent_eventsTab_name(), eventsTree); - eventsTreeTab.setClosable(false); - eventsTreeTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png")); // NON-NLS - eventsTreeTab.disableProperty().bind(controller.visualizationModeProperty().isEqualTo(VisualizationMode.COUNTS)); + dataResultPanel.open(); //get the explorermanager - final TabPane leftTabPane = new TabPane(filterTab, eventsTreeTab); - VBox.setVgrow(leftTabPane, Priority.ALWAYS); - controller.visualizationModeProperty().addListener((Observable observable) -> { - if (controller.visualizationModeProperty().get().equals(VisualizationMode.COUNTS)) { - //if view mode is counts, make sure events tabd is not active - leftTabPane.getSelectionModel().select(filterTab); - } - }); + Platform.runLater(this::initFXComponents); - HistoryToolBar historyToolBar = new HistoryToolBar(controller); - final TimeZonePanel timeZonePanel = new TimeZonePanel(); - VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); + //set up listeners + TimeLineController.getTimeZone().addListener(timeZone -> dataResultPanel.setPath(getResultViewerSummaryString())); + controller.getSelectedEventIDs().addListener(selectedEventsListener); - final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(controller); - - final VBox leftVBox = new VBox(5, timeZonePanel,historyToolBar, zoomSettingsPane, leftTabPane); - SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); - - final SplitPane mainSplitPane = new SplitPane(leftVBox, visualizationPanel); - mainSplitPane.setDividerPositions(0); - - final Scene scene = new Scene(mainSplitPane); - scene.addEventFilter(KeyEvent.KEY_PRESSED, - (KeyEvent event) -> { - if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(event)) { - new Back(controller).handle(null); - } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(event)) { - new Back(controller).handle(null); - } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(event)) { - new Forward(controller).handle(null); - } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(event)) { - new Forward(controller).handle(null); + //Listen to ViewMode and adjust GUI componenets as needed. + controller.viewModeProperty().addListener(viewMode -> { + switch (controller.getViewMode()) { + case COUNTS: + case DETAIL: + /* + * For counts and details mode, restore the result table at + * the bottom left. + */ + SwingUtilities.invokeLater(() -> { + splitYPane.remove(contentViewerPanel); + if ((horizontalSplitPane.getParent() == splitYPane) == false) { + splitYPane.setBottomComponent(horizontalSplitPane); + horizontalSplitPane.setRightComponent(contentViewerPanel); } }); - - //add ui componenets to JFXPanels - jFXVizPanel.setScene(scene); - jFXstatusPanel.setScene(new Scene(new StatusBar(controller))); - + break; + case LIST: + /* + * For list mode, remove the result table, and let the + * content viewer expand across the bottom. + */ + SwingUtilities.invokeLater(() -> { + splitYPane.setBottomComponent(contentViewerPanel); + }); + break; + default: + throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode()); + } }); } + /** + * Create and wire up JavaFX components of the interface + */ + @NbBundle.Messages({ + "TimeLineTopComponent.eventsTab.name=Events", + "TimeLineTopComponent.filterTab.name=Filters"}) + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + void initFXComponents() { + /////init componenets of left most column from top to bottom + final TimeZonePanel timeZonePanel = new TimeZonePanel(); + VBox.setVgrow(timeZonePanel, Priority.SOMETIMES); + HistoryToolBar historyToolBar = new HistoryToolBar(controller); + final ZoomSettingsPane zoomSettingsPane = new ZoomSettingsPane(controller); + + //set up filter tab + final Tab filterTab = new Tab(Bundle.TimeLineTopComponent_filterTab_name(), new FilterSetPanel(controller)); + filterTab.setClosable(false); + filterTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/funnel.png")); // NON-NLS + + //set up events tab + final EventsTree eventsTree = new EventsTree(controller); + final Tab eventsTreeTab = new Tab(Bundle.TimeLineTopComponent_eventsTab_name(), eventsTree); + eventsTreeTab.setClosable(false); + eventsTreeTab.setGraphic(new ImageView("org/sleuthkit/autopsy/timeline/images/timeline_marker.png")); // NON-NLS + eventsTreeTab.disableProperty().bind(controller.viewModeProperty().isNotEqualTo(ViewMode.DETAIL)); + + final TabPane leftTabPane = new TabPane(filterTab, eventsTreeTab); + VBox.setVgrow(leftTabPane, Priority.ALWAYS); + controller.viewModeProperty().addListener(viewMode -> { + if (controller.getViewMode().equals(ViewMode.DETAIL) == false) { + //if view mode is not details, switch back to the filter tab + leftTabPane.getSelectionModel().select(filterTab); + } + }); + + //assemble left column + final VBox leftVBox = new VBox(5, timeZonePanel, historyToolBar, zoomSettingsPane, leftTabPane); + SplitPane.setResizableWithParent(leftVBox, Boolean.FALSE); + + final ViewFrame viewFrame = new ViewFrame(controller, eventsTree); + final SplitPane mainSplitPane = new SplitPane(leftVBox, viewFrame); + mainSplitPane.setDividerPositions(0); + + final Scene scene = new Scene(mainSplitPane); + scene.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> { + if (new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN).match(keyEvent)) { + new Back(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE).match(keyEvent)) { + new Back(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN).match(keyEvent)) { + new Forward(controller).handle(null); + } else if (new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCodeCombination.SHIFT_DOWN).match(keyEvent)) { + new Forward(controller).handle(null); + } + }); + + //add ui componenets to JFXPanels + jFXViewPanel.setScene(scene); + jFXstatusPanel.setScene(new Scene(new StatusBar(controller))); + } + @Override public List availableModes(List modes) { return Collections.emptyList(); @@ -165,12 +295,12 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer // //GEN-BEGIN:initComponents private void initComponents() { - jFXstatusPanel = new JFXPanel(); + jFXstatusPanel = new javafx.embed.swing.JFXPanel(); splitYPane = new javax.swing.JSplitPane(); - jFXVizPanel = new JFXPanel(); - lowerSplitXPane = new javax.swing.JSplitPane(); - resultContainerPanel = new javax.swing.JPanel(); - contentViewerContainerPanel = new javax.swing.JPanel(); + jFXViewPanel = new javafx.embed.swing.JFXPanel(); + horizontalSplitPane = new javax.swing.JSplitPane(); + leftFillerPanel = new javax.swing.JPanel(); + rightfillerPanel = new javax.swing.JPanel(); jFXstatusPanel.setPreferredSize(new java.awt.Dimension(100, 16)); @@ -178,32 +308,47 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer splitYPane.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); splitYPane.setResizeWeight(0.9); splitYPane.setPreferredSize(new java.awt.Dimension(1024, 400)); - splitYPane.setLeftComponent(jFXVizPanel); + splitYPane.setLeftComponent(jFXViewPanel); - lowerSplitXPane.setDividerLocation(600); - lowerSplitXPane.setResizeWeight(0.5); - lowerSplitXPane.setPreferredSize(new java.awt.Dimension(1200, 300)); - lowerSplitXPane.setRequestFocusEnabled(false); + horizontalSplitPane.setDividerLocation(600); + horizontalSplitPane.setResizeWeight(0.5); + horizontalSplitPane.setPreferredSize(new java.awt.Dimension(1200, 300)); + horizontalSplitPane.setRequestFocusEnabled(false); - resultContainerPanel.setPreferredSize(new java.awt.Dimension(700, 300)); - resultContainerPanel.setLayout(new java.awt.BorderLayout()); - lowerSplitXPane.setLeftComponent(resultContainerPanel); + javax.swing.GroupLayout leftFillerPanelLayout = new javax.swing.GroupLayout(leftFillerPanel); + leftFillerPanel.setLayout(leftFillerPanelLayout); + leftFillerPanelLayout.setHorizontalGroup( + leftFillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 599, Short.MAX_VALUE) + ); + leftFillerPanelLayout.setVerticalGroup( + leftFillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 54, Short.MAX_VALUE) + ); - contentViewerContainerPanel.setPreferredSize(new java.awt.Dimension(500, 300)); - contentViewerContainerPanel.setLayout(new java.awt.BorderLayout()); - lowerSplitXPane.setRightComponent(contentViewerContainerPanel); + horizontalSplitPane.setLeftComponent(leftFillerPanel); - splitYPane.setRightComponent(lowerSplitXPane); + javax.swing.GroupLayout rightfillerPanelLayout = new javax.swing.GroupLayout(rightfillerPanel); + rightfillerPanel.setLayout(rightfillerPanelLayout); + rightfillerPanelLayout.setHorizontalGroup( + rightfillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 364, Short.MAX_VALUE) + ); + rightfillerPanelLayout.setVerticalGroup( + rightfillerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 54, Short.MAX_VALUE) + ); + + horizontalSplitPane.setRightComponent(rightfillerPanel); + + splitYPane.setRightComponent(horizontalSplitPane); 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)) + .addComponent(jFXstatusPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -215,11 +360,11 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JPanel contentViewerContainerPanel; - private javafx.embed.swing.JFXPanel jFXVizPanel; + private javax.swing.JSplitPane horizontalSplitPane; + private javafx.embed.swing.JFXPanel jFXViewPanel; private javafx.embed.swing.JFXPanel jFXstatusPanel; - private javax.swing.JSplitPane lowerSplitXPane; - private javax.swing.JPanel resultContainerPanel; + private javax.swing.JPanel leftFillerPanel; + private javax.swing.JPanel rightfillerPanel; private javax.swing.JSplitPane splitYPane; // End of variables declaration//GEN-END:variables @@ -229,25 +374,34 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer 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; } + + /** + * Get the string that should be used as the label above the result table. + * It displays the time range spanned by the selected events. + * + * @return A String representation of all the events displayed. + */ + @NbBundle.Messages({ + "# {0} - start of date range", + "# {1} - end of date range", + "TimeLineResultView.startDateToEndDate.text={0} to {1}"}) + private String getResultViewerSummaryString() { + Interval selectedTimeRange = controller.getSelectedTimeRange(); + if (selectedTimeRange == null) { + return ""; + } else { + final DateTimeFormatter zonedFormatter = TimeLineController.getZonedFormatter(); + String start = selectedTimeRange.getStart() + .withZone(TimeLineController.getJodaTimeZone()) + .toString(zonedFormatter); + String end = selectedTimeRange.getEnd() + .withZone(TimeLineController.getJodaTimeZone()) + .toString(zonedFormatter); + return Bundle.TimeLineResultView_startDateToEndDate_text(start, end); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java b/Core/src/org/sleuthkit/autopsy/timeline/ViewMode.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java rename to Core/src/org/sleuthkit/autopsy/timeline/ViewMode.java index 8990b792bc..bbbc3fa11b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/VisualizationMode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ViewMode.java @@ -21,7 +21,9 @@ package org.sleuthkit.autopsy.timeline; /** * */ -public enum VisualizationMode { +public enum ViewMode { - COUNTS, DETAIL; + COUNTS, + DETAIL, + LIST; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java index e50e1909a8..7649b35705 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshotAsReport.java @@ -71,7 +71,7 @@ public class SaveSnapshotAsReport extends Action { "Timeline.ModuleName=Timeline", "SaveSnapShotAsReport.action.dialogs.title=Timeline", "SaveSnapShotAsReport.action.name.text=Snapshot Report", - "SaveSnapShotAsReport.action.longText=Save a screen capture of the visualization as a report.", + "SaveSnapShotAsReport.action.longText=Save a screen capture of the current view of the timeline as a report.", "# {0} - report file path", "SaveSnapShotAsReport.ReportSavedAt=Report saved at [{0}]", "SaveSnapShotAsReport.Success=Success", diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java new file mode 100644 index 0000000000..71e022e65b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java @@ -0,0 +1,152 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.datamodel; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; + +/** + * A container for several events that have the same timestamp and description + * and are backed by the same file. Used in the ListView to coalesce the file + * system events for a file when they have the same timestamp. + */ +public class CombinedEvent { + + private final long fileID; + private final long epochMillis; + private final String description; + + /** + * A map from EventType to event ID. + */ + private final Map eventTypeMap = new HashMap<>(); + + /** + * Constructor + * + * @param epochMillis The timestamp for this event, in millis from the Unix + * epoch. + * @param description The full description shared by all the combined events + * @param fileID The ID of the file shared by all the combined events. + * @param eventMap A map from EventType to event ID. + */ + public CombinedEvent(long epochMillis, String description, long fileID, Map eventMap) { + this.epochMillis = epochMillis; + this.description = description; + eventTypeMap.putAll(eventMap); + this.fileID = fileID; + } + + /** + * Get the timestamp of this event as millis from the Unix epoch. + * + * @return The timestamp of this event as millis from the Unix epoch. + */ + public long getStartMillis() { + return epochMillis; + } + + /** + * Get the full description shared by all the combined events. + * + * @return The full description shared by all the combined events. + */ + public String getDescription() { + return description; + } + + /** + * Get the obj ID of the file shared by the combined events. + * + * @return The obj ID of the file shared by the combined events. + */ + public long getFileID() { + return fileID; + } + + /** + * Get the types of the combined events. + * + * @return The types of the combined events. + */ + public Set getEventTypes() { + return eventTypeMap.keySet(); + } + + /** + * Get the event IDs of the combined events. + * + * @return The event IDs of the combined events. + */ + public Collection getEventIDs() { + return eventTypeMap.values(); + } + + /** + * Get the event ID of one event that is representative of all the combined + * events. It can be used to look up a SingleEvent with more details, for + * example. + * + * @return An arbitrary representative event ID for the combined events. + */ + public Long getRepresentativeEventID() { + return eventTypeMap.values().stream().findFirst().get(); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 53 * hash + (int) (this.fileID ^ (this.fileID >>> 32)); + hash = 53 * hash + (int) (this.epochMillis ^ (this.epochMillis >>> 32)); + hash = 53 * hash + Objects.hashCode(this.description); + hash = 53 * hash + Objects.hashCode(this.eventTypeMap); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CombinedEvent other = (CombinedEvent) obj; + if (this.fileID != other.fileID) { + return false; + } + if (this.epochMillis != other.epochMillis) { + return false; + } + if (!Objects.equals(this.description, other.description)) { + return false; + } + if (!Objects.equals(this.eventTypeMap, other.eventTypeMap)) { + return false; + } + return true; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index ace1dd68ff..9f95d31337 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,10 +21,10 @@ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; +import java.util.Collection; import java.util.Comparator; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.SortedSet; import javax.annotation.concurrent.Immutable; import org.joda.time.Interval; @@ -108,8 +108,8 @@ public class EventCluster implements MultiEvent { */ private final ImmutableSet hashHits; - private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, - Set hashHits, Set tagged, String description, DescriptionLoD lod, + private EventCluster(Interval spanningInterval, EventType type, Collection eventIDs, + Collection hashHits, Collection tagged, String description, DescriptionLoD lod, EventStripe parent) { this.span = spanningInterval; @@ -122,8 +122,8 @@ public class EventCluster implements MultiEvent { this.parent = parent; } - public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, - Set hashHits, Set tagged, String description, DescriptionLoD lod) { + public EventCluster(Interval spanningInterval, EventType type, Collection eventIDs, + Collection hashHits, Collection tagged, String description, DescriptionLoD lod) { this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 1431673f22..6227256130 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -70,15 +70,15 @@ import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** - * This class acts as the model for a {@link TimeLineView} + * This class acts as the model for a TimelineView * * Views can register listeners on properties returned by methods. * * This class is implemented as a filtered view into an underlying - * {@link EventsRepository}. + * EventsRepository. * * TODO: as many methods as possible should cache their results so as to avoid - * unnecessary db calls through the {@link EventsRepository} -jm + * unnecessary db calls through the EventsRepository -jm * * Concurrency Policy: repo is internally synchronized, so methods that only * access the repo atomically do not need further synchronization @@ -279,7 +279,7 @@ public final class FilteredEventsModel { return repo.getTagCountsByTagName(eventIDsWithTags); } - public Set getEventIDs(Interval timeRange, Filter filter) { + public List getEventIDs(Interval timeRange, Filter filter) { final Interval overlap; final RootFilter intersect; synchronized (this) { @@ -290,6 +290,21 @@ public final class FilteredEventsModel { return repo.getEventIDs(overlap, intersect); } + /** + * Get a representation of all the events, within the given time range, that + * pass the given filter, grouped by time and description such that file + * system events for the same file, with the same timestamp, are combined + * together. + * + * @param timeRange The Interval that all returned events must be within. + * @param filter The Filter that all returned events must pass. + * + * @return A List of combined events, sorted by timestamp. + */ + public List getCombinedEvents() { + return repo.getCombinedEvents(requestedTimeRange.get(), requestedFilter.get()); + } + /** * return the number of events that pass the requested filter and are within * the given time range. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 996c35ef3b..8bbfc756af 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -56,6 +56,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.CombinedEvent; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; @@ -343,18 +344,29 @@ public class EventDB { return result; } - Set getEventIDs(Interval timeRange, RootFilter filter) { - return getEventIDs(timeRange.getStartMillis() / 1000, timeRange.getEndMillis() / 1000, filter); - } + /** + * Get the IDs of all the events within the given time range that pass the + * given filter. + * + * @param timeRange The Interval that all returned events must be within. + * @param filter The Filter that all returned events must pass. + * + * @return A List of event ids, sorted by timestamp of the corresponding + * event.. + */ + List getEventIDs(Interval timeRange, RootFilter filter) { + Long startTime = timeRange.getStartMillis() / 1000; + Long endTime = timeRange.getEndMillis() / 1000; - Set getEventIDs(Long startTime, Long endTime, RootFilter filter) { if (Objects.equals(startTime, endTime)) { - endTime++; + endTime++; //make sure end is at least 1 millisecond after start } - Set resultIDs = new HashSet<>(); + + ArrayList resultIDs = new ArrayList<>(); DBLock.lock(); - final String query = "SELECT events.event_id AS event_id FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + SQLHelper.getSQLWhere(filter); // NON-NLS + final String query = "SELECT events.event_id AS event_id FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + + " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + SQLHelper.getSQLWhere(filter) + " ORDER BY time ASC"; // NON-NLS try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query)) { while (rs.next()) { @@ -370,6 +382,55 @@ public class EventDB { return resultIDs; } + /** + * Get a representation of all the events, within the given time range, that + * pass the given filter, grouped by time and description such that file + * system events for the same file, with the same timestamp, are combined + * together. + * + * @param timeRange The Interval that all returned events must be within. + * @param filter The Filter that all returned events must pass. + * + * @return A List of combined events, sorted by timestamp. + */ + List getCombinedEvents(Interval timeRange, RootFilter filter) { + Long startTime = timeRange.getStartMillis() / 1000; + Long endTime = timeRange.getEndMillis() / 1000; + + if (Objects.equals(startTime, endTime)) { + endTime++; //make sure end is at least 1 millisecond after start + } + + ArrayList results = new ArrayList<>(); + + DBLock.lock(); + final String query = "SELECT full_description, time, file_id, GROUP_CONCAT(events.event_id), GROUP_CONCAT(sub_type)" + + " FROM events " + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + + " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + SQLHelper.getSQLWhere(filter) + + " GROUP BY time,full_description, file_id ORDER BY time ASC, full_description"; + try (Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery(query)) { + while (rs.next()) { + + //make a map from event type to event ID + List eventIDs = SQLHelper.unGroupConcat(rs.getString("GROUP_CONCAT(events.event_id)"), Long::valueOf); + List eventTypes = SQLHelper.unGroupConcat(rs.getString("GROUP_CONCAT(sub_type)"), s -> RootEventType.allTypes.get(Integer.valueOf(s))); + Map eventMap = new HashMap<>(); + for (int i = 0; i < eventIDs.size(); i++) { + eventMap.put(eventTypes.get(i), eventIDs.get(i)); + } + results.add(new CombinedEvent(rs.getLong("time") * 1000, rs.getString("full_description"), rs.getLong("file_id"), eventMap)); + } + + } catch (SQLException sqlEx) { + LOGGER.log(Level.SEVERE, "failed to execute query for combined events", sqlEx); // NON-NLS + } finally { + DBLock.unlock(); + } + + return results; + } + /** * this relies on the fact that no tskObj has ID 0 but 0 is the default * value for the datasource_id column in the events table. @@ -583,7 +644,14 @@ public class EventDB { insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, event_id) values (?,?)"); //NON-NLS insertTagStmt = prepareStatement("INSERT OR IGNORE INTO tags (tag_id, tag_name_id,tag_name_display_name, event_id) values (?,?,?,?)"); //NON-NLS deleteTagStmt = prepareStatement("DELETE FROM tags WHERE tag_id = ?"); //NON-NLS - countAllEventsStmt = prepareStatement("SELECT count(*) AS count FROM events"); //NON-NLS + + /* + * This SQL query is really just a select count(*), but that has + * performance problems on very large tables unless you include + * a where clause see http://stackoverflow.com/a/9338276/4004683 + * for more. + */ + countAllEventsStmt = prepareStatement("SELECT count(event_id) AS count FROM events WHERE event_id IS NOT null"); //NON-NLS dropEventsTableStmt = prepareStatement("DROP TABLE IF EXISTS events"); //NON-NLS dropHashSetHitsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_set_hits"); //NON-NLS dropHashSetsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_sets"); //NON-NLS @@ -1090,12 +1158,12 @@ public class EventDB { private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLoD descriptionLOD, TagsFilter filter) throws SQLException { Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, TimeLineController.getJodaTimeZone());// NON-NLS String eventIDsString = rs.getString("event_ids");// NON-NLS - Set eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf); + List eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf); String description = rs.getString(SQLHelper.getDescriptionColumn(descriptionLOD)); EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt("sub_type")) : BaseTypes.values()[rs.getInt("base_type")];// NON-NLS - Set hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS - Set tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); //NON-NLS + List hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS + List tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); //NON-NLS return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD); } @@ -1168,7 +1236,6 @@ public class EventDB { return useSubTypes ? "sub_type" : "base_type"; //NON-NLS } - private PreparedStatement prepareStatement(String queryString) throws SQLException { PreparedStatement prepareStatement = con.prepareStatement(queryString); preparedStatements.add(prepareStatement); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index a93bf9dc18..f4396e3d95 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,6 +56,7 @@ import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.CancellationProgressTask; +import org.sleuthkit.autopsy.timeline.datamodel.CombinedEvent; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; @@ -214,10 +215,25 @@ public class EventsRepository { idToEventCache.invalidateAll(); } - public Set getEventIDs(Interval timeRange, RootFilter filter) { + public List getEventIDs(Interval timeRange, RootFilter filter) { return eventDB.getEventIDs(timeRange, filter); } + /** + * Get a representation of all the events, within the given time range, that + * pass the given filter, grouped by time and description such that file + * system events for the same file, with the same timestamp, are combined + * together. + * + * @param timeRange The Interval that all returned events must be within. + * @param filter The Filter that all returned events must pass. + * + * @return A List of combined events, sorted by timestamp. + */ + public List getCombinedEvents(Interval timeRange, RootFilter filter) { + return eventDB.getCombinedEvents(timeRange, filter); + } + public Interval getSpanningInterval(Collection eventIDs) { return eventDB.getSpanningInterval(eventIDs); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java index 208ffa1b75..4f6dfa3eb9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.timeline.db; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,11 +81,11 @@ class SQLHelper { * @return a Set of X, each element mapped from one element of the original * comma delimited string */ - static Set unGroupConcat(String groupConcat, Function mapper) { - return StringUtils.isBlank(groupConcat) ? Collections.emptySet() + static List unGroupConcat(String groupConcat, Function mapper) { + return StringUtils.isBlank(groupConcat) ? Collections.emptyList() : Stream.of(groupConcat.split(",")) .map(mapper::apply) - .collect(Collectors.toSet()); + .collect(Collectors.toList()); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java index 47b04eb479..903cb55178 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/RefreshRequestedEvent.java @@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.timeline.events; /** * A "local" event published by filteredEventsModel to indicate that the user - * requested that the current visualization be refreshed with out changing any + * requested that the current view be refreshed with out changing any * of the parameters ( to include more up to date tag data for example.) * * This event is not intended for use out side of the Timeline module. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/Bundle_ja.properties index d431985f98..7096881555 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/Bundle_ja.properties @@ -1 +1 @@ -EventRoodNode.tooManyNode.displayName=\u8868\u793A\u3059\u308B\u30A4\u30D9\u30F3\u30C8\u6570\u304C\u591A\u3059\u304E\u307E\u3059\u3002\u6700\u5927 \= {0}\u3002\u8868\u793A\u3059\u308B\u30A4\u30D9\u30F3\u30C8\u306F{1}\u3042\u308A\u307E\u3059\u3002 \ No newline at end of file +EventRoodNode.tooManyNode.displayName=\u8868\u793a\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u6570\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u6700\u5927 \= {0}\u3002\u8868\u793a\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306f{1}\u3042\u308a\u307e\u3059\u3002 \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index abb7245eda..ee22cfd295 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,47 +23,60 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; -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.NbBundle; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; 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.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** - * * Explorer Node for {@link SingleEvent}s. + * * Explorer Node for a SingleEvent. */ -class EventNode extends DisplayableItemNode { +public class EventNode extends DisplayableItemNode { + + private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(EventNode.class.getName()); - private final SingleEvent e; + private final SingleEvent event; - EventNode(SingleEvent eventById, AbstractFile file, BlackboardArtifact artifact) { - super(Children.LEAF, Lookups.fixed(eventById, file, artifact)); - this.e = eventById; - this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getEventType().getIconBase()); // NON-NLS + EventNode(SingleEvent event, AbstractFile file, BlackboardArtifact artifact) { + super(Children.LEAF, Lookups.fixed(event, file, artifact)); + this.event = event; + this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + event.getEventType().getIconBase()); // NON-NLS } - EventNode(SingleEvent eventById, AbstractFile file) { - super(Children.LEAF, Lookups.fixed(eventById, file)); - this.e = eventById; - this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + e.getEventType().getIconBase()); // NON-NLS + EventNode(SingleEvent event, AbstractFile file) { + super(Children.LEAF, Lookups.fixed(event, file)); + this.event = event; + this.setIconBaseWithExtension("org/sleuthkit/autopsy/timeline/images/" + event.getEventType().getIconBase()); // NON-NLS } @Override + @NbBundle.Messages({ + "NodeProperty.displayName.icon=Icon", + "NodeProperty.displayName.description=Description", + "NodeProperty.displayName.baseType=Base Type", + "NodeProperty.displayName.subType=Sub Type", + "NodeProperty.displayName.known=Known", + "NodeProperty.displayName.dateTime=Date/Time"}) protected Sheet createSheet() { Sheet s = super.createSheet(); Sheet.Set properties = s.get(Sheet.PROPERTIES); @@ -72,28 +85,25 @@ class EventNode extends DisplayableItemNode { s.put(properties); } - final TimeProperty timePropery = new TimeProperty("time", "Date/Time", "time ", getDateTimeString()); // NON-NLS - - TimeLineController.getTimeZone().addListener((Observable observable) -> { - try { - timePropery.setValue(getDateTimeString()); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - LOGGER.log(Level.SEVERE, "unexpected error setting date/time property on EventNode explorer node", ex); //NON-NLS - } - }); - - properties.put(new NodeProperty<>("icon", "Icon", "icon", true)); // NON-NLS //gets overridden with icon - properties.put(timePropery); - properties.put(new NodeProperty<>("description", "Description", "description", e.getFullDescription())); // NON-NLS - properties.put(new NodeProperty<>("eventBaseType", "Base Type", "base type", e.getEventType().getSuperType().getDisplayName())); // NON-NLS - properties.put(new NodeProperty<>("eventSubType", "Sub Type", "sub type", e.getEventType().getDisplayName())); // NON-NLS - properties.put(new NodeProperty<>("Known", "Known", "known", e.getKnown().toString())); // NON-NLS + properties.put(new NodeProperty<>("icon", Bundle.NodeProperty_displayName_icon(), "icon", true)); // NON-NLS //gets overridden with icon + properties.put(new TimeProperty("time", Bundle.NodeProperty_displayName_dateTime(), "time ", getDateTimeString()));// NON-NLS + properties.put(new NodeProperty<>("description", Bundle.NodeProperty_displayName_description(), "description", event.getFullDescription())); // NON-NLS + properties.put(new NodeProperty<>("eventBaseType", Bundle.NodeProperty_displayName_baseType(), "base type", event.getEventType().getSuperType().getDisplayName())); // NON-NLS + properties.put(new NodeProperty<>("eventSubType", Bundle.NodeProperty_displayName_subType(), "sub type", event.getEventType().getDisplayName())); // NON-NLS + properties.put(new NodeProperty<>("Known", Bundle.NodeProperty_displayName_known(), "known", event.getKnown().toString())); // NON-NLS return s; } + /** + * Get the time of this event as a String formated according to the + * controller's time zone setting. + * + * @return The time of this event as a String formated according to the + * controller's time zone setting. + */ private String getDateTimeString() { - return new DateTime(e.getStartMillis(), DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter()); + return new DateTime(event.getStartMillis(), DateTimeZone.UTC).toString(TimeLineController.getZonedFormatter()); } @Override @@ -118,7 +128,7 @@ class EventNode extends DisplayableItemNode { @Override public T accept(DisplayableItemNodeVisitor dinv) { - throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates. + throw new UnsupportedOperationException("Not supported yet."); // NON-NLS } /* @@ -134,7 +144,7 @@ class EventNode extends DisplayableItemNode { * We use TimeProperty instead of a normal NodeProperty to correctly display * the date/time when the user changes the timezone setting. */ - private class TimeProperty extends PropertySupport.ReadWrite { + final private class TimeProperty extends PropertySupport.ReadWrite { private String value; @@ -147,6 +157,14 @@ class EventNode extends DisplayableItemNode { super(name, String.class, displayName, shortDescription); setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button NON-NLS this.value = value; + TimeLineController.getTimeZone().addListener(timeZone -> { + try { + setValue(getDateTimeString()); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + LOGGER.log(Level.SEVERE, "Unexpected error setting date/time property on EventNode explorer node", ex); //NON-NLS + } + }); + } @Override @@ -161,4 +179,32 @@ class EventNode extends DisplayableItemNode { firePropertyChange("time", oldValue, t); // NON-NLS } } + + /** + * Factory method to create an EventNode from the event ID and the events + * model. + * + * @param eventID The ID of the event this node is for. + * @param eventsModel The model that provides access to the events DB. + * + * @return An EventNode with the file (and artifact) backing this event in + * its lookup. + */ + public static EventNode createEventNode(final Long eventID, FilteredEventsModel eventsModel) throws TskCoreException, IllegalStateException { + /* + * Look up the event by id and creata an EventNode with the appropriate + * data in the lookup. + */ + final SingleEvent eventById = eventsModel.getEventById(eventID); + + SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + AbstractFile file = sleuthkitCase.getAbstractFileById(eventById.getFileID()); + + if (eventById.getArtifactID().isPresent()) { + BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(eventById.getArtifactID().get()); + return new EventNode(eventById, file, blackboardArtifact); + } else { + return new EventNode(eventById, file); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java index 6fd3693514..3d1d44132e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -27,22 +27,19 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; -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.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** - * Root Explorer node to represent events. + * Root Explorer Node to represent events. */ public class EventRootNode extends DisplayableItemNode { + private static final long serialVersionUID = 1L; + /** * Since the lazy loading seems to be broken if there are more than this * many child events, we don't show them and just show a message showing the @@ -50,18 +47,8 @@ public class EventRootNode extends DisplayableItemNode { */ public static final int MAX_EVENTS_TO_DISPLAY = 5000; - /** - * the number of child events - */ - private final int childCount; - - public EventRootNode(String NAME, Collection fileIds, FilteredEventsModel filteredEvents) { + public EventRootNode(Collection fileIds, FilteredEventsModel filteredEvents) { super(Children.create(new EventNodeChildFactory(fileIds, filteredEvents), true), Lookups.singleton(fileIds)); - - super.setName(NAME); - super.setDisplayName(NAME); - - childCount = fileIds.size(); } @Override @@ -74,9 +61,6 @@ public class EventRootNode extends DisplayableItemNode { return null; } - public int getChildCount() { - return childCount; - } /* * TODO (AUT-1849): Correct or remove peristent column reordering code @@ -95,12 +79,12 @@ public class EventRootNode extends DisplayableItemNode { private static final Logger LOGGER = Logger.getLogger(EventNodeChildFactory.class.getName()); /** - * list of event ids that act as keys for the child nodes. + * List of event IDs that act as keys for the child nodes. */ private final Collection eventIDs; /** - * filteredEvents is used to lookup the events from their ids + * filteredEvents is used to lookup the events from their IDs */ private final FilteredEventsModel filteredEvents; @@ -112,8 +96,8 @@ public class EventRootNode extends DisplayableItemNode { @Override protected boolean createKeys(List toPopulate) { /** - * if there are too many events, just add one id (-1) to indicate - * this. + * If there are too many events, just add one dummy ID (-1) to + * indicate this. */ if (eventIDs.size() < MAX_EVENTS_TO_DISPLAY) { toPopulate.addAll(eventIDs); @@ -127,34 +111,24 @@ public class EventRootNode extends DisplayableItemNode { protected Node createNodeForKey(Long eventID) { if (eventID < 0) { /* - * if the eventId is a the special value, return a node with a - * warning that their are too many evens + * If the eventId is a the special value ( -1 ), return a node + * with a warning that their are too many evens */ return new TooManyNode(eventIDs.size()); } else { - /* - * look up the event by id and creata an EventNode with the - * appropriate data in the lookup. - */ - final SingleEvent eventById = filteredEvents.getEventById(eventID); try { - SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); - AbstractFile file = sleuthkitCase.getAbstractFileById(eventById.getFileID()); - if (file != null) { - if (eventById.getArtifactID().isPresent()) { - BlackboardArtifact blackboardArtifact = sleuthkitCase.getBlackboardArtifact(eventById.getArtifactID().get()); - return new EventNode(eventById, file, blackboardArtifact); - } else { - return new EventNode(eventById, file); - } - } else { - //This should never happen in normal operations - LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent."); // NON-NLS - return null; - } - } catch (IllegalStateException | TskCoreException ex) { - //if some how the case was closed or ther is another unspecified exception, just bail out with a warning. - LOGGER.log(Level.WARNING, "Failed to lookup sleuthkit object backing TimeLineEvent.", ex); // NON-NLS + return EventNode.createEventNode(eventID, filteredEvents); + } catch (IllegalStateException ex) { + //Since the case is closed, the user probably doesn't care about this, just log it as a precaution. + LOGGER.log(Level.SEVERE, "There was no case open to lookup the Sleuthkit object backing a SingleEvent.", ex); // NON-NLS + return null; + } catch (TskCoreException ex) { + /* + * Just log it: There might be lots of these errors, and we + * don't want to flood the user with notifications. It will + * be obvious the UI is broken anyways + */ + LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS return null; } } @@ -162,8 +136,7 @@ public class EventRootNode extends DisplayableItemNode { } /** - * A Node that just shows a warning message that their are too many events - * to show + * A Node with a warning message that their are too many events to show. */ private static class TooManyNode extends AbstractNode { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/table.png b/Core/src/org/sleuthkit/autopsy/timeline/images/table.png new file mode 100644 index 0000000000..0d1e11a834 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/table.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java b/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java index 75377ff5bb..917d2a0881 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/snapshot/SnapShotReportWriter.java @@ -70,7 +70,7 @@ public class SnapShotReportWriter { * @param zoomParams The ZoomParams in effect when the snapshot was * taken. * @param generationDate The generation Date of the report. - * @param snapshot A snapshot of the visualization to include in the + * @param snapshot A snapshot of the view to include in the * report. */ public SnapShotReportWriter(Case currentCase, Path reportFolderPath, String reportName, ZoomParams zoomParams, Date generationDate, BufferedImage snapshot) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java new file mode 100644 index 0000000000..86fa7ec132 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java @@ -0,0 +1,372 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.timeline.ui; + +import com.google.common.eventbus.Subscribe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.concurrent.Task; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import org.controlsfx.control.MaskerPane; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.LoggedTask; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; + +public abstract class AbstractTimeLineView extends BorderPane { + + private static final Logger LOGGER = Logger.getLogger(AbstractTimeLineView.class.getName()); + + /** + * Boolean property that holds true if the view does not show any events + * with the current zoom and filter settings. + */ + private final ReadOnlyBooleanWrapper hasVisibleEvents = new ReadOnlyBooleanWrapper(true); + + /** + * Boolean property that holds true if the view may not represent the + * current state of the DB, because, for example, tags have been updated but + * the view. was not refreshed. + */ + private final ReadOnlyBooleanWrapper outOfDate = new ReadOnlyBooleanWrapper(false); + + /** + * List of Nodes to insert into the toolbar. This should be set in an + * implementation's constructor. + */ + private List settingsNodes; + + /** + * Listener that is attached to various properties that should trigger a + * view update when they change. + */ + private InvalidationListener updateListener = (Observable any) -> refresh(); + + /** + * Task used to reload the content of this view + */ + private Task updateTask; + + private final TimeLineController controller; + private final FilteredEventsModel filteredEvents; + + /** + * Constructor + * + * @param controller + */ + public AbstractTimeLineView(TimeLineController controller) { + this.controller = controller; + this.filteredEvents = controller.getEventsModel(); + this.filteredEvents.registerForEvents(this); + this.filteredEvents.zoomParametersProperty().addListener(updateListener); + TimeLineController.getTimeZone().addListener(updateListener); + } + + /** + * Handle a RefreshRequestedEvent from the events model by updating the + * view. + * + * @param event The RefreshRequestedEvent to handle. + */ + @Subscribe + public void handleRefreshRequested(RefreshRequestedEvent event) { + refresh(); + } + + /** + * Does the view represent an out-of-date state of the DB. It might if, for + * example, tags have been updated but the view was not refreshed. + * + * @return True if the view does not represent the current state of the DB. + */ + public boolean isOutOfDate() { + return outOfDate.get(); + } + + /** + * Get a ReadOnlyBooleanProperty that holds true if this view does not + * represent the current state of the DB> + * + * @return A ReadOnlyBooleanProperty that holds the out-of-date state for + * this view. + */ + public ReadOnlyBooleanProperty outOfDateProperty() { + return outOfDate.getReadOnlyProperty(); + } + + /** + * Get the TimelineController for this view. + * + * @return The TimelineController for this view. + */ + protected TimeLineController getController() { + return controller; + } + + /** + * Refresh this view based on current state of zoom / filters. Primarily + * this invokes the background ViewRefreshTask returned by + * getUpdateTask(), which derived classes must implement. + * + * TODO: replace this logic with a javafx Service ? -jm + */ + protected final synchronized void refresh() { + if (updateTask != null) { + updateTask.cancel(true); + updateTask = null; + } + updateTask = getNewUpdateTask(); + updateTask.stateProperty().addListener((Observable observable) -> { + switch (updateTask.getState()) { + case CANCELLED: + case FAILED: + case READY: + case RUNNING: + case SCHEDULED: + break; + case SUCCEEDED: + try { + this.hasVisibleEvents.set(updateTask.get()); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Unexpected exception updating view", ex); //NON-NLS + } + break; + } + }); + getController().monitorTask(updateTask); + } + + /** + * Get the FilteredEventsModel for this view. + * + * @return The FilteredEventsModel for this view. + */ + protected FilteredEventsModel getEventsModel() { + return filteredEvents; + } + + /** + * Get a new background Task that fetches the appropriate data and loads it + * into this view. + * + * @return A new task to execute on a background thread to reload this view + * with different data. + */ + protected abstract Task getNewUpdateTask(); + + /** + * Get a List of Nodes containing settings widgets to insert into this + * view's header. + * + * @return The List of settings Nodes. + */ + protected List getSettingsNodes() { + return Collections.unmodifiableList(settingsNodes); + } + + /** + * Set the List of Nodes containing settings widgets to insert into this + * view's header. + * + * + * @param settingsNodes The List of Nodes containing settings widgets to + * insert into this view's header. + */ + final protected void setSettingsNodes(List settingsNodes) { + this.settingsNodes = new ArrayList<>(settingsNodes); + } + + /** + * Dispose of this view and any resources it holds onto. + */ + final synchronized void dispose() { + //cancel and gc updateTask + if (updateTask != null) { + updateTask.cancel(true); + updateTask = null; + } + //remvoe and gc updateListener + this.filteredEvents.zoomParametersProperty().removeListener(updateListener); + TimeLineController.getTimeZone().removeListener(updateListener); + updateListener = null; + filteredEvents.unRegisterForEvents(this); + } + + /** + * Are there are any events visible in this view with the current view + * parameters? + * + * @return True if there are events visible in this view with the current + * view parameters. + */ + boolean hasVisibleEvents() { + return hasVisibleEventsProperty().get(); + } + + /** + * A property that indicates whether there are any events visible in this + * view with the current view parameters. + * + * @return A property that indicates whether there are any events visible in + * this view with the current view parameters. + */ + ReadOnlyBooleanProperty hasVisibleEventsProperty() { + return hasVisibleEvents.getReadOnlyProperty(); + } + + /** + * Set this view out of date because, for example, tags have been updated + * but the view was not refreshed. + */ + void setOutOfDate() { + outOfDate.set(true); + } + + /** + * Clear all data items from this chart. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + abstract protected void clearData(); + + /** + * Base class for Tasks that refreshes a view when the view settings change. + * + * @param The type of a single object that can represent + * the range of data displayed along the X-Axis. + */ + protected abstract class ViewRefreshTask extends LoggedTask { + + private final Node center; + + /** + * Constructor + * + * @param taskName The name of this task. + * @param logStateChanges Whether or not task state changes should be + * logged. + */ + protected ViewRefreshTask(String taskName, boolean logStateChanges) { + super(taskName, logStateChanges); + this.center = getCenter(); + } + + /** + * Sets initial progress value and message and shows blocking progress + * indicator over the view. Derived Tasks should be sure to call this as + * part of their call() implementation. + * + * @return True + * + * @throws Exception If there is an unhandled exception during the + * background operation + */ + @NbBundle.Messages(value = {"ViewRefreshTask.preparing=Analyzing zoom and filter settings"}) + @Override + protected Boolean call() throws Exception { + updateProgress(-1, 1); + updateMessage(Bundle.ViewRefreshTask_preparing()); + Platform.runLater(() -> { + MaskerPane maskerPane = new MaskerPane(); + maskerPane.textProperty().bind(messageProperty()); + maskerPane.progressProperty().bind(progressProperty()); + setCenter(new StackPane(center, maskerPane)); + setCursor(Cursor.WAIT); + }); + return true; + } + + /** + * Updates the horizontal axis and removes the blocking progress + * indicator. Derived Tasks should be sure to call this as part of their + * succeeded() implementation. + */ + @Override + protected void succeeded() { + super.succeeded(); + outOfDate.set(false); + cleanup(); + } + + /** + * Removes the blocking progress indicator. Derived Tasks should be sure + * to call this as part of their cancelled() implementation. + */ + @Override + protected void cancelled() { + super.cancelled(); + cleanup(); + } + + /** + * Removes the blocking progress indicator. Derived Tasks should be sure + * to call this as part of their failed() implementation. + */ + @Override + protected void failed() { + super.failed(); + cleanup(); + } + + /** + * Removes the blocking progress indicator and reset the cursor to the + * default. + */ + private void cleanup() { + setCenter(center); //clear masker pane installed in call() + setCursor(Cursor.DEFAULT); + } + + /** + * Set the horizontal range that this chart will show. + * + * @param values A single object representing the range that this chart + * will show. + */ + protected abstract void setDateValues(AxisValuesType values); + + /** + * Clears the chart data and sets the horizontal axis range. For use + * within the derived implementation of the call() method. + * + * @param axisValues + */ + @ThreadConfined(type = ThreadConfined.ThreadType.NOT_UI) + protected void resetView(AxisValuesType axisValues) { + Platform.runLater(() -> { + clearData(); + setDateValues(axisValues); + }); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java similarity index 54% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java index 377468db0a..976c6f3fff 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualizationPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimelineChart.java @@ -18,28 +18,16 @@ */ package org.sleuthkit.autopsy.timeline.ui; -import com.google.common.eventbus.Subscribe; -import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; import javafx.beans.binding.DoubleBinding; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; -import javafx.concurrent.Task; import javafx.geometry.Pos; -import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.XYChart; @@ -47,7 +35,6 @@ import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; import javafx.scene.layout.Border; -import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; @@ -55,7 +42,6 @@ import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; @@ -64,18 +50,14 @@ import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javax.annotation.concurrent.Immutable; import org.apache.commons.lang3.StringUtils; -import org.controlsfx.control.MaskerPane; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; /** - * Abstract base class for TimeLineChart based visualizations. + * Abstract base class for TimeLineChart based views. * * @param The type of data plotted along the x axis * @param The type of data plotted along the y axis @@ -88,36 +70,33 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * * TODO: pull up common history context menu items out of derived classes? -jm */ -public abstract class AbstractVisualizationPane> extends BorderPane { +public abstract class AbstractTimelineChart> extends AbstractTimeLineView { - private static final Logger LOGGER = Logger.getLogger(AbstractVisualizationPane.class.getName()); + private static final Logger LOGGER = Logger.getLogger(AbstractTimelineChart.class.getName()); - @NbBundle.Messages("AbstractVisualization.Default_Tooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.") - private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractVisualization_Default_Tooltip_text()); + @NbBundle.Messages("AbstractTimelineChart.defaultTooltip.text=Drag the mouse to select a time interval to zoom into.\nRight-click for more actions.") + private static final Tooltip DEFAULT_TOOLTIP = new Tooltip(Bundle.AbstractTimelineChart_defaultTooltip_text()); private static final Border ONLY_LEFT_BORDER = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(0, 0, 0, 1))); /** - * Get the tool tip to use for this visualization when no more specific - * Tooltip is needed. + * Get the tool tip to use for this view when no more specific Tooltip is + * needed. * * @return The default Tooltip. */ - public static Tooltip getDefaultTooltip() { + static public Tooltip getDefaultTooltip() { return DEFAULT_TOOLTIP; } /** - * Boolean property that holds true if the visualization may not represent - * the current state of the DB, because, for example, tags have been updated - * but the vis. was not refreshed. + * The nodes that are selected. + * + * @return An ObservableList of the nodes that are selected in + * this view. */ - private final ReadOnlyBooleanWrapper outOfDate = new ReadOnlyBooleanWrapper(false); - - /** - * Boolean property that holds true if the visualization does not show any - * events with the current zoom and filter settings. - */ - private final ReadOnlyBooleanWrapper hasVisibleEvents = new ReadOnlyBooleanWrapper(true); + protected ObservableList getSelectedNodes() { + return selectedNodes; + } /** * Access to chart data via series @@ -130,123 +109,36 @@ public abstract class AbstractVisualizationPane updateTask; - - final private TimeLineController controller; - final private FilteredEventsModel filteredEvents; - final private ObservableList selectedNodes = FXCollections.observableArrayList(); - /** - * Listener that is attached to various properties that should trigger a vis - * update when they change. - */ - private InvalidationListener updateListener = any -> refresh(); + public Pane getSpecificLabelPane() { + return specificLabelPane; + } - /** - * Does the visualization represent an out-of-date state of the DB. It might - * if, for example, tags have been updated but the vis. was not refreshed. - * - * @return True if the visualization does not represent the curent state of - * the DB. - */ - public boolean isOutOfDate() { - return outOfDate.get(); + public Pane getContextLabelPane() { + return contextLabelPane; + } + + public Region getSpacer() { + return spacer; } /** - * Set this visualization out of date because, for example, tags have been - * updated but the vis. was not refreshed. - */ - void setOutOfDate() { - outOfDate.set(true); - } - - /** - * Get a ReadOnlyBooleanProperty that holds true if this visualization does - * not represent the current state of the DB> + * Get the CharType that implements this view. * - * @return A ReadOnlyBooleanProperty that holds the out-of-date state for - * this visualization. - */ - public ReadOnlyBooleanProperty outOfDateProperty() { - return outOfDate.getReadOnlyProperty(); - } - - /** - * The visualization nodes that are selected. - * - * @return An ObservableList of the nodes that are selected in - * this visualization. - */ - protected ObservableList getSelectedNodes() { - return selectedNodes; - } - - /** - * List of Nodes to insert into the toolbar. This should be set in an - * implementations constructor. - */ - private List settingsNodes; - - /** - * Get a List of nodes containing settings widgets to insert into this - * visualization's header. - * - * @return The List of settings Nodes. - */ - protected List getSettingsNodes() { - return Collections.unmodifiableList(settingsNodes); - } - - /** - * Set the List of nodes containing settings widgets to insert into this - * visualization's header. - * - * - * @param settingsNodes The List of nodes containing settings widgets to - * insert into this visualization's header. - */ - protected void setSettingsNodes(List settingsNodes) { - this.settingsNodes = new ArrayList<>(settingsNodes); - } - - /** - * Get the TimelineController for this visualization. - * - * @return The TimelineController for this visualization. - */ - protected TimeLineController getController() { - return controller; - } - - /** - * Get the CharType that implements this visualization. - * - * @return The CharType that implements this visualization. + * @return The CharType that implements this view. */ protected ChartType getChart() { return chart; } /** - * Get the FilteredEventsModel for this visualization. + * Set the ChartType that implements this view. * - * @return The FilteredEventsModel for this visualization. - */ - protected FilteredEventsModel getEventsModel() { - return filteredEvents; - } - - /** - * Set the ChartType that implements this visualization. - * - * @param chart The ChartType that implements this visualization. + * @param chart The ChartType that implements this view. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) protected void setChart(ChartType chart) { @@ -255,29 +147,7 @@ public abstract class AbstractVisualizationPane getNewUpdateTask(); - /** * Get the label that should be used for a tick mark at the given value. * @@ -342,16 +203,16 @@ public abstract class AbstractVisualizationPane getXAxis(); /** - * Get the Y-Axis of this Visualization's chart + * Get the Y-Axis of this view's chart * - * @return The vertical axis used by this Visualization's chart + * @return The vertical axis used by this view's chart */ abstract protected Axis getYAxis(); @@ -364,74 +225,6 @@ public abstract class AbstractVisualizationPane { - switch (updateTask.getState()) { - case CANCELLED: - case FAILED: - case READY: - case RUNNING: - case SCHEDULED: - break; - case SUCCEEDED: - try { - this.hasVisibleEvents.set(updateTask.get()); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Unexpected exception updating visualization", ex); //NON-NLS - } - break; - } - }); - controller.monitorTask(updateTask); - } - - /** - * Handle a RefreshRequestedEvent from the events model by updating the - * visualization. - * - * @param event The RefreshRequestedEvent to handle. - */ - @Subscribe - public void handleRefreshRequested(RefreshRequestedEvent event) { - refresh(); - } - - /** - * Dispose of this visualization and any resources it holds onto. - */ - final synchronized void dispose() { - - //cancel and gc updateTask - if (updateTask != null) { - updateTask.cancel(true); - updateTask = null; - } - //remvoe and gc updateListener - this.filteredEvents.zoomParametersProperty().removeListener(updateListener); - TimeLineController.getTimeZone().removeListener(updateListener); - updateListener = null; - - filteredEvents.unRegisterForEvents(this); - } - /** * Make a series for each event type in a consistent order. */ @@ -459,23 +252,20 @@ public abstract class AbstractVisualizationPane { - VBox vBox = new VBox(specificLabelPane, contextLabelPane); + VBox vBox = new VBox(getSpecificLabelPane(), getContextLabelPane()); vBox.setFillWidth(false); - HBox hBox = new HBox(spacer, vBox); + HBox hBox = new HBox(getSpacer(), vBox); hBox.setFillHeight(false); setBottom(hBox); DoubleBinding spacerSize = getYAxis().widthProperty().add(getYAxis().tickLengthProperty()).add(getAxisMargin()); - spacer.minWidthProperty().bind(spacerSize); - spacer.prefWidthProperty().bind(spacerSize); - spacer.maxWidthProperty().bind(spacerSize); + getSpacer().minWidthProperty().bind(spacerSize); + getSpacer().prefWidthProperty().bind(spacerSize); + getSpacer().maxWidthProperty().bind(spacerSize); }); createSeries(); @@ -487,10 +277,8 @@ public abstract class AbstractVisualizationPane controller.setStatusMessage(isHover() ? DEFAULT_TOOLTIP.getText() : "")); + hoverProperty().addListener(hoverProp -> controller.setStatusMessage(isHover() ? getDefaultTooltip().getText() : "")); } @@ -689,117 +477,4 @@ public abstract class AbstractVisualizationPane The type of a single object that can represent - * the range of data displayed along the X-Axis. - */ - abstract protected class VisualizationRefreshTask extends LoggedTask { - - private final Node center; - - /** - * Constructor - * - * @param taskName The name of this task. - * @param logStateChanges Whether or not task state changes should be - * logged. - */ - protected VisualizationRefreshTask(String taskName, boolean logStateChanges) { - super(taskName, logStateChanges); - this.center = getCenter(); - } - - /** - * Sets initial progress value and message and shows blocking progress - * indicator over the visualization. Derived Tasks should be sure to - * call this as part of their call() implementation. - * - * @return True - * - * @throws Exception If there is an unhandled exception during the - * background operation - */ - @NbBundle.Messages({"VisualizationUpdateTask.preparing=Analyzing zoom and filter settings"}) - @Override - protected Boolean call() throws Exception { - updateProgress(-1, 1); - updateMessage(Bundle.VisualizationUpdateTask_preparing()); - Platform.runLater(() -> { - MaskerPane maskerPane = new MaskerPane(); - maskerPane.textProperty().bind(messageProperty()); - maskerPane.progressProperty().bind(progressProperty()); - setCenter(new StackPane(center, maskerPane)); - setCursor(Cursor.WAIT); - }); - - return true; - } - - /** - * Updates the horizontal axis and removes the blocking progress - * indicator. Derived Tasks should be sure to call this as part of their - * succeeded() implementation. - */ - @Override - protected void succeeded() { - super.succeeded(); - outOfDate.set(false); - layoutDateLabels(); - cleanup(); - } - - /** - * Removes the blocking progress indicator. Derived Tasks should be sure - * to call this as part of their cancelled() implementation. - */ - @Override - protected void cancelled() { - super.cancelled(); - cleanup(); - } - - /** - * Removes the blocking progress indicator. Derived Tasks should be sure - * to call this as part of their failed() implementation. - */ - @Override - protected void failed() { - super.failed(); - cleanup(); - } - - /** - * Removes the blocking progress indicator and reset the cursor to the - * default. - */ - private void cleanup() { - setCenter(center); //clear masker pane installed in call() - setCursor(Cursor.DEFAULT); - } - - /** - * Clears the chart data and sets the horizontal axis range. For use - * within the derived implementation of the call() method. - * - * @param axisValues - */ - @ThreadConfined(type = ThreadConfined.ThreadType.NOT_UI) - protected void resetChart(AxisValuesType axisValues) { - Platform.runLater(() -> { - clearChartData(); - setDateAxisValues(axisValues); - }); - } - - /** - * Set the horizontal range that this chart will show. - * - * @param values A single object representing the range that this chart - * will show. - */ - abstract protected void setDateAxisValues(AxisValuesType values); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties index b08bf41e42..9751a035d5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle.properties @@ -35,14 +35,13 @@ Timeline.ui.ZoomRanges.threeyears.text=Three Years Timeline.ui.ZoomRanges.fiveyears.text=Five Years Timeline.ui.ZoomRanges.tenyears.text=Ten Years Timeline.ui.ZoomRanges.all.text=All -TimeLineResultView.startDateToEndDate.text={0} to {1} -VisualizationPanel.histogramTask.title=Rebuild Histogram -VisualizationPanel.histogramTask.preparing=preparing -VisualizationPanel.histogramTask.resetUI=resetting ui -VisualizationPanel.histogramTask.queryDb=querying db -VisualizationPanel.histogramTask.updateUI2=updating ui -VisualizationPanel.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings. -VisualizationPanel.zoomButton.text=Zoom to events +ViewFrame.histogramTask.title=Rebuild Histogram +ViewFrame.histogramTask.preparing=preparing +ViewFrame.histogramTask.resetUI=resetting ui +ViewFrame.histogramTask.queryDb=querying db +ViewFrame.histogramTask.updateUI2=updating ui +ViewFrame.noEventsDialogLabel.text=There are no events visible with the current zoom / filter settings. +ViewFrame.zoomButton.text=Zoom to events TimeZonePanel.localRadio.text=Local Time Zone TimeZonePanel.otherRadio.text=GMT / UTC -VisualizationPanel.resetFiltersButton.text=Reset all filters +ViewFrame.resetFiltersButton.text=Reset all filters diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle_ja.properties index 2ba0472b32..5c8dacfed2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/Bundle_ja.properties @@ -1,24 +1,24 @@ Timeline.node.root=\u30eb\u30fc\u30c8 Timeline.ui.TimeLineChart.tooltip.text=\u30c0\u30d6\u30eb\u30af\u30ea\u30c3\u30af\u3067\u4e0b\u8a18\u306e\u7bc4\u56f2\u3078\u30ba\u30fc\u30e0\uff1a\n{0}\u301c{1}\n\u53f3\u30af\u30ea\u30c3\u30af\u3067\u5143\u306b\u623b\u308a\u307e\u3059\u3002 Timeline.ui.ZoomRanges.all.text=\u5168\u3066 -TimeLineResultView.startDateToEndDate.text={0}\u304b\u3089{1} -VisualizationPanel.histogramTask.preparing=\u6e96\u5099\u4e2d -VisualizationPanel.histogramTask.queryDb=DB\u3092\u30af\u30a8\u30ea\u4e2d -VisualizationPanel.histogramTask.resetUI=ui\u3092\u518d\u8a2d\u5b9a\u4e2d -VisualizationPanel.histogramTask.title=\u30d2\u30b9\u30c8\u30b0\u30e9\u30e0\u3092\u518d\u30d3\u30eb\u30c9 -VisualizationPanel.histogramTask.updateUI2=ui\u3092\u66f4\u65b0\u4e2d + +ViewFrame.histogramTask.preparing=\u6e96\u5099\u4e2d +ViewFrame.histogramTask.queryDb=DB\u3092\u30af\u30a8\u30ea\u4e2d +ViewFrame.histogramTask.resetUI=ui\u3092\u518d\u8a2d\u5b9a\u4e2d +ViewFrame.histogramTask.title=\u30d2\u30b9\u30c8\u30b0\u30e9\u30e0\u3092\u518d\u30d3\u30eb\u30c9 +ViewFrame.histogramTask.updateUI2=ui\u3092\u66f4\u65b0\u4e2d TimeZonePanel.localRadio.text=\u30ed\u30fc\u30ab\u30eb\u30bf\u30a4\u30e0\u30be\u30fc\u30f3 -VisualizationPanel.countsToggle.text=\u30ab\u30a6\u30f3\u30c8 -VisualizationPanel.detailsToggle.text=\u8a73\u7d30 -VisualizationPanel.endLabel.text=\u30a8\u30f3\u30c9\uff1a -VisualizationPanel.noEventsDialogLabel.text=\u73fe\u5728\u306e\u30ba\u30fc\u30e0\uff0f\u30d5\u30a3\u30eb\u30bf\u30fc\u8a2d\u5b9a\u3067\u306f\u898b\u3048\u308b\u30a4\u30d9\u30f3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002 -VisualizationPanel.resetFiltersButton.text=\u5168\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8 -VisualizationPanel.startLabel.text=\u30b9\u30bf\u30fc\u30c8\uff1a -VisualizationPanel.visualizationModeLabel.text=\u30d3\u30b8\u30e5\u30a2\u30e9\u30a4\u30bc\u30fc\u30b7\u30e7\u30f3\u30e2\u30fc\u30c9\uff1a -VisualizationPanel.zoomButton.text=\u30a4\u30d9\u30f3\u30c8\u3078\u30ba\u30fc\u30e0 -VisualizationPanel.zoomMenuButton.text=\u4e0b\u8a18\u3078\u30ba\u30fc\u30e0\u30a4\u30f3\uff0f\u30ba\u30fc\u30e0\u30a2\u30a6\u30c8 +ViewFrame.countsToggle.text=\u30ab\u30a6\u30f3\u30c8 +ViewFrame.detailsToggle.text=\u8a73\u7d30 +ViewFrame.endLabel.text=\u30a8\u30f3\u30c9\uff1a +ViewFrame.noEventsDialogLabel.text=\u73fe\u5728\u306e\u30ba\u30fc\u30e0\uff0f\u30d5\u30a3\u30eb\u30bf\u30fc\u8a2d\u5b9a\u3067\u306f\u898b\u3048\u308b\u30a4\u30d9\u30f3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002 +ViewFrame.resetFiltersButton.text=\u5168\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u30fc\u3092\u30ea\u30bb\u30c3\u30c8 +ViewFrame.startLabel.text=\u30b9\u30bf\u30fc\u30c8\uff1a +ViewFrame.viewModeLabel.text=\u30d3\u30b8\u30e5\u30a2\u30e9\u30a4\u30bc\u30fc\u30b7\u30e7\u30f3\u30e2\u30fc\u30c9\uff1a +ViewFrame.zoomButton.text=\u30a4\u30d9\u30f3\u30c8\u3078\u30ba\u30fc\u30e0 +ViewFrame.zoomMenuButton.text=\u4e0b\u8a18\u3078\u30ba\u30fc\u30e0\u30a4\u30f3\uff0f\u30ba\u30fc\u30e0\u30a2\u30a6\u30c8 *=Autopsy\u30d5\u30a9\u30ec\u30f3\u30b8\u30c3\u30af\u30d6\u30e9\u30a6\u30b6 -AbstractVisualization.Default_Tooltip.text=\u30de\u30a6\u30b9\u3092\u30c9\u30e9\u30c3\u30b0\u3057\u3066\u30ba\u30fc\u30e0\u3059\u308b\u30bf\u30a4\u30e0\u9593\u9694\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u305d\u306e\u4ed6\u306e\u30a2\u30af\u30b7\u30e7\u30f3\u306f\u53f3\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044 +AbstractTimelineChart.defaultTooltip.text=\u30de\u30a6\u30b9\u3092\u30c9\u30e9\u30c3\u30b0\u3057\u3066\u30ba\u30fc\u30e0\u3059\u308b\u30bf\u30a4\u30e0\u9593\u9694\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\u305d\u306e\u4ed6\u306e\u30a2\u30af\u30b7\u30e7\u30f3\u306f\u53f3\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044 IntervalSelector.ClearSelectedIntervalAction.tooltTipText=\u9078\u629e\u3057\u305f\u9593\u9694\u3092\u30af\u30ea\u30a2\u3059\u308b IntervalSelector.ZoomAction.name=\u30ba\u30fc\u30e0 NoEventsDialog.titledPane.text=\u898b\u308c\u308b\u30a4\u30d9\u30f3\u30c8\u304c\u3042\u308a\u307e\u305b\u3093 @@ -40,6 +40,6 @@ Timeline.ui.ZoomRanges.fiveyears.text=5\u5e74 Timeline.ui.ZoomRanges.tenyears.text=10\u5e74 TimeLineChart.zoomHistoryActionGroup.name=\u30ba\u30fc\u30e0\u5c65\u6b74 TimeZonePanel.title=\u6642\u9593\u3092\u6b21\u3067\u8868\u793a\uff1a -VisualizationPanel.refresh=\u30ea\u30d5\u30ec\u30c3\u30b7\u30e5 -VisualizationPanel.tagsAddedOrDeleted=\u30bf\u30b0\u304c\u4f5c\u6210\u3055\u308c\u307e\u3057\u305f\u304a\u3088\u3073\u307e\u305f\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\u30d3\u30b8\u30e5\u30a2\u30e9\u30a4\u30bc\u30fc\u30b7\u30e7\u30f3\u304c\u6700\u65b0\u3067\u306f\u306a\u3044\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 -VisualizationUpdateTask.preparing=\u30ba\u30fc\u30e0\u304a\u3088\u3073\u30d5\u30a3\u30eb\u30bf\u30fc\u8a2d\u5b9a\u3092\u89e3\u6790\u4e2d +ViewFrame.refresh=\u30ea\u30d5\u30ec\u30c3\u30b7\u30e5 +ViewFrame.tagsAddedOrDeleted=\u30bf\u30b0\u304c\u4f5c\u6210\u3055\u308c\u307e\u3057\u305f\u304a\u3088\u3073\u307e\u305f\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\u30d3\u30b8\u30e5\u30a2\u30e9\u30a4\u30bc\u30fc\u30b7\u30e7\u30f3\u304c\u6700\u65b0\u3067\u306f\u306a\u3044\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 +ViewRefreshTask.preparing=\u30ba\u30fc\u30e0\u304a\u3088\u3073\u30d5\u30a3\u30eb\u30bf\u30fc\u8a2d\u5b9a\u3092\u89e3\u6790\u4e2d diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java deleted file mode 100644 index f61e2b5b28..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/TimeLineResultView.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.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.corecomponentinterfaces.DataContent; -import org.sleuthkit.autopsy.corecomponents.DataResultPanel; -import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.explorernodes.EventRootNode; - -/** - * 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 { - - /** - * the {@link DataResultPanel} that is the real view proxied by this class - */ - private final DataResultPanel dataResultPanel; - - private final TimeLineController controller; - - private final FilteredEventsModel filteredEvents; - - private Set selectedEventIDs = new HashSet<>(); - - public DataResultPanel getDataResultPanel() { - return dataResultPanel; - } - - public TimeLineResultView(TimeLineController controller, DataContent dataContent) { - - this.controller = controller; - this.filteredEvents = controller.getEventsModel(); - dataResultPanel = DataResultPanel.createInstanceUninitialized("", "", Node.EMPTY, 0, dataContent); - - //set up listeners on relevant properties - TimeLineController.getTimeZone().addListener((Observable observable) -> { - dataResultPanel.setPath(getSummaryString()); - }); - - controller.getSelectedEventIDs().addListener((Observable o) -> { - refresh(); - }); - refresh(); - } - - /** - * @return a String representation of all the Events displayed - */ - private String getSummaryString() { - if (controller.getSelectedTimeRange().get() != null) { - final DateTimeFormatter zonedFormatter = TimeLineController.getZonedFormatter(); - return NbBundle.getMessage(this.getClass(), "TimeLineResultView.startDateToEndDate.text", - controller.getSelectedTimeRange().get().getStart() - .withZone(TimeLineController.getJodaTimeZone()) - .toString(zonedFormatter), - 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 newSelectedEventIDs = new HashSet<>(controller.getSelectedEventIDs()); - if (selectedEventIDs.equals(newSelectedEventIDs) == false) { - selectedEventIDs = newSelectedEventIDs; - final EventRootNode root = new EventRootNode( - NbBundle.getMessage(this.getClass(), "Timeline.node.root"), selectedEventIDs, - filteredEvents); - - //this must be in edt or exception is thrown - SwingUtilities.invokeLater(() -> { - dataResultPanel.setPath(getSummaryString()); - dataResultPanel.setNode(root); - }); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml similarity index 89% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml rename to Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml index 9bb04129a1..777986e600 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml @@ -23,7 +23,7 @@ - @@ -81,7 +93,7 @@ -