From 1065d135367b12c5d0f20cf1393516e4b5a30062 Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 1 Sep 2015 17:39:10 -0400 Subject: [PATCH 01/15] single cluster WIP --- .../timeline/datamodel/EventBundle.java | 33 ++ ...{AggregateEvent.java => EventCluster.java} | 28 +- .../timeline/datamodel/EventStripe.java | 134 ++++++ .../datamodel/FilteredEventsModel.java | 4 +- .../autopsy/timeline/db/EventDB.java | 30 +- .../autopsy/timeline/db/EventsRepository.java | 6 +- .../timeline/ui/AbstractVisualization.java | 6 +- .../ui/detailview/DetailViewNode.java | 41 ++ .../ui/detailview/DetailViewPane.java | 56 +-- .../ui/detailview/DetailViewSettingsPane.fxml | 1 + .../timeline/ui/detailview/EventAxis.java | 18 +- ...teEventNode.java => EventClusterNode.java} | 94 ++-- .../ui/detailview/EventDetailChart.java | 295 +++++++----- .../ui/detailview/EventStripeNode.java | 425 ++++++++++++++++++ .../tree/EventDescriptionTreeItem.java | 9 +- .../ui/detailview/tree/EventTypeTreeItem.java | 9 +- .../timeline/ui/detailview/tree/NavPanel.java | 12 +- .../ui/detailview/tree/NavTreeItem.java | 7 +- .../timeline/ui/detailview/tree/RootItem.java | 7 +- CoreLibs/ivy.xml | 3 +- CoreLibs/nbproject/project.properties | 6 +- CoreLibs/nbproject/project.xml | 14 +- 22 files changed, 1004 insertions(+), 234 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java rename Core/src/org/sleuthkit/autopsy/timeline/datamodel/{AggregateEvent.java => EventCluster.java} (79%) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java rename Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/{AggregateEventNode.java => EventClusterNode.java} (87%) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java new file mode 100644 index 0000000000..0677f86c61 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -0,0 +1,33 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.datamodel; + +import java.util.Set; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; + +/** + * + */ +public interface EventBundle { + + String getDescription(); + + DescriptionLOD getDescriptionLOD(); + + long getEndMillis(); + + Set getEventIDs(); + + Set getEventIDsWithHashHits(); + + Set getEventIDsWithTags(); + + long getStartMillis(); + + EventType getType(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java similarity index 79% rename from Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java rename to Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 6ca47c55be..9a26affa7e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; +import com.google.common.collect.Range; import com.google.common.collect.Sets; import java.util.Collections; import java.util.Set; @@ -33,7 +34,7 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; * designated 'zoom level'. */ @Immutable -public class AggregateEvent { +public class EventCluster implements EventBundle { /** * the smallest time interval containing all the aggregated events @@ -72,7 +73,7 @@ public class AggregateEvent { */ private final Set hashHits; - public AggregateEvent(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLOD lod) { + public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLOD lod) { this.span = spanningInterval; this.type = type; @@ -90,6 +91,14 @@ public class AggregateEvent { return span; } + public long getStartMillis() { + return span.getStartMillis(); + } + + public long getEndMillis() { + return span.getEndMillis(); + } + public Set getEventIDs() { return Collections.unmodifiableSet(eventIDs); } @@ -110,7 +119,8 @@ public class AggregateEvent { return type; } - public DescriptionLOD getLOD() { + @Override + public DescriptionLOD getDescriptionLOD() { return lod; } @@ -123,7 +133,7 @@ public class AggregateEvent { * @return a new aggregate event that is the result of merging the given * events */ - public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) { + public static EventCluster merge(EventCluster aggEvent1, EventCluster ag2) { if (aggEvent1.getType() != ag2.getType()) { throw new IllegalArgumentException("aggregate events are not compatible they have different types"); @@ -136,6 +146,14 @@ public class AggregateEvent { Sets.SetView hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits()); Sets.SetView taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags()); - return new AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); + return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); + } + + Range getRange() { + if (getEndMillis() > getStartMillis()) { + return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); + } else { + return Range.singleton(getStartMillis()); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java new file mode 100644 index 0000000000..49dd736611 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -0,0 +1,134 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.datamodel; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Range; +import com.google.common.collect.RangeMap; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeMap; +import com.google.common.collect.TreeRangeSet; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.concurrent.Immutable; +import org.python.google.common.base.Objects; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; + +/** + * + */ +@Immutable +public final class EventStripe implements EventBundle { + + private final RangeSet spans = TreeRangeSet.create(); + private final RangeMap spanMap = TreeRangeMap.create(); + + /** + * the type of all the aggregted events + */ + private final EventType type; + + /** + * the common description of all the aggregated events + */ + private final String description; + + /** + * the description level of detail that the events were aggregated at. + */ + private final DescriptionLOD lod; + + /** + * the set of ids of the aggregated events + */ + private final Set eventIDs = new HashSet<>(); + + /** + * the ids of the subset of aggregated events that have at least one tag + * applied to them + */ + private final Set tagged = new HashSet<>(); + + /** + * the ids of the subset of aggregated events that have at least one hash + * set hit + */ + private final Set hashHits = new HashSet<>(); + + public EventStripe(EventCluster aggEvent) { + spans.add(aggEvent.getRange()); + spanMap.put(aggEvent.getRange(), aggEvent); + type = aggEvent.getType(); + description = aggEvent.getDescription(); + lod = aggEvent.getDescriptionLOD(); + eventIDs.addAll(aggEvent.getEventIDs()); + tagged.addAll(aggEvent.getEventIDsWithTags()); + hashHits.addAll(aggEvent.getEventIDsWithHashHits()); + } + + private EventStripe(EventStripe u, EventStripe v) { + spans.addAll(u.spans); + spans.addAll(v.spans); + spanMap.putAll(u.spanMap); + spanMap.putAll(v.spanMap); + type = u.getType(); + description = u.getDescription(); + lod = u.getDescriptionLOD(); + eventIDs.addAll(u.getEventIDs()); + eventIDs.addAll(v.getEventIDs()); + tagged.addAll(u.getEventIDsWithTags()); + tagged.addAll(v.getEventIDsWithTags()); + hashHits.addAll(u.getEventIDsWithHashHits()); + hashHits.addAll(v.getEventIDsWithHashHits()); + } + + public static EventStripe merge(EventStripe u, EventStripe v) { + Preconditions.checkNotNull(u); + Preconditions.checkNotNull(v); + Preconditions.checkArgument(Objects.equal(u.description, v.description)); + Preconditions.checkArgument(Objects.equal(u.lod, v.lod)); + Preconditions.checkArgument(Objects.equal(u.type, v.type)); + return new EventStripe(u, v); + } + + public String getDescription() { + return description; + } + + public EventType getType() { + return type; + } + + public DescriptionLOD getDescriptionLOD() { + return lod; + } + + public Set getEventIDs() { + return Collections.unmodifiableSet(eventIDs); + } + + public Set getEventIDsWithHashHits() { + return Collections.unmodifiableSet(hashHits); + } + + public Set getEventIDsWithTags() { + return Collections.unmodifiableSet(tagged); + } + + public long getStartMillis() { + return spans.span().lowerEndpoint(); + } + + public long getEndMillis() { + return spans.span().upperEndpoint(); + } + + public Iterable> getRanges() { + return spans.asRanges(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 3de6dacc61..80aa51d571 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -326,7 +326,7 @@ public final class FilteredEventsModel { * range and pass the requested filter, using the given aggregation * to control the grouping of events */ - public List getAggregatedEvents() { + public List getAggregatedEvents() { final Interval range; final RootFilter filter; final EventTypeZoomLevel zoom; @@ -347,7 +347,7 @@ public final class FilteredEventsModel { * range and pass the requested filter, using the given aggregation * to control the grouping of events */ - public List getAggregatedEvents(ZoomParams params) { + public List getAggregatedEvents(ZoomParams params) { return repo.getAggregatedEvents(params); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 3c3ba4069a..01837e30ca 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -54,7 +54,7 @@ import org.joda.time.Period; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -1031,7 +1031,7 @@ public class EventDB { } /** - * get a list of {@link AggregateEvent}s, clustered according to the given + * get a list of {@link EventCluster}s, clustered according to the given * zoom paramaters. * * @param params the zoom params that determine the zooming, filtering and @@ -1041,7 +1041,7 @@ public class EventDB { * the supplied filter, aggregated according to the given event type * and description zoom levels */ - List getAggregatedEvents(ZoomParams params) { + List getAggregatedEvents(ZoomParams params) { //unpack params Interval timeRange = params.getTimeRange(); RootFilter filter = params.getFilter(); @@ -1076,7 +1076,7 @@ public class EventDB { + "\n ORDER BY min(time)"; // NON-NLS // perform query and map results to AggregateEvent objects - List events = new ArrayList<>(); + List events = new ArrayList<>(); DBLock.lock(); @@ -1107,7 +1107,7 @@ public class EventDB { * * @throws SQLException */ - private AggregateEvent aggregateEventHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException { + private EventCluster aggregateEventHelper(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); @@ -1117,7 +1117,7 @@ public class EventDB { Set hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); Set tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); - return new AggregateEvent(interval, type, eventIDs, hashHits, tagged, + return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD); } @@ -1134,36 +1134,36 @@ public class EventDB { * * @return */ - static private List mergeAggregateEvents(Period timeUnitLength, List preMergedEvents) { + static private List mergeAggregateEvents(Period timeUnitLength, List preMergedEvents) { //effectively map from type to (map from description to events) - Map> typeMap = new HashMap<>(); + Map> typeMap = new HashMap<>(); - for (AggregateEvent aggregateEvent : preMergedEvents) { + for (EventCluster aggregateEvent : preMergedEvents) { typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create()) .put(aggregateEvent.getDescription(), aggregateEvent); } //result list to return - ArrayList aggEvents = new ArrayList<>(); + ArrayList aggEvents = new ArrayList<>(); //For each (type, description) key, merge agg events - for (SetMultimap descrMap : typeMap.values()) { + for (SetMultimap descrMap : typeMap.values()) { //for each description ... for (String descr : descrMap.keySet()) { //run through the sorted events, merging together adjacent events - Iterator iterator = descrMap.get(descr).stream() + Iterator iterator = descrMap.get(descr).stream() .sorted(Comparator.comparing(event -> event.getSpan().getStartMillis())) .iterator(); - AggregateEvent current = iterator.next(); + EventCluster current = iterator.next(); while (iterator.hasNext()) { - AggregateEvent next = iterator.next(); + EventCluster next = iterator.next(); Interval gap = current.getSpan().gap(next.getSpan()); //if they overlap or gap is less one quarter timeUnitLength //TODO: 1/4 factor is arbitrary. review! -jm if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) { //merge them - current = AggregateEvent.merge(current, next); + current = EventCluster.merge(current, next); } else { //done merging into current, set next as new current aggEvents.add(current); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 7a537626fd..1a9e61c2d1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -45,7 +45,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.ProgressWindow; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType; @@ -98,7 +98,7 @@ public class EventsRepository { private final LoadingCache idToEventCache; private final LoadingCache> eventCountsCache; - private final LoadingCache> aggregateEventsCache; + private final LoadingCache> aggregateEventsCache; private final ObservableMap datasourcesMap = FXCollections.observableHashMap(); private final ObservableMap hashSetMap = FXCollections.observableHashMap(); @@ -206,7 +206,7 @@ public class EventsRepository { } - synchronized public List getAggregatedEvents(ZoomParams params) { + synchronized public List getAggregatedEvents(ZoomParams params) { return aggregateEventsCache.getUnchecked(params); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java index 1f8a57de48..6d6fb8fb2e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java @@ -73,7 +73,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up * common history context menu items out of derived classes? -jm */ -public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView { +public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView { protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); @@ -173,8 +173,8 @@ public abstract class AbstractVisualization getYAxis(); /** - * update this visualization based on current state of zoom / - * filters. Primarily this invokes the background {@link Task} returned by + * update this visualization based on current state of zoom / filters. + * Primarily this invokes the background {@link Task} returned by * {@link #getUpdateTask()} which derived classes must implement. */ synchronized public void update() { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java new file mode 100644 index 0000000000..4378eed8b3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -0,0 +1,41 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.util.List; +import java.util.Set; +import javafx.scene.layout.Pane; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; + +/** + * + */ +public interface DetailViewNode { + + long getStartMillis(); + + long getEndMillis(); + + public void setDescriptionVisibility(DescriptionVisibility get); + + public Pane getSubNodePane(); + + public void setSpanWidths(List spanWidths); + + public void setDescriptionWidth(double max); + + public EventType getType(); + + public Set getEventIDs(); + + public void applySelectionEffect(boolean applied); + + public String getDescription(); + + public EventBundle getBundleDescriptor(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index e1a0ec1867..773ba836f7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -46,6 +46,7 @@ import javafx.scene.control.RadioButton; import javafx.scene.control.ScrollBar; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Slider; +import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; @@ -70,7 +71,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization; @@ -100,7 +101,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -109,20 +110,20 @@ public class DetailViewPane extends AbstractVisualization verticalAxis = new EventAxis(); + private final Axis verticalAxis = new EventAxis(); //private access to barchart data - private final Map> eventTypeToSeriesMap = new ConcurrentHashMap<>(); + private final Map> eventTypeToSeriesMap = new ConcurrentHashMap<>(); private final ScrollBar vertScrollBar = new ScrollBar(); private final Region region = new Region(); - private final ObservableList aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - public ObservableList getAggregatedEvents() { + public ObservableList getAggregatedEvents() { return aggregatedEvents; } @@ -149,7 +150,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change change) -> { while (change.next()) { change.getAddedSubList().forEach(aeNode -> { aeNode.applyHighlightEffect(true); @@ -212,8 +213,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (AggregateEventNode n : chart.getNodes((AggregateEventNode t) - -> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) { + for (EventClusterNode n : chart.getNodes((EventClusterNode t) + -> t.getEvent().getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -236,7 +237,7 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (AggregateEventNode n : chart.getNodes((AggregateEventNode t) + for (EventClusterNode n : chart.getNodes((EventClusterNode t) -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } @@ -250,7 +251,7 @@ public class DetailViewPane extends AbstractVisualization getYAxis() { + protected Axis getYAxis() { return verticalAxis; } @@ -279,15 +280,13 @@ public class DetailViewPane extends AbstractVisualization getSeries(final EventType et) { - XYChart.Series series = eventTypeToSeriesMap.get(et); - if (series == null) { - series = new XYChart.Series<>(); + private XYChart.Series getSeries(final EventType et) { + return eventTypeToSeriesMap.computeIfAbsent(et, (EventType t) -> { + XYChart.Series series = new XYChart.Series<>(); series.setName(et.getDisplayName()); - eventTypeToSeriesMap.put(et, series); dataSets.add(series); - } - return series; + return series; + }); } @Override @@ -328,13 +327,13 @@ public class DetailViewPane extends AbstractVisualization xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e); + final XYChart.Data xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e); Platform.runLater(() -> { if (isCancelled() == false) { @@ -359,12 +358,15 @@ public class DetailViewPane extends AbstractVisualization { + filteredEvents.refresh(); + }); + bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty()); + truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty()); + oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty()); truncateSliderLabel.disableProperty().bind(truncateAllBox.selectedProperty().not()); truncateSliderLabel.setText(NbBundle.getMessage(this.getClass(), "DetailViewPane.truncateSliderLabel.text")); final InvalidationListener sliderListener = o -> { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml index 08e3964711..051067ce67 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml @@ -74,6 +74,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index 486f9f290e..89b4de0ec7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -22,21 +22,21 @@ import java.util.Collections; import java.util.List; import javafx.scene.chart.Axis; import javafx.scene.chart.XYChart; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; /** * No-Op axis that doesn't do anything usefull but is necessary to pass * AggregateEvent as the second member of {@link XYChart.Data} objects */ -class EventAxis extends Axis { +class EventAxis extends Axis { @Override - public double getDisplayPosition(AggregateEvent value) { + public double getDisplayPosition(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public AggregateEvent getValueForDisplay(double displayPosition) { + public EventCluster getValueForDisplay(double displayPosition) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @@ -46,17 +46,17 @@ class EventAxis extends Axis { } @Override - public boolean isValueOnAxis(AggregateEvent value) { + public boolean isValueOnAxis(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public double toNumericValue(AggregateEvent value) { + public double toNumericValue(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public AggregateEvent toRealValue(double value) { + public EventCluster toRealValue(double value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @@ -66,7 +66,7 @@ class EventAxis extends Axis { } @Override - protected List calculateTickValues(double length, Object range) { + protected List calculateTickValues(double length, Object range) { return Collections.emptyList(); } @@ -76,7 +76,7 @@ class EventAxis extends Axis { } @Override - protected String getTickMarkLabel(AggregateEvent value) { + protected String getTickMarkLabel(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java similarity index 87% rename from Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java rename to Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index f28ec9269d..e5e38d8375 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -64,9 +64,11 @@ 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.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; @@ -76,11 +78,11 @@ import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** - * Represents an {@link AggregateEvent} in a {@link EventDetailChart}. + * Represents an {@link EventCluster} in a {@link EventDetailChart}. */ -public class AggregateEventNode extends StackPane { +public class EventClusterNode extends StackPane implements DetailViewNode { - private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.class.getName()); + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS @@ -97,9 +99,9 @@ public class AggregateEventNode extends StackPane { /** * The event this AggregateEventNode represents visually */ - private AggregateEvent aggEvent; + private EventCluster aggEvent; - private final AggregateEventNode parentEventNode; + private final EventClusterNode parentEventNode; /** * the region that represents the time span of this node's event @@ -169,9 +171,9 @@ public class AggregateEventNode extends StackPane { private final ImageView hashIV = new ImageView(HASH_PIN); private final ImageView tagIV = new ImageView(TAG); - public AggregateEventNode(final AggregateEvent aggEvent, AggregateEventNode parentEventNode, EventDetailChart chart) { + public EventClusterNode(final EventCluster aggEvent, EventClusterNode parentEventNode, EventDetailChart chart) { this.aggEvent = aggEvent; - descLOD.set(aggEvent.getLOD()); + descLOD.set(aggEvent.getDescriptionLOD()); this.parentEventNode = parentEventNode; this.chart = chart; sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); @@ -230,7 +232,7 @@ public class AggregateEventNode extends StackPane { //setup backgrounds final Color evtColor = aggEvent.getType().getColor(); spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setBackground(spanFill); setCursor(Cursor.HAND); spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS spanRegion.setBackground(spanFill); @@ -258,7 +260,7 @@ public class AggregateEventNode extends StackPane { setOnMouseClicked(new EventMouseHandler()); plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getLOD())); + minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getDescriptionLOD())); plusButton.setOnMouseClicked(e -> { final DescriptionLOD next = descLOD.get().next(); @@ -296,7 +298,7 @@ public class AggregateEventNode extends StackPane { Map tagCounts = new HashMap<>(); if (!aggEvent.getEventIDsWithTags().isEmpty()) { - tagCounts.putAll( eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags())); + tagCounts.putAll(eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags())); } @@ -315,15 +317,16 @@ public class AggregateEventNode extends StackPane { + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) ); - Tooltip.install(AggregateEventNode.this, tooltip); + Tooltip.install(EventClusterNode.this, tooltip); } } + @Override public Pane getSubNodePane() { return subNodePane; } - synchronized public AggregateEvent getEvent() { + synchronized public EventCluster getEvent() { return aggEvent; } @@ -333,16 +336,23 @@ public class AggregateEventNode extends StackPane { * * @param w */ - public void setSpanWidth(double w) { + private void setSpanWidth(double w) { spanRegion.setPrefWidth(w); spanRegion.setMaxWidth(w); spanRegion.setMinWidth(Math.max(2, w)); } + @Override + public void setSpanWidths(List spanWidths) { + setSpanWidth(spanWidths.get(0)); + + } + /** * * @param w the maximum width the description label should have */ + @Override public void setDescriptionWidth(double w) { descrLabel.setMaxWidth(w); } @@ -351,7 +361,8 @@ public class AggregateEventNode extends StackPane { * @param descrVis the level of description that should be displayed */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) { + @Override + public final synchronized void setDescriptionVisibility(DescriptionVisibility descrVis) { this.descrVis = descrVis; final int size = aggEvent.getEventIDs().size(); @@ -381,7 +392,8 @@ public class AggregateEventNode extends StackPane { * * @param applied true to apply the selection 'effect', false to remove it */ - void applySelectionEffect(final boolean applied) { + @Override + public void applySelectionEffect(boolean applied) { Platform.runLater(() -> { if (applied) { setBorder(selectionBorder); @@ -391,6 +403,11 @@ public class AggregateEventNode extends StackPane { }); } + @Override + public String getDescription() { + return aggEvent.getDescription(); + } + /** * apply the 'effect' to visually indicate highlighted nodes * @@ -441,7 +458,7 @@ public class AggregateEventNode extends StackPane { */ synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { getSubNodePane().getChildren().clear(); - if (newDescriptionLOD == aggEvent.getLOD()) { + if (newDescriptionLOD == aggEvent.getDescriptionLOD()) { chart.setRequiresLayout(true); chart.requestChartLayout(); } else { @@ -454,19 +471,19 @@ public class AggregateEventNode extends StackPane { final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000); //make a task to load the subnodes - LoggedTask> loggedTask = new LoggedTask>( + LoggedTask> loggedTask = new LoggedTask>( NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { @Override - protected List call() throws Exception { + protected List call() throws Exception { //query for the sub-clusters - List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, + List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, eventsModel.eventTypeZoomProperty().get(), combinedFilter, newDescriptionLOD)); //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position return aggregatedEvents.stream().map(aggEvent -> { - AggregateEventNode subNode = new AggregateEventNode(aggEvent, AggregateEventNode.this, chart); + EventClusterNode subNode = new EventClusterNode(aggEvent, EventClusterNode.this, chart); subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation()); return subNode; }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters @@ -494,7 +511,7 @@ public class AggregateEventNode extends StackPane { } /** - * event handler used for mouse events on {@link AggregateEventNode}s + * event handler used for mouse events on {@link EventClusterNode}s */ private class EventMouseHandler implements EventHandler { @@ -503,11 +520,11 @@ public class AggregateEventNode extends StackPane { if (t.getButton() == MouseButton.PRIMARY) { t.consume(); if (t.isShiftDown()) { - if (chart.selectedNodes.contains(AggregateEventNode.this) == false) { - chart.selectedNodes.add(AggregateEventNode.this); + if (chart.selectedNodes.contains(EventClusterNode.this) == false) { + chart.selectedNodes.add(EventClusterNode.this); } } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(AggregateEventNode.this); + chart.selectedNodes.removeAll(EventClusterNode.this); } else if (t.getClickCount() > 1) { final DescriptionLOD next = descLOD.get().next(); if (next != null) { @@ -515,9 +532,34 @@ public class AggregateEventNode extends StackPane { descLOD.set(next); } } else { - chart.selectedNodes.setAll(AggregateEventNode.this); + chart.selectedNodes.setAll(EventClusterNode.this); } } } } + + @Override + public long getStartMillis() { + return getEvent().getStartMillis(); + } + + @Override + public long getEndMillis() { + return getEvent().getStartMillis(); + } + + @Override + public EventType getType() { + return getEvent().getType(); + } + + @Override + public Set getEventIDs() { + return getEvent().getEventIDs(); + } + + @Override + public EventBundle getBundleDescriptor() { + return aggEvent; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 9d8ba43e70..b5266f8207 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,15 +18,17 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Collections2; +import com.google.common.collect.Range; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.TreeMap; +import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; import javafx.animation.KeyFrame; @@ -34,6 +36,7 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.SimpleBooleanProperty; @@ -58,10 +61,12 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; import javafx.scene.shape.Line; import javafx.scene.shape.StrokeLineCap; import javafx.util.Duration; import javax.annotation.concurrent.GuardedBy; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionGroup; import org.controlsfx.control.action.ActionUtils; @@ -71,7 +76,8 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Forward; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; @@ -90,7 +96,7 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; * * //TODO: refactor the projected lines to a separate class. -jm */ -public final class EventDetailChart extends XYChart implements TimeLineChart { +public final class EventDetailChart extends XYChart implements TimeLineChart { private static final int PROJECTED_LINE_Y_OFFSET = 5; @@ -151,17 +157,9 @@ public final class EventDetailChart extends XYChart im /** * map from event to node */ - private final Map nodeMap = new TreeMap<>(( - AggregateEvent o1, - AggregateEvent o2) -> { - int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis()); - if (comp != 0) { - return comp; - } else { - return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2); - } - }); - + private final Map nodeMap = new HashMap<>(); + private final Map, EventStripe> stripeDescMap = new HashMap<>(); + private final Map stripeNodeMap = new HashMap<>(); /** * true == enforce that no two events can share the same 'row', leading to * sparser but possibly clearer layout. false == put unrelated events in the @@ -169,7 +167,7 @@ public final class EventDetailChart extends XYChart im */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap projectionMap = FXCollections.observableHashMap(); + private final ObservableMap projectionMap = FXCollections.observableHashMap(); /** * flag indicating whether this chart actually needs a layout pass @@ -177,16 +175,16 @@ public final class EventDetailChart extends XYChart im @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedNodes; + final ObservableList selectedNodes; /** * list of series of data added to this chart TODO: replace this with a map * from name to series? -jm */ - private final ObservableList> seriesList - = FXCollections.>observableArrayList(); + private final ObservableList> seriesList + = FXCollections.>observableArrayList(); - private final ObservableList> sortedSeriesList = seriesList + private final ObservableList> sortedSeriesList = seriesList .sorted((s1, s2) -> { final List collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); @@ -205,8 +203,9 @@ public final class EventDetailChart extends XYChart im * via slider if truncateAll is true */ private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); @@ -225,6 +224,7 @@ public final class EventDetailChart extends XYChart im widthProperty().addListener(layoutInvalidationListener); heightProperty().addListener(layoutInvalidationListener); // boundsInLocalProperty().addListener(layoutInvalidationListener); + bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); @@ -285,7 +285,7 @@ public final class EventDetailChart extends XYChart im setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change change) -> { + projectionMap.addListener((MapChangeListener.Change change) -> { final Line valueRemoved = change.getValueRemoved(); if (valueRemoved != null) { getChartChildren().removeAll(valueRemoved); @@ -298,16 +298,16 @@ public final class EventDetailChart extends XYChart im this.selectedNodes = selectedNodes; this.selectedNodes.addListener(( - ListChangeListener.Change c) -> { + ListChangeListener.Change c) -> { while (c.next()) { - c.getRemoved().forEach((AggregateEventNode t) -> { + c.getRemoved().forEach((DetailViewNode t) -> { projectionMap.remove(t); }); - c.getAddedSubList().forEach((AggregateEventNode t) -> { - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + c.getAddedSubList().forEach((DetailViewNode t) -> { + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET ); - line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5)); + line.setStroke(t.getType().getColor().deriveColor(0, 1, 1, .5)); line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); line.setStrokeLineCap(StrokeLineCap.ROUND); projectionMap.put(t, line); @@ -316,7 +316,7 @@ public final class EventDetailChart extends XYChart im } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream()) + .flatMap((DetailViewNode aggNode) -> aggNode.getEventIDs().stream()) .collect(Collectors.toList())); }); @@ -329,7 +329,7 @@ public final class EventDetailChart extends XYChart im intervalSelector = null; } - public synchronized SimpleBooleanProperty getBandByType() { + public synchronized SimpleBooleanProperty bandByTypeProperty() { return bandByType; } @@ -389,11 +389,11 @@ public final class EventDetailChart extends XYChart im getChartChildren().add(getIntervalSelector()); } - public synchronized SimpleBooleanProperty getOneEventPerRow() { + public synchronized SimpleBooleanProperty oneEventPerRowProperty() { return oneEventPerRow; } - public synchronized SimpleBooleanProperty getTruncateAll() { + public synchronized SimpleBooleanProperty truncateAllProperty() { return truncateAll; } @@ -407,30 +407,49 @@ public final class EventDetailChart extends XYChart im } @Override - protected synchronized void dataItemAdded(Series series, int i, Data data) { - final AggregateEvent aggEvent = data.getYValue(); - AggregateEventNode eventNode = nodeMap.get(aggEvent); - if (eventNode == null) { - eventNode = new AggregateEventNode(aggEvent, null, this); - - eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); - data.setNode(eventNode); - nodeMap.put(aggEvent, eventNode); - nodeGroup.getChildren().add(eventNode); - requiresLayout = true; + protected synchronized void dataItemAdded(Series series, int i, Data data) { + final EventCluster aggEvent = data.getYValue(); + if (alternateLayout.get()) { + EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getType(), aggEvent.getDescription()), + new EventStripe(aggEvent), + (EventStripe u, EventStripe v) -> { + EventStripeNode remove = stripeNodeMap.remove(u); + nodeGroup.getChildren().remove(remove); + remove = stripeNodeMap.remove(v); + nodeGroup.getChildren().remove(remove); + return EventStripe.merge(u, v); + } + ); + EventStripeNode clusterNode = new EventStripeNode(eventCluster,null, EventDetailChart.this); + stripeNodeMap.put(eventCluster, clusterNode); + nodeGroup.getChildren().add(clusterNode); + } else { + nodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> { + EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this); + eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); + nodeMap.put(aggEvent, eventNode); + nodeGroup.getChildren().add(eventNode); + return eventNode; + }); } } @Override - protected synchronized void dataItemChanged(Data data) { + protected synchronized void dataItemChanged(Data data) { //TODO: can we use this to help with local detail level adjustment -jm throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates. } @Override - protected synchronized void dataItemRemoved(Data data, Series series) { - nodeMap.remove(data.getYValue()); - nodeGroup.getChildren().remove(data.getNode()); + protected synchronized void dataItemRemoved(Data data, Series series) { + EventCluster aggEvent = data.getYValue(); + Node removedNode = nodeMap.remove(aggEvent); + nodeGroup.getChildren().remove(removedNode); + + EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getType(), aggEvent.getDescription())); + removedNode = stripeNodeMap.remove(removedCluster); + nodeGroup.getChildren().remove(removedNode); + data.setNode(null); } @@ -465,17 +484,36 @@ public final class EventDetailChart extends XYChart im maxY.set(0.0); if (bandByType.get() == false) { + if (alternateLayout.get() == true) { + List nodes = new ArrayList<>(stripeNodeMap.values()); + Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + layoutNodes(nodes, minY, 0); + } else { + List nodes = new ArrayList<>(nodeMap.values()); + Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + layoutNodes(nodes, minY, 0); + } - ObservableList nodes = FXCollections.observableArrayList(nodeMap.values()); - FXCollections.sort(nodes, new StartTimeComparator()); - layoutNodes(nodes, minY, 0); -// layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0); } else { - for (Series s : sortedSeriesList) { - ObservableList nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode)); - - FXCollections.sort(nodes, new StartTimeComparator()); - layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0); + for (Series s : sortedSeriesList) { + if (alternateLayout.get() == true) { + List nodes = s.getData().stream() + .map(Data::getYValue) + .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getType(), cluster.getDescription()))) + .distinct() + .sorted(Comparator.comparing(EventStripe::getStartMillis)) + .map(stripeNodeMap::get) + .collect(Collectors.toList()); + layoutNodes(nodes, minY, 0); + } else { + List nodes = s.getData().stream() + .map(Data::getYValue) + .map(nodeMap::get) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(EventClusterNode::getStartMillis)) + .collect(Collectors.toList()); + layoutNodes(nodes, minY, 0); + } minY = maxY.get(); } } @@ -486,7 +524,7 @@ public final class EventDetailChart extends XYChart im } @Override - protected synchronized void seriesAdded(Series series, int i) { + protected synchronized void seriesAdded(Series series, int i) { for (int j = 0; j < series.getData().size(); j++) { dataItemAdded(series, j, series.getData().get(j)); } @@ -495,7 +533,7 @@ public final class EventDetailChart extends XYChart im } @Override - protected synchronized void seriesRemoved(Series series) { + protected synchronized void seriesRemoved(Series series) { for (int j = 0; j < series.getData().size(); j++) { dataItemRemoved(series.getData().get(j), series); } @@ -503,44 +541,48 @@ public final class EventDetailChart extends XYChart im requiresLayout = true; } - synchronized SimpleObjectProperty getDescrVisibility() { + synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() { return descrVisibility; } - synchronized ReadOnlyDoubleProperty getMaxVScroll() { + synchronized ReadOnlyDoubleProperty + getMaxVScroll() { return maxY.getReadOnlyProperty(); } - Iterable getNodes(Predicate p) { - List nodes = new ArrayList<>(); + Iterable getNodes(Predicate p) { + List nodes = new ArrayList<>(); - for (AggregateEventNode node : nodeMap.values()) { + for (EventClusterNode node : nodeMap.values()) { checkNode(node, p, nodes); } return nodes; } - Iterable getAllNodes() { + Iterable getAllNodes() { return getNodes(x -> true); } - synchronized SimpleDoubleProperty getTruncateWidth() { + synchronized SimpleDoubleProperty + getTruncateWidth() { return truncateWidth; } - synchronized void setVScroll(double d) { + synchronized void + setVScroll(double d + ) { final double h = maxY.get() - (getHeight() * .9); nodeGroup.setTranslateY(-d * h); } - private static void checkNode(AggregateEventNode node, Predicate p, List nodes) { + private static void checkNode(EventClusterNode node, Predicate p, List nodes) { if (node != null) { if (p.test(node)) { nodes.add(node); } for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { - checkNode((AggregateEventNode) n, p, nodes); + checkNode((EventClusterNode) n, p, nodes); } } } @@ -557,41 +599,77 @@ public final class EventDetailChart extends XYChart im * @param nodes * @param minY */ - private synchronized double layoutNodes(final List nodes, final double minY, final double xOffset) { + private synchronized double layoutNodes(final Collection nodes, final double minY, final double xOffset) { //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (Node n : nodes) { - final AggregateEventNode tlNode = (AggregateEventNode) n; - tlNode.setDescriptionVisibility(descrVisibility.get()); + for (D n : nodes) { + n.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis())); - AggregateEvent ie = tlNode.getEvent(); - final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis())); //position of start and end according to range of axis - double xPos = rawDisplayPosition - xOffset; + double startX = rawDisplayPosition - xOffset; double layoutNodesResultHeight = 0; - if (tlNode.getSubNodePane().getChildren().isEmpty() == false) { - FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator()); - layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition); - } - double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset; - double span = xPos2 - xPos; - //size timespan border - tlNode.setSpanWidth(span); - if (truncateAll.get()) { //if truncate option is selected limit width of description label - tlNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); - } else { //else set it unbounded - tlNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + double span = 0; + if (n instanceof EventClusterNode) { + if (n.getSubNodePane().getChildren().isEmpty() == false) { + List children = n.getSubNodePane().getChildren().stream() + .map(EventClusterNode.class::cast) + .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) + .collect(Collectors.toList()); + layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); + } + double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset; + span = endX - startX; + + //size timespan border + n.setSpanWidths(Arrays.asList(span)); + } else { + if (n.getSubNodePane().getChildren().isEmpty() == false) { + List children = n.getSubNodePane().getChildren().stream() + .map(EventStripeNode.class::cast) + .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) + .collect(Collectors.toList()); + layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); + } + EventStripeNode cn = (EventStripeNode) n; + List spanWidths = new ArrayList<>(); + double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; + double x2; + Iterator> ranges = cn.getCluster().getRanges().iterator(); + Range range = ranges.next(); + do { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + double clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + if (ranges.hasNext()) { + range = ranges.next(); + x = getXAxis().getDisplayPosition(new DateTime(range.lowerEndpoint())); + double gapSpan = x - x2; + span += gapSpan; + spanWidths.add(gapSpan); + } + + } while (ranges.hasNext()); + + cn.setSpanWidths(spanWidths); } - tlNode.autosize(); //compute size of tlNode based on constraints and event data + if (truncateAll.get()) { //if truncate option is selected limit width of description label + n.setDescriptionWidth(Math.max(span, truncateWidth.get())); + } else { //else set it unbounded + n.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + } + + n.autosize(); //compute size of tlNode based on constraints and event data //get position of right edge of node ( influenced by description label) - double xRight = xPos + tlNode.getWidth(); + double xRight = startX + n.getWidth(); //get the height of the node - final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + final double h = layoutNodesResultHeight == 0 ? n.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; //initial test position double yPos = minY; @@ -612,7 +690,7 @@ public final class EventDetailChart extends XYChart im //check each pixel from bottom to top. for (double y = yPos2; y >= yPos; y--) { final Double maxX = maxXatY.get((int) y); - if (maxX != null && maxX >= xPos - 4) { + if (maxX != null && maxX >= startX - 4) { //if that pixel is already used //jump top to this y value and repeat until free slot is found. overlapping = true; @@ -630,24 +708,24 @@ public final class EventDetailChart extends XYChart im localMax = Math.max(yPos2, localMax); Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(tlNode.layoutXProperty(), xPos), - new KeyValue(tlNode.layoutYProperty(), yPos))); + new KeyValue(n.layoutXProperty(), startX), + new KeyValue(n.layoutYProperty(), yPos))); tm.play(); -// tlNode.relocate(xPos, yPos); } maxY.set(Math.max(maxY.get(), localMax)); return localMax - minY; } + private static final int DEFAULT_ROW_HEIGHT = 24; private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final AggregateEventNode aggNode = entry.getKey(); + for (final Map.Entry entry : projectionMap.entrySet()) { + final DetailViewNode aggNode = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone()))); - line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone()))); + line.setStartX(getParentXForValue(new DateTime(aggNode.getStartMillis(), TimeLineController.getJodaTimeZone()))); + line.setEndX(getParentXForValue(new DateTime(aggNode.getEndMillis(), TimeLineController.getJodaTimeZone()))); line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); } @@ -678,22 +756,17 @@ public final class EventDetailChart extends XYChart im return chartContextMenu; } - private static class StartTimeComparator implements Comparator { + Property alternateLayoutProperty() { + return alternateLayout; + } + + private static class StartTimeComparator implements Comparator { @Override - public int compare(Node n1, Node n2) { - - if (n1 == null) { - return 1; - } else if (n2 == null) { - return -1; - } else { - - return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(), - (((AggregateEventNode) n2).getEvent().getSpan().getStartMillis())); - } + public int compare(T n1, T n2) { + return Long.compare(n1.getStartMillis(), n2.getStartMillis() + ); } - } private class DetailIntervalSelector extends IntervalSelector { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java new file mode 100644 index 0000000000..aab34c242a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -0,0 +1,425 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import com.google.common.collect.Range; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +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; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ColorUtilities; +import org.sleuthkit.autopsy.coreutils.LoggedTask; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.filters.RootFilter; +import org.sleuthkit.autopsy.timeline.filters.TextFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * + */ +public class EventStripeNode extends StackPane implements DetailViewNode { + + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); + + private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); + private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS + private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS + private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS + + private final Pane subNodePane = new Pane(); + private final EventStripe cluster; + private final EventStripeNode parentNode; + private final EventDetailChart chart; + private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + private final SleuthkitCase sleuthkitCase; + private final FilteredEventsModel eventsModel; + /** + * The label used to display this node's event's description + */ + private final Label descrLabel = new Label(); + + /** + * The label used to display this node's event count + */ + private final Label countLabel = new Label(); + + private final ImageView hashIV = new ImageView(HASH_PIN); + private final ImageView tagIV = new ImageView(TAG); + private final Button plusButton = new Button(null, new ImageView(PLUS)) { + { + setMinSize(16, 16); + setMaxSize(16, 16); + setPrefSize(16, 16); + } + }; + private final Button minusButton = new Button(null, new ImageView(MINUS)) { + { + setMinSize(16, 16); + setMaxSize(16, 16); + setPrefSize(16, 16); + } + }; + private DescriptionVisibility descrVis; + private final HBox spanRegion = new HBox(); + /** + * The IamgeView used to show the icon for this node's event's type + */ + private final ImageView eventTypeImageView = new ImageView(); + private Background spanFill; + private static final CornerRadii CORNER_RADII = new CornerRadii(3); + + EventStripeNode(EventStripe cluster, EventStripeNode parentNode, EventDetailChart chart) { + this.chart = chart; + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chart.getController().getEventsModel(); + + this.parentNode = parentNode; + this.cluster = cluster; + descLOD.set(cluster.getDescriptionLOD()); + + final Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + final HBox hBox = new HBox(descrLabel, countLabel, spacer, hashIV, tagIV, minusButton, plusButton); + if (cluster.getEventIDsWithHashHits().isEmpty()) { + hashIV.setManaged(false); + hashIV.setVisible(false); + } + if (cluster.getEventIDsWithTags().isEmpty()) { + tagIV.setManaged(false); + tagIV.setVisible(false); + } + hBox.setPrefWidth(USE_COMPUTED_SIZE); + hBox.setMinWidth(USE_PREF_SIZE); + hBox.setPadding(new Insets(2, 5, 2, 5)); + hBox.setAlignment(Pos.CENTER_LEFT); + + minusButton.setVisible(false); + plusButton.setVisible(false); + minusButton.setManaged(false); + plusButton.setManaged(false); + final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); + BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); + borderPane.setPrefWidth(USE_COMPUTED_SIZE); + final Color evtColor = cluster.getType().getColor(); + + spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)); + for (Range r : cluster.getRanges()) { + Region region = new Region(); + region.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS + region.setBackground(spanFill); + spanRegion.getChildren().addAll(region, new Region()); + } + spanRegion.getChildren().remove(spanRegion.getChildren().size() - 1); + + getChildren().addAll(spanRegion, borderPane); + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setAlignment(Pos.TOP_LEFT); + setMinHeight(24); + minWidthProperty().bind(spanRegion.widthProperty()); + setPrefHeight(USE_COMPUTED_SIZE); + setMaxHeight(USE_PREF_SIZE); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMinHeight(USE_PREF_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + subNodePane.setPickOnBounds(false); + + //setup description label + eventTypeImageView.setImage(cluster.getType().getFXImage()); + descrLabel.setGraphic(eventTypeImageView); + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + + descrLabel.setMouseTransparent(true); + setDescriptionVisibility(chart.getDescrVisibility().get()); + setOnMouseClicked(new EventMouseHandler()); + + //set up mouse hover effect and tooltip + setOnMouseEntered((MouseEvent e) -> { + //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart +// installTooltip(); + spanRegion.setEffect(new DropShadow(10, evtColor)); + minusButton.setVisible(true); + plusButton.setVisible(true); + minusButton.setManaged(true); + plusButton.setManaged(true); + toFront(); + }); + + setOnMouseExited((MouseEvent e) -> { + spanRegion.setEffect(null); + minusButton.setVisible(false); + plusButton.setVisible(false); + minusButton.setManaged(false); + plusButton.setManaged(false); + }); + + plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + minusButton.disableProperty().bind(descLOD.isEqualTo(cluster.getDescriptionLOD())); + + plusButton.setOnMouseClicked(e -> { + final DescriptionLOD next = descLOD.get().next(); + if (next != null) { + loadSubClusters(next); + descLOD.set(next); + } + }); + minusButton.setOnMouseClicked(e -> { + final DescriptionLOD previous = descLOD.get().previous(); + if (previous != null) { + loadSubClusters(previous); + descLOD.set(previous); + } + }); + } + + @Override + public long getStartMillis() { + return cluster.getStartMillis(); + } + + @Override + public void setSpanWidths(List spanWidths) { + for (int i = 0; i < spanWidths.size(); i++) { + Region get = (Region) spanRegion.getChildren().get(i); + Double w = spanWidths.get(i); + get.setPrefWidth(w); + get.setMaxWidth(w); + get.setMinWidth(Math.max(2, w)); + } + } + + public void setDescriptionVisibility(DescriptionVisibility descrVis) { + this.descrVis = descrVis; + final int size = cluster.getEventIDs().size(); + + switch (descrVis) { + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + default: + case SHOWN: + String description = cluster.getDescription(); + description = parentNode != null + ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) + : description; + descrLabel.setText(description); + countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS + break; + } + } + + EventStripe getCluster() { + return cluster; + } + + @Override + public void setDescriptionWidth(double w) { + descrLabel.setMaxWidth(w); + } + + @Override + public long getEndMillis() { + return cluster.getEndMillis(); + } + + @Override + public Pane getSubNodePane() { + return subNodePane; + } + + /** + * event handler used for mouse events on {@link AggregateEventNode}s + */ + private class EventMouseHandler implements EventHandler { + + @Override + public void handle(MouseEvent t) { + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventStripeNode.this) == false) { + chart.selectedNodes.add(EventStripeNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(EventStripeNode.this); + } else if (t.getClickCount() > 1) { + final DescriptionLOD next = descLOD.get().next(); + if (next != null) { + loadSubClusters(next); + descLOD.set(next); + } + } else { + chart.selectedNodes.setAll(EventStripeNode.this); + } + } + } + } + + @Override + public EventType getType() { + return cluster.getType(); + } + + @Override + public Set getEventIDs() { + return cluster.getEventIDs(); + } + private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + @Override + public void applySelectionEffect(boolean applied) { + Platform.runLater(() -> { + if (applied) { + setBorder(selectionBorder); + } else { + setBorder(null); + } + }); + } + + @Override + public String getDescription() { + return cluster.getDescription(); + } + + @Override + public EventBundle getBundleDescriptor() { + return getCluster(); + } + + /** + * loads sub-clusters at the given Description LOD + * + * @param newDescriptionLOD + */ + synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { + getSubNodePane().getChildren().clear(); + if (newDescriptionLOD == cluster.getDescriptionLOD()) { + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } else { + RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); + //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters + combinedFilter.getSubFilters().addAll(new TextFilter(cluster.getDescription()), + new TypeFilter(cluster.getType())); + + //make a new end inclusive span (to 'filter' with) + final Interval span = new Interval(cluster.getStartMillis(), cluster.getEndMillis() + 1000); + + //make a task to load the subnodes + LoggedTask> loggedTask = new LoggedTask>( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + + @Override + protected List call() throws Exception { + //query for the sub-clusters + List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, + eventsModel.eventTypeZoomProperty().get(), + combinedFilter, + newDescriptionLOD)); + //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position + HashMap stripeDescMap = new HashMap<>(); + for (EventCluster subCluster : aggregatedEvents) { + stripeDescMap.merge(subCluster.getDescription(), + new EventStripe(subCluster), + (EventStripe u, EventStripe v) -> { + return EventStripe.merge(u, v); + } + ); + } + + return stripeDescMap.values().stream().map(subStripe -> { + EventStripeNode subNode = new EventStripeNode(subStripe, EventStripeNode.this, chart); + subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(subStripe.getStartMillis())) - getLayoutXCompensation()); + return subNode; + }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters + } + + @Override + protected void succeeded() { + try { + chart.setCursor(Cursor.WAIT); + //assign subNodes and request chart layout + getSubNodePane().getChildren().setAll(get()); + setDescriptionVisibility(descrVis); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + chart.setCursor(null); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + } + }; + + //start task + chart.getController().monitorTask(loggedTask); + } + } + + double getLayoutXCompensation() { + return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 04ec644a25..f2f5a7b1da 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -20,14 +20,15 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; /** * */ class EventDescriptionTreeItem extends NavTreeItem { - public EventDescriptionTreeItem(AggregateEvent g) { + public EventDescriptionTreeItem(EventCluster g) { setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size())); } @@ -37,7 +38,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public void insert(AggregateEvent g) { + public void insert(EventCluster g) { NavTreeNode value = getValue(); if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { throw new IllegalArgumentException(); @@ -52,7 +53,7 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { + public TreeItem findTreeItemForEvent(EventBundle t) { if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { return this; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index ac16b31949..d9508cfd4d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -24,7 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; class EventTypeTreeItem extends NavTreeItem { @@ -35,7 +36,7 @@ class EventTypeTreeItem extends NavTreeItem { private final Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(AggregateEvent g) { + EventTypeTreeItem(EventCluster g) { setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0)); } @@ -52,7 +53,7 @@ class EventTypeTreeItem extends NavTreeItem { * @param tree True if it is part of a tree (versus a list) */ @Override - public void insert(AggregateEvent g) { + public void insert(EventCluster g) { EventDescriptionTreeItem treeItem = childMap.get(g.getDescription()); if (treeItem == null) { @@ -77,7 +78,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { + public TreeItem findTreeItemForEvent(EventBundle t) { if (t.getType().getBaseType() == getValue().getType().getBaseType()) { for (TreeItem child : getChildren()) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 361c8b3250..8718b6f90c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -41,9 +41,9 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** @@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView { }); detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent())); + detailViewPane.getSelectedNodes().forEach((DetailViewNode t) -> { + eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getBundleDescriptor())); }); }); @@ -100,10 +100,10 @@ public class NavPanel extends BorderPane implements TimeLineView { private void setRoot() { RootItem root = new RootItem(); - final ObservableList aggregatedEvents = detailViewPane.getAggregatedEvents(); + final ObservableList aggregatedEvents = detailViewPane.getAggregatedEvents(); synchronized (aggregatedEvents) { - for (AggregateEvent agg : aggregatedEvents) { + for (EventCluster agg : aggregatedEvents) { root.insert(agg); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index 3f96c57a71..e87b10cc17 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -20,7 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; /** * A node in the nav tree. Manages inserts and resorts. Has parents and @@ -30,12 +31,12 @@ import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; */ abstract class NavTreeItem extends TreeItem { - abstract void insert(AggregateEvent g); + abstract void insert(EventCluster g); abstract int getCount(); abstract void resort(Comparator> comp); - abstract TreeItem findTreeItemForEvent(AggregateEvent t); + abstract TreeItem findTreeItemForEvent(EventBundle t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 95ec59287b..38a9547cba 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -24,8 +24,9 @@ import java.util.Map; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; /** * @@ -56,7 +57,7 @@ class RootItem extends NavTreeItem { * @param g Group to add */ @Override - public void insert(AggregateEvent g) { + public void insert(EventCluster g) { EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType()); if (treeItem == null) { @@ -85,7 +86,7 @@ class RootItem extends NavTreeItem { } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { + public TreeItem findTreeItemForEvent(EventBundle t) { for (TreeItem child : getChildren()) { final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); if (findTreeItemForEvent != null) { diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml index 5a6cd9dd17..7e247684b4 100644 --- a/CoreLibs/ivy.xml +++ b/CoreLibs/ivy.xml @@ -1,5 +1,3 @@ - - @@ -18,6 +16,7 @@ + diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index bb6e8ee7c5..fac6eca1cc 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -20,7 +20,7 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar file.reference.gson-1.4.jar=release/modules/ext/gson-1.4.jar file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar -file.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2.jar +file.reference.guava-18.0.jar=C:\\dev\\autopsy\\CoreLibs\\release\\modules\\ext\\guava-18.0.jar file.reference.imageio-bmp-3.1.1.jar=release/modules/ext/imageio-bmp-3.1.1.jar file.reference.imageio-core-3.1.1.jar=release/modules/ext/imageio-core-3.1.1.jar file.reference.imageio-icns-3.1.1.jar=release/modules/ext/imageio-icns-3.1.1.jar @@ -71,13 +71,13 @@ file.reference.xmlbeans-2.3.0.jar=release/modules/ext/xmlbeans-2.3.0.jar javac.source=1.7 javac.compilerargs=-Xlint -Xlint:-serial javadoc.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-javadoc.jar -javadoc.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-javadoc.jar +javadoc.reference.guava-18.0.jar=release/modules/ext/guava-18.0-javadoc.jar javadoc.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-javadoc.jar javadoc.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-javadoc.jar javadoc.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-javadoc.jar nbm.needs.restart=true source.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-sources.jar -source.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-sources.jar +source.reference.guava-18.0.jar=release/modules/ext/guava-18.0-sources.jar source.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-sources.jar source.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-sources.jar source.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-sources.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 5eb250b40c..09020ac24e 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -29,17 +29,11 @@ com.apple.eawt com.apple.eawt.event com.apple.eio - com.google.common.annotations com.google.common.base - com.google.common.base.internal com.google.common.cache com.google.common.collect com.google.common.eventbus - com.google.common.hash com.google.common.io - com.google.common.math - com.google.common.net - com.google.common.primitives com.google.common.util.concurrent com.google.gson com.google.gson.annotations @@ -708,10 +702,6 @@ ext/commons-lang3-3.0-sources.jar release/modules/ext/commons-lang3-3.0-sources.jar - - ext/guava-11.0.2.jar - release/modules/ext/guava-11.0.2.jar - ext/mail-1.4.3.jar release/modules/ext/mail-1.4.3.jar @@ -724,6 +714,10 @@ ext/imageio-pnm-3.1.1.jar release/modules/ext/imageio-pnm-3.1.1.jar + + ext/guava-18.0.jar + C:\dev\autopsy\CoreLibs\release\modules\ext\guava-18.0.jar + ext/common-lang-3.1.1.jar release/modules/ext/common-lang-3.1.1.jar From dbdf8bffc90f44405726ad4d3477fd76a0611ddd Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 2 Sep 2015 15:56:46 -0400 Subject: [PATCH 02/15] extend DetailViewNode interface to support more features of old visualization in new mode. --- .../ui/detailview/DescriptionVisibility.java | 7 +- .../ui/detailview/DetailViewNode.java | 19 ++- .../ui/detailview/DetailViewPane.java | 19 ++- .../ui/detailview/EventClusterNode.java | 35 ++--- .../ui/detailview/EventDetailChart.java | 92 ++++++------ .../ui/detailview/EventStripeNode.java | 131 +++++++++++------- 6 files changed, 163 insertions(+), 140 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java index 6689a87ed4..3f32941725 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java @@ -22,8 +22,9 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; * Level of description shown in UI NOTE: this is a separate concept form * {@link DescriptionLOD} */ -enum DescriptionVisibility { - - HIDDEN, COUNT_ONLY, SHOWN; +public enum DescriptionVisibility { + HIDDEN, + COUNT_ONLY, + SHOWN; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java index 4378eed8b3..e8a5bb8dcb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -7,22 +7,21 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import java.util.List; import java.util.Set; -import javafx.scene.layout.Pane; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** * */ -public interface DetailViewNode { +public interface DetailViewNode> { - long getStartMillis(); + public long getStartMillis(); - long getEndMillis(); + public long getEndMillis(); public void setDescriptionVisibility(DescriptionVisibility get); - public Pane getSubNodePane(); + public List getSubNodes(); public void setSpanWidths(List spanWidths); @@ -32,10 +31,16 @@ public interface DetailViewNode { public Set getEventIDs(); - public void applySelectionEffect(boolean applied); - public String getDescription(); public EventBundle getBundleDescriptor(); + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + void applyHighlightEffect(boolean applied); + + public void applySelectionEffect(boolean applied); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 773ba836f7..9118e6872a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -30,7 +30,6 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.scene.Cursor; @@ -101,7 +100,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -121,7 +120,7 @@ public class DetailViewPane extends AbstractVisualization aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); public ObservableList getAggregatedEvents() { return aggregatedEvents; @@ -150,7 +149,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getAddedSubList().forEach(aeNode -> { aeNode.applyHighlightEffect(true); @@ -167,7 +166,7 @@ public class DetailViewPane extends AbstractVisualization) (ScrollEvent t) -> { + this.onScrollProperty().set((ScrollEvent t) -> { vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0))); }); @@ -213,8 +212,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (EventClusterNode n : chart.getNodes((EventClusterNode t) - -> t.getEvent().getDescription().equals(tn.getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) + -> t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -237,8 +236,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (EventClusterNode n : chart.getNodes((EventClusterNode t) - -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) + -> t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -358,7 +357,7 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean applied) { c1.applySelectionEffect(applied); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index e5e38d8375..133ab3e511 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -80,7 +80,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Represents an {@link EventCluster} in a {@link EventDetailChart}. */ -public class EventClusterNode extends StackPane implements DetailViewNode { +public class EventClusterNode extends StackPane implements DetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); @@ -148,18 +148,23 @@ public class EventClusterNode extends StackPane implements DetailViewNode { private final Button plusButton = new Button(null, new ImageView(PLUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; private final Button minusButton = new Button(null, new ImageView(MINUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; + + private static void configureLODButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + b.setVisible(false); + b.setManaged(false); + } + private final EventDetailChart chart; private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); @@ -196,10 +201,6 @@ public class EventClusterNode extends StackPane implements DetailViewNode { hBox.setPadding(new Insets(2, 5, 2, 5)); hBox.setAlignment(Pos.CENTER_LEFT); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); borderPane.setPrefWidth(USE_COMPUTED_SIZE); @@ -322,8 +323,10 @@ public class EventClusterNode extends StackPane implements DetailViewNode { } @Override - public Pane getSubNodePane() { - return subNodePane; + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(EventClusterNode.class::cast) + .collect(Collectors.toList()); } synchronized public EventCluster getEvent() { @@ -413,7 +416,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { * * @param applied true to apply the highlight 'effect', false to remove it */ - synchronized void applyHighlightEffect(boolean applied) { + public synchronized void applyHighlightEffect(boolean applied) { if (applied) { descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS @@ -457,7 +460,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { * @param newDescriptionLOD */ synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); + subNodePane.getChildren().clear(); if (newDescriptionLOD == aggEvent.getDescriptionLOD()) { chart.setRequiresLayout(true); chart.requestChartLayout(); @@ -494,7 +497,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode { try { chart.setCursor(Cursor.WAIT); //assign subNodes and request chart layout - getSubNodePane().getChildren().setAll(get()); + subNodePane.getChildren().setAll(get()); setDescriptionVisibility(descrVis); chart.setRequiresLayout(true); chart.requestChartLayout(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index b5266f8207..8dadf6937a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -167,7 +168,7 @@ public final class EventDetailChart extends XYChart impl */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap projectionMap = FXCollections.observableHashMap(); + private final ObservableMap, Line> projectionMap = FXCollections.observableHashMap(); /** * flag indicating whether this chart actually needs a layout pass @@ -175,7 +176,7 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedNodes; + final ObservableList> selectedNodes; /** * list of series of data added to this chart TODO: replace this with a map @@ -205,7 +206,7 @@ public final class EventDetailChart extends XYChart impl private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); @@ -285,7 +286,7 @@ public final class EventDetailChart extends XYChart impl setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change change) -> { + projectionMap.addListener((MapChangeListener.Change, ? extends Line> change) -> { final Line valueRemoved = change.getValueRemoved(); if (valueRemoved != null) { getChartChildren().removeAll(valueRemoved); @@ -298,12 +299,12 @@ public final class EventDetailChart extends XYChart impl this.selectedNodes = selectedNodes; this.selectedNodes.addListener(( - ListChangeListener.Change c) -> { + ListChangeListener.Change> c) -> { while (c.next()) { - c.getRemoved().forEach((DetailViewNode t) -> { + c.getRemoved().forEach((DetailViewNode t) -> { projectionMap.remove(t); }); - c.getAddedSubList().forEach((DetailViewNode t) -> { + c.getAddedSubList().forEach((DetailViewNode t) -> { Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET ); @@ -316,7 +317,7 @@ public final class EventDetailChart extends XYChart impl } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap((DetailViewNode aggNode) -> aggNode.getEventIDs().stream()) + .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); @@ -420,7 +421,7 @@ public final class EventDetailChart extends XYChart impl return EventStripe.merge(u, v); } ); - EventStripeNode clusterNode = new EventStripeNode(eventCluster,null, EventDetailChart.this); + EventStripeNode clusterNode = new EventStripeNode(eventCluster, null, EventDetailChart.this); stripeNodeMap.put(eventCluster, clusterNode); nodeGroup.getChildren().add(clusterNode); } else { @@ -486,11 +487,11 @@ public final class EventDetailChart extends XYChart impl if (bandByType.get() == false) { if (alternateLayout.get() == true) { List nodes = new ArrayList<>(stripeNodeMap.values()); - Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } else { List nodes = new ArrayList<>(nodeMap.values()); - Collections.sort(nodes, Comparator.comparing(DetailViewNode::getStartMillis)); + nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } @@ -545,22 +546,28 @@ public final class EventDetailChart extends XYChart impl return descrVisibility; } - synchronized ReadOnlyDoubleProperty - getMaxVScroll() { + synchronized ReadOnlyDoubleProperty getMaxVScroll() { return maxY.getReadOnlyProperty(); } - Iterable getNodes(Predicate p) { - List nodes = new ArrayList<>(); + Iterable> getNodes(Predicate> p) { + Collection> values = alternateLayout.get() + ? stripeNodeMap.values() + : nodeMap.values(); - for (EventClusterNode node : nodeMap.values()) { - checkNode(node, p, nodes); - } - - return nodes; + //collapse tree of DetailViewNoeds to list and then filter on given predicate + return values.stream() + .flatMap(EventDetailChart::flatten) + .filter(p).collect(Collectors.toList()); } - Iterable getAllNodes() { + public static Stream> flatten(DetailViewNode node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(EventDetailChart::flatten)); + } + + Iterable> getAllNodes() { return getNodes(x -> true); } @@ -576,17 +583,6 @@ public final class EventDetailChart extends XYChart impl nodeGroup.setTranslateY(-d * h); } - private static void checkNode(EventClusterNode node, Predicate p, List nodes) { - if (node != null) { - if (p.test(node)) { - nodes.add(node); - } - for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { - checkNode((EventClusterNode) n, p, nodes); - } - } - } - private void clearGuideLine() { getChartChildren().remove(guideLine); guideLine = null; @@ -599,12 +595,12 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized double layoutNodes(final Collection nodes, final double minY, final double xOffset) { + private synchronized > double layoutNodes(final Collection nodes, final double minY, final double xOffset) { //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (D n : nodes) { + for (DVRegion n : nodes) { n.setDescriptionVisibility(descrVisibility.get()); double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis())); @@ -613,27 +609,19 @@ public final class EventDetailChart extends XYChart impl double layoutNodesResultHeight = 0; double span = 0; + List subNodes = n.getSubNodes(); + if (subNodes.isEmpty() == false) { + subNodes.sort(Comparator.comparing((DVRegion t) -> t.getStartMillis())); + layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); + } + if (n instanceof EventClusterNode) { - if (n.getSubNodePane().getChildren().isEmpty() == false) { - List children = n.getSubNodePane().getChildren().stream() - .map(EventClusterNode.class::cast) - .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) - .collect(Collectors.toList()); - layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); - } double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset; span = endX - startX; - //size timespan border n.setSpanWidths(Arrays.asList(span)); } else { - if (n.getSubNodePane().getChildren().isEmpty() == false) { - List children = n.getSubNodePane().getChildren().stream() - .map(EventStripeNode.class::cast) - .sorted(Comparator.comparing(DetailViewNode::getStartMillis)) - .collect(Collectors.toList()); - layoutNodesResultHeight = layoutNodes(children, 0, rawDisplayPosition); - } + EventStripeNode cn = (EventStripeNode) n; List spanWidths = new ArrayList<>(); double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; @@ -720,8 +708,8 @@ public final class EventDetailChart extends XYChart impl private static final int DEFAULT_ROW_HEIGHT = 24; private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final DetailViewNode aggNode = entry.getKey(); + for (final Map.Entry, Line> entry : projectionMap.entrySet()) { + final DetailViewNode aggNode = entry.getKey(); final Line line = entry.getValue(); line.setStartX(getParentXForValue(new DateTime(aggNode.getStartMillis(), TimeLineController.getJodaTimeZone()))); @@ -760,7 +748,7 @@ public final class EventDetailChart extends XYChart impl return alternateLayout; } - private static class StartTimeComparator implements Comparator { + private static class StartTimeComparator> implements Comparator { @Override public int compare(T n1, T n2) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index aab34c242a..3953b2baba 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -18,6 +18,7 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; @@ -29,7 +30,6 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; 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; @@ -41,6 +41,7 @@ import javafx.scene.layout.Region; import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; @@ -64,7 +65,7 @@ import org.sleuthkit.datamodel.SleuthkitCase; /** * */ -public class EventStripeNode extends StackPane implements DetailViewNode { +public class EventStripeNode extends StackPane implements DetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); @@ -94,20 +95,28 @@ public class EventStripeNode extends StackPane implements DetailViewNode { private final ImageView tagIV = new ImageView(TAG); private final Button plusButton = new Button(null, new ImageView(PLUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; private final Button minusButton = new Button(null, new ImageView(MINUS)) { { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); + configureLODButton(this); } }; + + private static void configureLODButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + private static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } private DescriptionVisibility descrVis; - private final HBox spanRegion = new HBox(); + private final HBox spansHBox = new HBox(); /** * The IamgeView used to show the icon for this node's event's type */ @@ -127,43 +136,36 @@ public class EventStripeNode extends StackPane implements DetailViewNode { final Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS); - final HBox hBox = new HBox(descrLabel, countLabel, spacer, hashIV, tagIV, minusButton, plusButton); + final HBox header = new HBox(descrLabel, countLabel, hashIV, tagIV, spacer, minusButton, plusButton); if (cluster.getEventIDsWithHashHits().isEmpty()) { - hashIV.setManaged(false); - hashIV.setVisible(false); + show(hashIV, false); } if (cluster.getEventIDsWithTags().isEmpty()) { - tagIV.setManaged(false); - tagIV.setVisible(false); + show(tagIV, false); } - hBox.setPrefWidth(USE_COMPUTED_SIZE); - hBox.setMinWidth(USE_PREF_SIZE); - hBox.setPadding(new Insets(2, 5, 2, 5)); - hBox.setAlignment(Pos.CENTER_LEFT); + header.setMinWidth(USE_PREF_SIZE); + header.setPadding(new Insets(2, 5, 2, 5)); + header.setAlignment(Pos.CENTER_LEFT); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); - final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); - BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); - borderPane.setPrefWidth(USE_COMPUTED_SIZE); + final VBox internalVBox = new VBox(header, subNodePane); + internalVBox.setAlignment(Pos.CENTER_LEFT); final Color evtColor = cluster.getType().getColor(); spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)); for (Range r : cluster.getRanges()) { - Region region = new Region(); - region.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS - region.setBackground(spanFill); - spanRegion.getChildren().addAll(region, new Region()); + Region spanRegion = new Region(); + spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS + spanRegion.setBackground(spanFill); + spansHBox.getChildren().addAll(spanRegion, new Region()); } - spanRegion.getChildren().remove(spanRegion.getChildren().size() - 1); - - getChildren().addAll(spanRegion, borderPane); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + spansHBox.getChildren().remove(spansHBox.getChildren().size() - 1); + spansHBox.setMaxWidth(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + getChildren().addAll(spansHBox, internalVBox); + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .05), CORNER_RADII, Insets.EMPTY))); setAlignment(Pos.TOP_LEFT); setMinHeight(24); - minWidthProperty().bind(spanRegion.widthProperty()); + minWidthProperty().bind(spansHBox.widthProperty()); setPrefHeight(USE_COMPUTED_SIZE); setMaxHeight(USE_PREF_SIZE); @@ -189,20 +191,20 @@ public class EventStripeNode extends StackPane implements DetailViewNode { setOnMouseEntered((MouseEvent e) -> { //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart // installTooltip(); - spanRegion.setEffect(new DropShadow(10, evtColor)); - minusButton.setVisible(true); - plusButton.setVisible(true); - minusButton.setManaged(true); - plusButton.setManaged(true); + spansHBox.setEffect(new DropShadow(10, evtColor)); + show(spacer, true); + show(minusButton, true); + show(plusButton, true); + toFront(); }); setOnMouseExited((MouseEvent e) -> { - spanRegion.setEffect(null); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); + spansHBox.setEffect(null); + show(spacer, false); + show(minusButton, false); + show(plusButton, false); + }); plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); @@ -232,11 +234,11 @@ public class EventStripeNode extends StackPane implements DetailViewNode { @Override public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { - Region get = (Region) spanRegion.getChildren().get(i); + Region spanRegion = (Region) spansHBox.getChildren().get(i); Double w = spanWidths.get(i); - get.setPrefWidth(w); - get.setMaxWidth(w); - get.setMinWidth(Math.max(2, w)); + spanRegion.setPrefWidth(w); + spanRegion.setMaxWidth(w); + spanRegion.setMinWidth(Math.max(2, w)); } } @@ -280,8 +282,10 @@ public class EventStripeNode extends StackPane implements DetailViewNode { } @Override - public Pane getSubNodePane() { - return subNodePane; + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(EventStripeNode.class::cast) + .collect(Collectors.toList()); } /** @@ -339,6 +343,26 @@ public class EventStripeNode extends StackPane implements DetailViewNode { }); } + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + @Override + public synchronized void applyHighlightEffect(boolean applied) { + if (applied) { + descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS + spanFill = new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); + spansHBox.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + } else { + descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS + spanFill = new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); + spansHBox.setBackground(spanFill); + setBackground(new Background(new BackgroundFill(cluster.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + } + } + @Override public String getDescription() { return cluster.getDescription(); @@ -355,11 +379,14 @@ public class EventStripeNode extends StackPane implements DetailViewNode { * @param newDescriptionLOD */ synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); + subNodePane.getChildren().clear(); + if (newDescriptionLOD == cluster.getDescriptionLOD()) { + spansHBox.setVisible(true); chart.setRequiresLayout(true); chart.requestChartLayout(); } else { + spansHBox.setVisible(false); RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters combinedFilter.getSubFilters().addAll(new TextFilter(cluster.getDescription()), @@ -402,7 +429,7 @@ public class EventStripeNode extends StackPane implements DetailViewNode { try { chart.setCursor(Cursor.WAIT); //assign subNodes and request chart layout - getSubNodePane().getChildren().setAll(get()); + subNodePane.getChildren().setAll(get()); setDescriptionVisibility(descrVis); chart.setRequiresLayout(true); chart.requestChartLayout(); From d48ff720985d640030228551a2dc62ca366898ef Mon Sep 17 00:00:00 2001 From: jmillman Date: Fri, 11 Sep 2015 15:13:26 -0400 Subject: [PATCH 03/15] WIP Cleanup, verification that all existing features till work in new alternate layout mode. --- .../timeline/datamodel/EventBundle.java | 11 +- .../timeline/datamodel/EventCluster.java | 12 +- .../timeline/datamodel/EventStripe.java | 15 ++- .../autopsy/timeline/db/EventDB.java | 2 +- .../ui/detailview/DetailViewNode.java | 4 +- .../ui/detailview/DetailViewPane.java | 10 +- .../ui/detailview/EventClusterNode.java | 58 ++++----- .../ui/detailview/EventDetailChart.java | 112 ++++++++++-------- .../ui/detailview/EventStripeNode.java | 70 +++++------ .../tree/EventDescriptionTreeItem.java | 6 +- .../ui/detailview/tree/EventTypeTreeItem.java | 4 +- .../timeline/ui/detailview/tree/NavPanel.java | 6 +- .../timeline/ui/detailview/tree/RootItem.java | 4 +- 13 files changed, 174 insertions(+), 140 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java index 0677f86c61..17d5a01e7b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -5,6 +5,7 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; +import com.google.common.collect.Range; import java.util.Set; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; @@ -18,16 +19,18 @@ public interface EventBundle { DescriptionLOD getDescriptionLOD(); - long getEndMillis(); - Set getEventIDs(); Set getEventIDsWithHashHits(); Set getEventIDsWithTags(); + EventType getEventType(); + + long getEndMillis(); + long getStartMillis(); - EventType getType(); - + Iterable> getRanges(); + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 9a26affa7e..9f5cc5428c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -115,7 +115,7 @@ public class EventCluster implements EventBundle { return description; } - public EventType getType() { + public EventType getEventType() { return type; } @@ -135,7 +135,7 @@ public class EventCluster implements EventBundle { */ public static EventCluster merge(EventCluster aggEvent1, EventCluster ag2) { - if (aggEvent1.getType() != ag2.getType()) { + if (aggEvent1.getEventType() != ag2.getEventType()) { throw new IllegalArgumentException("aggregate events are not compatible they have different types"); } @@ -146,7 +146,7 @@ public class EventCluster implements EventBundle { Sets.SetView hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits()); Sets.SetView taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags()); - return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); + return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); } Range getRange() { @@ -156,4 +156,10 @@ public class EventCluster implements EventBundle { return Range.singleton(getStartMillis()); } } + + @Override + public Iterable> getRanges() { + return Collections.singletonList(getRange()); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 49dd736611..9bd45ce278 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -63,7 +63,7 @@ public final class EventStripe implements EventBundle { public EventStripe(EventCluster aggEvent) { spans.add(aggEvent.getRange()); spanMap.put(aggEvent.getRange(), aggEvent); - type = aggEvent.getType(); + type = aggEvent.getEventType(); description = aggEvent.getDescription(); lod = aggEvent.getDescriptionLOD(); eventIDs.addAll(aggEvent.getEventIDs()); @@ -76,7 +76,7 @@ public final class EventStripe implements EventBundle { spans.addAll(v.spans); spanMap.putAll(u.spanMap); spanMap.putAll(v.spanMap); - type = u.getType(); + type = u.getEventType(); description = u.getDescription(); lod = u.getDescriptionLOD(); eventIDs.addAll(u.getEventIDs()); @@ -96,38 +96,47 @@ public final class EventStripe implements EventBundle { return new EventStripe(u, v); } + @Override public String getDescription() { return description; } - public EventType getType() { + @Override + public EventType getEventType() { return type; } + @Override public DescriptionLOD getDescriptionLOD() { return lod; } + @Override public Set getEventIDs() { return Collections.unmodifiableSet(eventIDs); } + @Override public Set getEventIDsWithHashHits() { return Collections.unmodifiableSet(hashHits); } + @Override public Set getEventIDsWithTags() { return Collections.unmodifiableSet(tagged); } + @Override public long getStartMillis() { return spans.span().lowerEndpoint(); } + @Override public long getEndMillis() { return spans.span().upperEndpoint(); } + @Override public Iterable> getRanges() { return spans.asRanges(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 01837e30ca..ec72314dc4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -1140,7 +1140,7 @@ public class EventDB { Map> typeMap = new HashMap<>(); for (EventCluster aggregateEvent : preMergedEvents) { - typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create()) + typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create()) .put(aggregateEvent.getDescription(), aggregateEvent); } //result list to return diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java index e8a5bb8dcb..efd3a47586 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -27,13 +27,13 @@ public interface DetailViewNode> { public void setDescriptionWidth(double max); - public EventType getType(); + public EventType getEventType(); public Set getEventIDs(); public String getDescription(); - public EventBundle getBundleDescriptor(); + public EventBundle getEventBundle(); /** * apply the 'effect' to visually indicate highlighted nodes diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 9118e6872a..5f1d32b083 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -70,6 +70,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -100,7 +101,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization, EventDetailChart> { +public class DetailViewPane extends AbstractVisualization { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -336,7 +337,7 @@ public class DetailViewPane extends AbstractVisualization { if (isCancelled() == false) { - getSeries(e.getType()).getData().add(xyData); + getSeries(e.getEventType()).getData().add(xyData); } }); } @@ -357,8 +358,9 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean applied) { - c1.applySelectionEffect(applied); + protected void applySelectionEffect(EventBundle c1, Boolean selected) { + chart.applySelectionEffect(c1, selected); + } private class DetailViewSettingsPane extends HBox { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 133ab3e511..20baf039b1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -99,7 +99,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode hashSetCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithHashHits().isEmpty()) { + if (!eventCluster.getEventIDsWithHashHits().isEmpty()) { hashSetCounts = new HashMap<>(); try { - for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.getEventIDsWithHashHits())) { + for (TimeLineEvent tle : eventsModel.getEventsById(eventCluster.getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -298,8 +298,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode tagCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithTags().isEmpty()) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags())); + if (!eventCluster.getEventIDsWithTags().isEmpty()) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(eventCluster.getEventIDsWithTags())); } @@ -312,7 +312,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode> loggedTask = new LoggedTask>( @@ -523,11 +523,11 @@ public class EventClusterNode extends StackPane implements DetailViewNode 1) { final DescriptionLOD next = descLOD.get().next(); if (next != null) { @@ -535,7 +535,7 @@ public class EventClusterNode extends StackPane implements DetailViewNode impl /** * map from event to node */ - private final Map nodeMap = new HashMap<>(); + private final Map clusterNodeMap = new HashMap<>(); private final Map, EventStripe> stripeDescMap = new HashMap<>(); private final Map stripeNodeMap = new HashMap<>(); /** @@ -168,7 +167,7 @@ public final class EventDetailChart extends XYChart impl */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap, Line> projectionMap = FXCollections.observableHashMap(); + private final Map, Line> projectionMap = new HashMap<>(); /** * flag indicating whether this chart actually needs a layout pass @@ -176,7 +175,7 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList> selectedNodes; + final ObservableList selectedBundles; /** * list of series of data added to this chart TODO: replace this with a map @@ -206,11 +205,11 @@ public final class EventDetailChart extends XYChart impl private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); - //yAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm + //verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm verticalAxis.setTickLabelsVisible(false); verticalAxis.setTickMarkVisible(false); @@ -224,7 +223,6 @@ public final class EventDetailChart extends XYChart impl //bind listener to events that should trigger layout widthProperty().addListener(layoutInvalidationListener); heightProperty().addListener(layoutInvalidationListener); -// boundsInLocalProperty().addListener(layoutInvalidationListener); bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); @@ -286,34 +284,41 @@ public final class EventDetailChart extends XYChart impl setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change, ? extends Line> change) -> { - final Line valueRemoved = change.getValueRemoved(); - if (valueRemoved != null) { - getChartChildren().removeAll(valueRemoved); - } - final Line valueAdded = change.getValueAdded(); - if (valueAdded != null) { - getChartChildren().add(valueAdded); - } - }); - - this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(( - ListChangeListener.Change> c) -> { +// projectionMap.addListener((MapChangeListener.Change, ? extends Line> change) -> { +// final Line valueRemoved = change.getValueRemoved(); +// if (valueRemoved != null) { +// getChartChildren().removeAll(valueRemoved); +// } +// final Line valueAdded = change.getValueAdded(); +// if (valueAdded != null) { +// getChartChildren().add(valueAdded); +// } +// }); + this.selectedBundles = selectedNodes; + this.selectedBundles.addListener(( + ListChangeListener.Change c) -> { while (c.next()) { - c.getRemoved().forEach((DetailViewNode t) -> { - projectionMap.remove(t); - }); - c.getAddedSubList().forEach((DetailViewNode t) -> { - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(t.getType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(t, line); - }); + c.getRemoved().forEach((EventBundle t) -> { + t.getRanges().forEach((Range t1) -> { + Line removeAll = projectionMap.remove(t1); + getChartChildren().removeAll(removeAll); + }); + }); + c.getAddedSubList().forEach((EventBundle t) -> { + + for (Range r : t.getRanges()) { + + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + ); + line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5)); + line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); + line.setStrokeLineCap(StrokeLineCap.ROUND); + projectionMap.put(r, line); + getChartChildren().add(line); + } + }); } this.controller.selectEventIDs(selectedNodes.stream() @@ -348,7 +353,7 @@ public final class EventDetailChart extends XYChart impl clearGuideLine(); clearIntervalSelector(); - selectedNodes.clear(); + selectedBundles.clear(); projectionMap.clear(); controller.selectEventIDs(Collections.emptyList()); }); @@ -411,7 +416,7 @@ public final class EventDetailChart extends XYChart impl protected synchronized void dataItemAdded(Series series, int i, Data data) { final EventCluster aggEvent = data.getYValue(); if (alternateLayout.get()) { - EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getType(), aggEvent.getDescription()), + EventStripe eventCluster = stripeDescMap.merge(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription()), new EventStripe(aggEvent), (EventStripe u, EventStripe v) -> { EventStripeNode remove = stripeNodeMap.remove(u); @@ -425,10 +430,10 @@ public final class EventDetailChart extends XYChart impl stripeNodeMap.put(eventCluster, clusterNode); nodeGroup.getChildren().add(clusterNode); } else { - nodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> { + clusterNodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> { EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this); eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); - nodeMap.put(aggEvent, eventNode); + clusterNodeMap.put(aggEvent, eventNode); nodeGroup.getChildren().add(eventNode); return eventNode; }); @@ -444,10 +449,10 @@ public final class EventDetailChart extends XYChart impl @Override protected synchronized void dataItemRemoved(Data data, Series series) { EventCluster aggEvent = data.getYValue(); - Node removedNode = nodeMap.remove(aggEvent); + Node removedNode = clusterNodeMap.remove(aggEvent); nodeGroup.getChildren().remove(removedNode); - EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getType(), aggEvent.getDescription())); + EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription())); removedNode = stripeNodeMap.remove(removedCluster); nodeGroup.getChildren().remove(removedNode); @@ -490,7 +495,7 @@ public final class EventDetailChart extends XYChart impl nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } else { - List nodes = new ArrayList<>(nodeMap.values()); + List nodes = new ArrayList<>(clusterNodeMap.values()); nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); layoutNodes(nodes, minY, 0); } @@ -500,7 +505,7 @@ public final class EventDetailChart extends XYChart impl if (alternateLayout.get() == true) { List nodes = s.getData().stream() .map(Data::getYValue) - .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getType(), cluster.getDescription()))) + .map(cluster -> stripeDescMap.get(ImmutablePair.of(cluster.getEventType(), cluster.getDescription()))) .distinct() .sorted(Comparator.comparing(EventStripe::getStartMillis)) .map(stripeNodeMap::get) @@ -509,7 +514,7 @@ public final class EventDetailChart extends XYChart impl } else { List nodes = s.getData().stream() .map(Data::getYValue) - .map(nodeMap::get) + .map(clusterNodeMap::get) .filter(Objects::nonNull) .sorted(Comparator.comparing(EventClusterNode::getStartMillis)) .collect(Collectors.toList()); @@ -553,7 +558,7 @@ public final class EventDetailChart extends XYChart impl Iterable> getNodes(Predicate> p) { Collection> values = alternateLayout.get() ? stripeNodeMap.values() - : nodeMap.values(); + : clusterNodeMap.values(); //collapse tree of DetailViewNoeds to list and then filter on given predicate return values.stream() @@ -626,7 +631,7 @@ public final class EventDetailChart extends XYChart impl List spanWidths = new ArrayList<>(); double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; double x2; - Iterator> ranges = cn.getCluster().getRanges().iterator(); + Iterator> ranges = cn.getStripe().getRanges().iterator(); Range range = ranges.next(); do { x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); @@ -708,12 +713,12 @@ public final class EventDetailChart extends XYChart impl private static final int DEFAULT_ROW_HEIGHT = 24; private void layoutProjectionMap() { - for (final Map.Entry, Line> entry : projectionMap.entrySet()) { - final DetailViewNode aggNode = entry.getKey(); + for (final Map.Entry, Line> entry : projectionMap.entrySet()) { + final Range eventBundle = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForValue(new DateTime(aggNode.getStartMillis(), TimeLineController.getJodaTimeZone()))); - line.setEndX(getParentXForValue(new DateTime(aggNode.getEndMillis(), TimeLineController.getJodaTimeZone()))); + line.setStartX(getParentXForValue(new DateTime(eventBundle.lowerEndpoint(), TimeLineController.getJodaTimeZone()))); + line.setEndX(getParentXForValue(new DateTime(eventBundle.upperEndpoint(), TimeLineController.getJodaTimeZone()))); line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); } @@ -791,4 +796,13 @@ public final class EventDetailChart extends XYChart impl protected void requestChartLayout() { super.requestChartLayout(); } + + void applySelectionEffect(EventBundle c1, Boolean selected) { + if (alternateLayout.get()) { + EventStripe eventStripe = stripeDescMap.get(ImmutablePair.of(c1.getEventType(), c1.getDescription())); + stripeNodeMap.get(eventStripe).applySelectionEffect(selected); + } else { + clusterNodeMap.get(c1).applySelectionEffect(selected); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 3953b2baba..4e03ff342d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -75,7 +75,7 @@ public class EventStripeNode extends StackPane implements DetailViewNode descLOD = new SimpleObjectProperty<>(); @@ -124,23 +124,23 @@ public class EventStripeNode extends StackPane implements DetailViewNode r : cluster.getRanges()) { + for (Range r : eventStripe.getRanges()) { Region spanRegion = new Region(); spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS spanRegion.setBackground(spanFill); @@ -162,7 +162,7 @@ public class EventStripeNode extends StackPane implements DetailViewNode { final DescriptionLOD next = descLOD.get().next(); @@ -228,7 +228,7 @@ public class EventStripeNode extends StackPane implements DetailViewNode 1) { final DescriptionLOD next = descLOD.get().next(); if (next != null) { @@ -310,20 +310,20 @@ public class EventStripeNode extends StackPane implements DetailViewNode getEventIDs() { - return cluster.getEventIDs(); + return eventStripe.getEventIDs(); } private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); @@ -352,25 +352,25 @@ public class EventStripeNode extends StackPane implements DetailViewNode> loggedTask = new LoggedTask>( diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index f2f5a7b1da..69fdd1fe43 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -29,7 +29,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; class EventDescriptionTreeItem extends NavTreeItem { public EventDescriptionTreeItem(EventCluster g) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size())); + setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getDescription(), g.getEventIDs().size())); } @Override @@ -40,7 +40,7 @@ class EventDescriptionTreeItem extends NavTreeItem { @Override public void insert(EventCluster g) { NavTreeNode value = getValue(); - if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { + if ((value.getType().getBaseType().equals(g.getEventType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { throw new IllegalArgumentException(); } @@ -54,7 +54,7 @@ class EventDescriptionTreeItem extends NavTreeItem { @Override public TreeItem findTreeItemForEvent(EventBundle t) { - if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { + if (getValue().getType().getBaseType() == t.getEventType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { return this; } return null; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index d9508cfd4d..fb2947bc42 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -37,7 +37,7 @@ class EventTypeTreeItem extends NavTreeItem { private final Comparator> comparator = TreeComparator.Description; EventTypeTreeItem(EventCluster g) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0)); + setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getEventType().getBaseType().getDisplayName(), 0)); } @Override @@ -79,7 +79,7 @@ class EventTypeTreeItem extends NavTreeItem { @Override public TreeItem findTreeItemForEvent(EventBundle t) { - if (t.getType().getBaseType() == getValue().getType().getBaseType()) { + if (t.getEventType().getBaseType() == getValue().getType().getBaseType()) { for (TreeItem child : getChildren()) { final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 8718b6f90c..898917ae15 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -41,9 +41,9 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** @@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView { }); detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach((DetailViewNode t) -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getBundleDescriptor())); + detailViewPane.getSelectedNodes().forEach((EventBundle t) -> { + eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t)); }); }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 38a9547cba..cb15a0b622 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -59,11 +59,11 @@ class RootItem extends NavTreeItem { @Override public void insert(EventCluster g) { - EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType()); + EventTypeTreeItem treeItem = childMap.get(g.getEventType().getBaseType()); if (treeItem == null) { final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); newTreeItem.setExpanded(true); - childMap.put(g.getType().getBaseType(), newTreeItem); + childMap.put(g.getEventType().getBaseType(), newTreeItem); newTreeItem.insert(g); Platform.runLater(() -> { From 36097522a137056b01ebe0649af9f425779f3823 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 14 Sep 2015 15:53:41 -0400 Subject: [PATCH 04/15] refactor common code out of clustering modes --- .../timeline/datamodel/EventCluster.java | 16 +- .../timeline/datamodel/EventStripe.java | 18 +- .../ui/detailview/AbstractDetailViewNode.java | 452 ++++++++++++++++++ .../ui/detailview/DetailViewNode.java | 43 +- .../ui/detailview/DetailViewPane.java | 6 +- .../ui/detailview/EventClusterNode.java | 443 ++--------------- .../ui/detailview/EventDetailChart.java | 106 ++-- .../ui/detailview/EventStripeNode.java | 418 ++-------------- .../timeline/ui/detailview/tree/NavPanel.java | 8 +- .../ui/detailview/tree/NavTreeItem.java | 2 +- 10 files changed, 618 insertions(+), 894 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 9f5cc5428c..7533c486e1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -127,26 +127,26 @@ public class EventCluster implements EventBundle { /** * merge two aggregate events into one new aggregate event. * - * @param aggEvent1 + * @param cluster1 * @param aggEVent2 * * @return a new aggregate event that is the result of merging the given * events */ - public static EventCluster merge(EventCluster aggEvent1, EventCluster ag2) { + public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { - if (aggEvent1.getEventType() != ag2.getEventType()) { + if (cluster1.getEventType() != cluster2.getEventType()) { throw new IllegalArgumentException("aggregate events are not compatible they have different types"); } - if (!aggEvent1.getDescription().equals(ag2.getDescription())) { + if (!cluster1.getDescription().equals(cluster2.getDescription())) { throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions"); } - Sets.SetView idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs()); - Sets.SetView hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits()); - Sets.SetView taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags()); + Sets.SetView idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); + Sets.SetView hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); + Sets.SetView taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags()); - return new EventCluster(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); + return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); } Range getRange() { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java index 9bd45ce278..e88e8ff623 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -60,15 +60,15 @@ public final class EventStripe implements EventBundle { */ private final Set hashHits = new HashSet<>(); - public EventStripe(EventCluster aggEvent) { - spans.add(aggEvent.getRange()); - spanMap.put(aggEvent.getRange(), aggEvent); - type = aggEvent.getEventType(); - description = aggEvent.getDescription(); - lod = aggEvent.getDescriptionLOD(); - eventIDs.addAll(aggEvent.getEventIDs()); - tagged.addAll(aggEvent.getEventIDsWithTags()); - hashHits.addAll(aggEvent.getEventIDsWithHashHits()); + public EventStripe(EventCluster cluster) { + spans.add(cluster.getRange()); + spanMap.put(cluster.getRange(), cluster); + type = cluster.getEventType(); + description = cluster.getDescription(); + lod = cluster.getDescriptionLOD(); + eventIDs.addAll(cluster.getEventIDs()); + tagged.addAll(cluster.getEventIDsWithTags()); + hashHits.addAll(cluster.getEventIDsWithHashHits()); } private EventStripe(EventStripe u, EventStripe v) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java new file mode 100644 index 0000000000..743e49b476 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -0,0 +1,452 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.ui.detailview; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.LoggedTask; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.filters.RootFilter; +import org.sleuthkit.autopsy.timeline.filters.TextFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.SleuthkitCase; + +public abstract class AbstractDetailViewNode< T extends EventBundle, S extends AbstractDetailViewNode> extends StackPane implements DetailViewNode> { + + static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); + static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS + static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS + static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS + static final CornerRadii CORNER_RADII = new CornerRadii(3); + Map dropShadowMap = new HashMap<>(); + + static void configureLODButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + /** + * the border to apply when this node is 'selected' + */ + static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + final Color evtColor; + + @Override + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(t -> (S) t) + .collect(Collectors.toList()); + } + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + @Override + public void applySelectionEffect(boolean applied) { + Platform.runLater(() -> { + if (applied) { + setBorder(selectionBorder); + } else { + setBorder(null); + } + }); + } + + static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } + final ImageView hashIV = new ImageView(HASH_PIN); + final ImageView tagIV = new ImageView(TAG); + private final S parentNode; + DescriptionVisibility descrVis; + + /** + * Pane that contains AggregateEventNodes of any 'subevents' if they are + * displayed + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart + */ + final Pane subNodePane = new Pane(); + + /** + * The ImageView used to show the icon for this node's event's type + */ + private final ImageView eventTypeImageView = new ImageView(); + + /** + * The label used to display this node's event's description + */ + final Label descrLabel = new Label(); + + /** + * The label used to display this node's event count + */ + final Label countLabel = new Label(); + + private final T eventBundle; + private final EventDetailChart chart; + final SleuthkitCase sleuthkitCase; + final FilteredEventsModel eventsModel; + + final Button plusButton = new Button(null, new ImageView(PLUS)) { + { + configureLODButton(this); + } + + }; + + final Button minusButton = new Button(null, new ImageView(MINUS)) { + { + configureLODButton(this); + } + }; + SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final HBox header; + + /** + * + * @param showControls the value of par + */ + void showDescriptionLoDControls(final boolean showControls) { + DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), + eventType -> new DropShadow(10, eventType.getColor())); + getSpanFillNode().setEffect(showControls ? dropShadow : null); + show(minusButton, showControls); + show(plusButton, showControls); + } + final Region spacer = new Region(); + + RootFilter getSubClusterFilter() { + RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); + //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters + combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()), + new TypeFilter(getEventType())); + return combinedFilter; + } + + abstract Collection makeBundlesFromClusters(List eventClusters); + + abstract void showSpans(final boolean showSpans); + + public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) { + this.eventBundle = bundle; + this.parentNode = parentEventNode; + this.chart = chart; + descLOD.set(bundle.getDescriptionLOD()); + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chart.getController().getEventsModel(); + + if (eventBundle.getEventIDsWithHashHits().isEmpty()) { + show(hashIV, false); + } + if (eventBundle.getEventIDsWithTags().isEmpty()) { + show(tagIV, false); + } + HBox.setHgrow(spacer, Priority.ALWAYS); + header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /*spacer,*/ minusButton, plusButton); + + header.setMinWidth(USE_PREF_SIZE); + header.setPadding(new Insets(2, 5, 2, 5)); + header.setAlignment(Pos.CENTER_LEFT); + //setup description label + evtColor = getEventType().getColor(); + eventTypeImageView.setImage(getEventType().getFXImage()); + descrLabel.setGraphic(eventTypeImageView); + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + descrLabel.setMouseTransparent(true); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMinHeight(USE_PREF_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + subNodePane.setPickOnBounds(false); + + setAlignment(Pos.TOP_LEFT); + setMinHeight(24); + + setPrefHeight(USE_COMPUTED_SIZE); + setMaxHeight(USE_PREF_SIZE); + setOnMouseClicked(new EventMouseHandler()); + + //set up mouse hover effect and tooltip + setOnMouseEntered((MouseEvent e) -> { + //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart + installTooltip(); + showDescriptionLoDControls(true); + toFront(); + }); + + setOnMouseExited((MouseEvent e) -> { + showDescriptionLoDControls(false); + }); + setCursor(Cursor.HAND); + + plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + minusButton.disableProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); + + plusButton.setOnMouseClicked(e -> { + final DescriptionLOD next = descLOD.get().next(); + if (next != null) { + loadSubClusters(next); + descLOD.set(next); + } + }); + minusButton.setOnMouseClicked(e -> { + final DescriptionLOD previous = descLOD.get().previous(); + if (previous != null) { + loadSubClusters(previous); + descLOD.set(previous); + } + }); + + setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + + setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); + } + + /** + * @param w the maximum width the description label should have + */ + @Override + public void setDescriptionWidth(double w) { + getDescrLabel().setMaxWidth(w); + } + + abstract void installTooltip(); + + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + @Override + public synchronized void applyHighlightEffect(boolean applied) { + if (applied) { + getDescrLabel().setStyle("-fx-font-weight: bold;"); // NON-NLS + getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY))); + setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + } else { + getDescrLabel().setStyle("-fx-font-weight: normal;"); // NON-NLS + getSpanFillNode().setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); + } + } + + String getDisplayedDescription() { + return getDescrLabel().getText(); + } + + abstract Region getSpanFillNode(); + + Button getPlusButton() { + return plusButton; + } + + Button getMinusButton() { + return minusButton; + } + + public final Label getDescrLabel() { + return descrLabel; + } + + public final Label getCountLabel() { + return countLabel; + } + + public S getParentNode() { + return parentNode; + } + + @Override + public final T getEventBundle() { + return eventBundle; + } + + public final EventDetailChart getChart() { + return chart; + } + + public DescriptionLOD getDescLOD() { + return descLOD.get(); + } + private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class.getName()); + + /** + * loads sub-clusters at the given Description LOD + * + * @param newDescriptionLOD + */ + final synchronized void loadSubClusters(DescriptionLOD newDescriptionLOD) { + subNodePane.getChildren().clear(); + if (newDescriptionLOD == getEventBundle().getDescriptionLOD()) { + showSpans(true); + getChart().setRequiresLayout(true); + getChart().requestChartLayout(); + } else { + showSpans(false); + RootFilter combinedFilter = getSubClusterFilter(); + + //make a new end inclusive span (to 'filter' with) + final Interval span = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); + + //make a task to load the subnodes + LoggedTask> loggedTask = new LoggedTask>( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + + @Override + protected List call() throws Exception { + //query for the sub-clusters + List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, + eventsModel.eventTypeZoomProperty().get(), + combinedFilter, + newDescriptionLOD)); + + return makeBundlesFromClusters(aggregatedEvents).stream() + .map(aggEvent -> { + return getNodeForCluser(aggEvent); + }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters + } + + @Override + protected void succeeded() { + try { + getChart().setCursor(Cursor.WAIT); + //assign subNodes and request chart layout + subNodePane.getChildren().setAll(get()); + setDescriptionVisibility(descrVis); + getChart().setRequiresLayout(true); + getChart().requestChartLayout(); + getChart().setCursor(null); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + } + }; + + //start task + getChart().getController().monitorTask(loggedTask); + } + } + + final double getLayoutXCompensation() { + return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } + + @Override + final public void setDescriptionVisibility(DescriptionVisibility descrVis) { + this.descrVis = descrVis; + final int size = getEventBundle().getEventIDs().size(); + + switch (descrVis) { + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + default: + case SHOWN: + String description = getEventBundle().getDescription(); + description = getParentNode() != null + ? " ..." + StringUtils.substringAfter(description, getParentNode().getDescription()) + : description; + descrLabel.setText(description); + countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS + break; + } + } + + /** + * event handler used for mouse events on {@link AggregateEventNode}s + */ + private class EventMouseHandler implements EventHandler { + + @Override + public void handle(MouseEvent t) { + + if (t.getButton() == MouseButton.PRIMARY) { + t.consume(); + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(AbstractDetailViewNode.this) == false) { + chart.selectedNodes.add(AbstractDetailViewNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(AbstractDetailViewNode.this); + } else if (t.getClickCount() > 1) { + final DescriptionLOD next = descLOD.get().next(); + if (next != null) { + loadSubClusters(next); + descLOD.set(next); + } + } else { + chart.selectedNodes.setAll(AbstractDetailViewNode.this); + } + + } + } + } + + abstract S getNodeForCluser(T cluster); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java index efd3a47586..5ae2ab2e0b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -5,6 +5,7 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; +import java.util.Comparator; import java.util.List; import java.util.Set; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; @@ -15,24 +16,14 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; */ public interface DetailViewNode> { - public long getStartMillis(); - - public long getEndMillis(); - public void setDescriptionVisibility(DescriptionVisibility get); - public List getSubNodes(); + public List getSubNodes(); public void setSpanWidths(List spanWidths); public void setDescriptionWidth(double max); - public EventType getEventType(); - - public Set getEventIDs(); - - public String getDescription(); - public EventBundle getEventBundle(); /** @@ -43,4 +34,34 @@ public interface DetailViewNode> { void applyHighlightEffect(boolean applied); public void applySelectionEffect(boolean applied); + + default String getDescription() { + return getEventBundle().getDescription(); + } + + default EventType getEventType() { + return getEventBundle().getEventType(); + } + + default Set getEventIDs() { + return getEventBundle().getEventIDs(); + } + + default public long getStartMillis() { + return getEventBundle().getStartMillis(); + } + + default long getEndMillis() { + return getEventBundle().getEndMillis(); + } + + + + static class StartTimeComparator implements Comparator> { + + @Override + public int compare(DetailViewNode o1, DetailViewNode o2) { + return Long.compare(o1.getStartMillis(), o2.getStartMillis()); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index 5f1d32b083..f40948c20b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -70,7 +70,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -101,7 +100,7 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; * TODO: refactor common code out of this class and CountsChartPane into * {@link AbstractVisualization} */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); @@ -358,9 +357,8 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { chart.applySelectionEffect(c1, selected); - } private class DetailViewSettingsPane extends HBox { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index 20baf039b1..b07adac60b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -18,120 +18,39 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; -import javafx.event.EventHandler; -import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; -import javafx.scene.control.OverrunStyle; import javafx.scene.control.Tooltip; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -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; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.Interval; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.ColorUtilities; -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.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TextFilter; -import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; -import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** * Represents an {@link EventCluster} in a {@link EventDetailChart}. */ -public class EventClusterNode extends StackPane implements DetailViewNode { +public class EventClusterNode extends AbstractDetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); - private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS - private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS - private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS - - private static final CornerRadii CORNER_RADII = new CornerRadii(3); - - /** - * the border to apply when this node is 'selected' - */ - private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - - /** - * The event this AggregateEventNode represents visually - */ - private EventCluster eventCluster; - - private final EventClusterNode parentEventNode; - /** * the region that represents the time span of this node's event */ private final Region spanRegion = new Region(); - /** - * The label used to display this node's event's description - */ - private final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - private final Label countLabel = new Label(); - - /** - * The IamgeView used to show the icon for this node's event's type - */ - private final ImageView eventTypeImageView = new ImageView(); - - /** - * Pane that contains AggregateEventNodes of any 'subevents' if they are - * displayed - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final Pane subNodePane = new Pane(); - /** * the context menu that with the slider that controls subnode/event display * @@ -140,153 +59,34 @@ public class EventClusterNode extends StackPane implements DetailViewNode contextMenu = new SimpleObjectProperty<>(); - /** - * the Background used to fill the spanRegion, this varies epending on the - * selected/highlighted state of this node in its parent EventDetailChart - */ - private Background spanFill; - - private final Button plusButton = new Button(null, new ImageView(PLUS)) { - { - configureLODButton(this); - } - }; - private final Button minusButton = new Button(null, new ImageView(MINUS)) { - { - configureLODButton(this); - } - }; - - private static void configureLODButton(Button b) { - b.setMinSize(16, 16); - b.setMaxSize(16, 16); - b.setPrefSize(16, 16); - b.setVisible(false); - b.setManaged(false); - } - - private final EventDetailChart chart; - - private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - private DescriptionVisibility descrVis; - private final SleuthkitCase sleuthkitCase; - private final FilteredEventsModel eventsModel; - private Tooltip tooltip; - private final ImageView hashIV = new ImageView(HASH_PIN); - private final ImageView tagIV = new ImageView(TAG); - public EventClusterNode(final EventCluster aggEvent, EventClusterNode parentEventNode, EventDetailChart chart) { - this.eventCluster = aggEvent; - descLOD.set(aggEvent.getDescriptionLOD()); - this.parentEventNode = parentEventNode; - this.chart = chart; - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); + public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) { + super(chart, eventCluster, parentEventNode); + minWidthProperty().bind(spanRegion.widthProperty()); + header.setPrefWidth(USE_COMPUTED_SIZE); - final Region region = new Region(); - HBox.setHgrow(region, Priority.ALWAYS); - - final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton); - if (aggEvent.getEventIDsWithHashHits().isEmpty()) { - hashIV.setManaged(false); - hashIV.setVisible(false); - } - if (aggEvent.getEventIDsWithTags().isEmpty()) { - tagIV.setManaged(false); - tagIV.setVisible(false); - } - hBox.setPrefWidth(USE_COMPUTED_SIZE); - hBox.setMinWidth(USE_PREF_SIZE); - hBox.setPadding(new Insets(2, 5, 2, 5)); - hBox.setAlignment(Pos.CENTER_LEFT); - - final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); + final BorderPane borderPane = new BorderPane(subNodePane, header, null, null, null); BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); borderPane.setPrefWidth(USE_COMPUTED_SIZE); getChildren().addAll(spanRegion, borderPane); - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - minWidthProperty().bind(spanRegion.widthProperty()); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - - //set up subnode pane sizing contraints - subNodePane.setPrefHeight(USE_COMPUTED_SIZE); - subNodePane.setMinHeight(USE_PREF_SIZE); - subNodePane.setMinWidth(USE_PREF_SIZE); - subNodePane.setMaxHeight(USE_PREF_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setPickOnBounds(false); - - //setup description label - eventTypeImageView.setImage(aggEvent.getEventType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); - descrLabel.setPrefWidth(USE_COMPUTED_SIZE); - descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - - descrLabel.setMouseTransparent(true); - setDescriptionVisibility(chart.getDescrVisibility().get()); - //setup backgrounds - final Color evtColor = aggEvent.getEventType().getColor(); - spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - setBackground(spanFill); - setCursor(Cursor.HAND); spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS - spanRegion.setBackground(spanFill); + spanRegion.setBackground(getBackground()); - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart - installTooltip(); - spanRegion.setEffect(new DropShadow(10, evtColor)); - minusButton.setVisible(true); - plusButton.setVisible(true); - minusButton.setManaged(true); - plusButton.setManaged(true); - toFront(); - }); - - setOnMouseExited((MouseEvent e) -> { - spanRegion.setEffect(null); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); - }); - - setOnMouseClicked(new EventMouseHandler()); - - plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getDescriptionLOD())); - - plusButton.setOnMouseClicked(e -> { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - }); - minusButton.setOnMouseClicked(e -> { - final DescriptionLOD previous = descLOD.get().previous(); - if (previous != null) { - loadSubClusters(previous); - descLOD.set(previous); - } - }); } - synchronized private void installTooltip() { + @Override + synchronized void installTooltip() { //TODO: all this work should probably go on a background thread... if (tooltip == null) { HashMap hashSetCounts = new HashMap<>(); - if (!eventCluster.getEventIDsWithHashHits().isEmpty()) { + if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) { hashSetCounts = new HashMap<>(); try { - for (TimeLineEvent tle : eventsModel.getEventsById(eventCluster.getEventIDsWithHashHits())) { + for (TimeLineEvent tle : eventsModel.getEventsById(getEventCluster().getEventIDsWithHashHits())) { Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); @@ -298,8 +98,8 @@ public class EventClusterNode extends StackPane implements DetailViewNode tagCounts = new HashMap<>(); - if (!eventCluster.getEventIDsWithTags().isEmpty()) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(eventCluster.getEventIDsWithTags())); + if (!getEventCluster().getEventIDsWithTags().isEmpty()) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventCluster().getEventIDsWithTags())); } @@ -312,9 +112,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(EventClusterNode.class::cast) - .collect(Collectors.toList()); - } - - synchronized public EventCluster getEvent() { - return eventCluster; + synchronized public EventCluster getEventCluster() { + return getEventBundle(); } /** @@ -351,93 +144,9 @@ public class EventClusterNode extends StackPane implements DetailViewNode { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - - @Override - public String getDescription() { - return eventCluster.getDescription(); - } - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - public synchronized void applyHighlightEffect(boolean applied) { - - if (applied) { - descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS - spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); - } else { - descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS - spanFill = new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(eventCluster.getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - } - } - - String getDisplayedDescription() { - return descrLabel.getText(); - } - - double getLayoutXCompensation() { - return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0) - + getBoundsInParent().getMinX(); + Region getSpanFillNode() { + return spanRegion; } /** @@ -454,115 +163,19 @@ public class EventClusterNode extends StackPane implements DetailViewNode> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - - @Override - protected List call() throws Exception { - //query for the sub-clusters - List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, - eventsModel.eventTypeZoomProperty().get(), - combinedFilter, - newDescriptionLOD)); - //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position - return aggregatedEvents.stream().map(aggEvent -> { - EventClusterNode subNode = new EventClusterNode(aggEvent, EventClusterNode.this, chart); - subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation()); - return subNode; - }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters - } - - @Override - protected void succeeded() { - try { - chart.setCursor(Cursor.WAIT); - //assign subNodes and request chart layout - subNodePane.getChildren().setAll(get()); - setDescriptionVisibility(descrVis); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - chart.setCursor(null); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - } - }; - - //start task - chart.getController().monitorTask(loggedTask); - } - } - - /** - * event handler used for mouse events on {@link EventClusterNode}s - */ - private class EventMouseHandler implements EventHandler { - - @Override - public void handle(MouseEvent t) { - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedBundles.contains(eventCluster) == false) { - chart.selectedBundles.add(eventCluster); - } - } else if (t.isShortcutDown()) { - chart.selectedBundles.removeAll(eventCluster); - } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - } else { - chart.selectedBundles.setAll(eventCluster); - } - } - } + @Override + void showSpans(boolean showSpans) { + //no-op for now } @Override - public long getStartMillis() { - return getEvent().getStartMillis(); + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters; } @Override - public long getEndMillis() { - return getEvent().getStartMillis(); + EventClusterNode getNodeForCluser(EventCluster cluster) { + return new EventClusterNode(cluster, this, getChart()); } - @Override - public EventType getEventType() { - return getEvent().getEventType(); - } - - @Override - public Set getEventIDs() { - return getEvent().getEventIDs(); - } - - @Override - public EventBundle getEventBundle() { - return eventCluster; - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 17b38e8e71..5d9bd03eb4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -60,7 +60,6 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Region; import javafx.scene.shape.Line; import javafx.scene.shape.StrokeLineCap; import javafx.util.Duration; @@ -75,7 +74,6 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Forward; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; @@ -175,7 +173,7 @@ public final class EventDetailChart extends XYChart impl @GuardedBy(value = "this") private boolean requiresLayout = true; - final ObservableList selectedBundles; + final ObservableList> selectedNodes; /** * list of series of data added to this chart TODO: replace this with a map @@ -205,7 +203,7 @@ public final class EventDetailChart extends XYChart impl private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); private final SimpleBooleanProperty alternateLayout = new SimpleBooleanProperty(true); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); @@ -284,38 +282,28 @@ public final class EventDetailChart extends XYChart impl setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); -// projectionMap.addListener((MapChangeListener.Change, ? extends Line> change) -> { -// final Line valueRemoved = change.getValueRemoved(); -// if (valueRemoved != null) { -// getChartChildren().removeAll(valueRemoved); -// } -// final Line valueAdded = change.getValueAdded(); -// if (valueAdded != null) { -// getChartChildren().add(valueAdded); -// } -// }); - this.selectedBundles = selectedNodes; - this.selectedBundles.addListener(( - ListChangeListener.Change c) -> { + this.selectedNodes = selectedNodes; + this.selectedNodes.addListener(( + ListChangeListener.Change> c) -> { while (c.next()) { - c.getRemoved().forEach((EventBundle t) -> { - t.getRanges().forEach((Range t1) -> { - Line removeAll = projectionMap.remove(t1); - getChartChildren().removeAll(removeAll); + c.getRemoved().forEach((DetailViewNode t) -> { + t.getEventBundle().getRanges().forEach((Range t1) -> { + Line removedLine = projectionMap.remove(t1); + getChartChildren().removeAll(removedLine); }); }); - c.getAddedSubList().forEach((EventBundle t) -> { + c.getAddedSubList().forEach((DetailViewNode t) -> { - for (Range r : t.getRanges()) { + for (Range range : t.getEventBundle().getRanges()) { - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(r.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.lowerEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.upperEndpoint(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET ); line.setStroke(t.getEventType().getColor().deriveColor(0, 1, 1, .5)); line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(r, line); + projectionMap.put(range, line); getChartChildren().add(line); } }); @@ -353,7 +341,7 @@ public final class EventDetailChart extends XYChart impl clearGuideLine(); clearIntervalSelector(); - selectedBundles.clear(); + selectedNodes.clear(); projectionMap.clear(); controller.selectEventIDs(Collections.emptyList()); }); @@ -600,38 +588,38 @@ public final class EventDetailChart extends XYChart impl * @param nodes * @param minY */ - private synchronized > double layoutNodes(final Collection nodes, final double minY, final double xOffset) { + private synchronized double layoutNodes(final Collection> nodes, final double minY, final double xOffset) { //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot Map maxXatY = new HashMap<>(); double localMax = minY; //for each node lay size it and position it in first available slot - for (DVRegion n : nodes) { - n.setDescriptionVisibility(descrVisibility.get()); - double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(n.getStartMillis())); + for (AbstractDetailViewNode node : nodes) { + node.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.getStartMillis())); //position of start and end according to range of axis double startX = rawDisplayPosition - xOffset; double layoutNodesResultHeight = 0; double span = 0; - List subNodes = n.getSubNodes(); + List> subNodes = node.getSubNodes(); if (subNodes.isEmpty() == false) { - subNodes.sort(Comparator.comparing((DVRegion t) -> t.getStartMillis())); + subNodes.sort(new DetailViewNode.StartTimeComparator()); layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); } - if (n instanceof EventClusterNode) { - double endX = getXAxis().getDisplayPosition(new DateTime(n.getEndMillis())) - xOffset; + if (alternateLayout.get() == false) { + double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset; span = endX - startX; //size timespan border - n.setSpanWidths(Arrays.asList(span)); + node.setSpanWidths(Arrays.asList(span)); } else { - EventStripeNode cn = (EventStripeNode) n; + EventStripeNode stripeNode = (EventStripeNode) node; List spanWidths = new ArrayList<>(); - double x = getXAxis().getDisplayPosition(new DateTime(cn.getStartMillis()));; + double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; double x2; - Iterator> ranges = cn.getStripe().getRanges().iterator(); + Iterator> ranges = stripeNode.getStripe().getRanges().iterator(); Range range = ranges.next(); do { x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); @@ -644,25 +632,31 @@ public final class EventDetailChart extends XYChart impl double gapSpan = x - x2; span += gapSpan; spanWidths.add(gapSpan); + if (ranges.hasNext() == false) { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + } } } while (ranges.hasNext()); - cn.setSpanWidths(spanWidths); + stripeNode.setSpanWidths(spanWidths); } if (truncateAll.get()) { //if truncate option is selected limit width of description label - n.setDescriptionWidth(Math.max(span, truncateWidth.get())); + node.setDescriptionWidth(Math.max(span, truncateWidth.get())); } else { //else set it unbounded - n.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); } - n.autosize(); //compute size of tlNode based on constraints and event data + node.autosize(); //compute size of tlNode based on constraints and event data //get position of right edge of node ( influenced by description label) - double xRight = startX + n.getWidth(); + double xRight = startX + node.getWidth(); //get the height of the node - final double h = layoutNodesResultHeight == 0 ? n.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; //initial test position double yPos = minY; @@ -701,8 +695,8 @@ public final class EventDetailChart extends XYChart impl localMax = Math.max(yPos2, localMax); Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(n.layoutXProperty(), startX), - new KeyValue(n.layoutYProperty(), yPos))); + new KeyValue(node.layoutXProperty(), startX), + new KeyValue(node.layoutYProperty(), yPos))); tm.play(); } @@ -753,15 +747,6 @@ public final class EventDetailChart extends XYChart impl return alternateLayout; } - private static class StartTimeComparator> implements Comparator { - - @Override - public int compare(T n1, T n2) { - return Long.compare(n1.getStartMillis(), n2.getStartMillis() - ); - } - } - private class DetailIntervalSelector extends IntervalSelector { public DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { @@ -797,12 +782,7 @@ public final class EventDetailChart extends XYChart impl super.requestChartLayout(); } - void applySelectionEffect(EventBundle c1, Boolean selected) { - if (alternateLayout.get()) { - EventStripe eventStripe = stripeDescMap.get(ImmutablePair.of(c1.getEventType(), c1.getDescription())); - stripeNodeMap.get(eventStripe).applySelectionEffect(selected); - } else { - clusterNodeMap.get(c1).applySelectionEffect(selected); - } + void applySelectionEffect(DetailViewNode c1, Boolean selected) { + c1.applySelectionEffect(selected); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java index 4e03ff342d..ac79c021a3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -6,235 +6,64 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import com.google.common.collect.Range; -import java.util.HashMap; +import java.util.Collection; import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.OverrunStyle; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.ColorUtilities; -import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TextFilter; -import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; -import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; -import org.sleuthkit.datamodel.SleuthkitCase; +import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show; /** * */ -public class EventStripeNode extends StackPane implements DetailViewNode { +public class EventStripeNode extends AbstractDetailViewNode { private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); - private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS - private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS - private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS - - private final Pane subNodePane = new Pane(); - private final EventStripe eventStripe; - private final EventStripeNode parentNode; - private final EventDetailChart chart; - private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - private final SleuthkitCase sleuthkitCase; - private final FilteredEventsModel eventsModel; - /** - * The label used to display this node's event's description - */ - private final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - private final Label countLabel = new Label(); - - private final ImageView hashIV = new ImageView(HASH_PIN); - private final ImageView tagIV = new ImageView(TAG); - private final Button plusButton = new Button(null, new ImageView(PLUS)) { - { - configureLODButton(this); - } - }; - private final Button minusButton = new Button(null, new ImageView(MINUS)) { - { - configureLODButton(this); - } - }; - - private static void configureLODButton(Button b) { - b.setMinSize(16, 16); - b.setMaxSize(16, 16); - b.setPrefSize(16, 16); - show(b, false); - } - - private static void show(Node b, boolean show) { - b.setVisible(show); - b.setManaged(show); - } - private DescriptionVisibility descrVis; - private final HBox spansHBox = new HBox(); - /** - * The IamgeView used to show the icon for this node's event's type - */ - private final ImageView eventTypeImageView = new ImageView(); - private Background spanFill; - private static final CornerRadii CORNER_RADII = new CornerRadii(3); + private final HBox rangesHBox = new HBox(); EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) { - this.chart = chart; - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - - this.parentNode = parentNode; - this.eventStripe = eventStripe; - descLOD.set(eventStripe.getDescriptionLOD()); - - final Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - final HBox header = new HBox(descrLabel, countLabel, hashIV, tagIV, spacer, minusButton, plusButton); - if (eventStripe.getEventIDsWithHashHits().isEmpty()) { - show(hashIV, false); - } - if (eventStripe.getEventIDsWithTags().isEmpty()) { - show(tagIV, false); - } - header.setMinWidth(USE_PREF_SIZE); - header.setPadding(new Insets(2, 5, 2, 5)); - header.setAlignment(Pos.CENTER_LEFT); - + super(chart, eventStripe, parentNode); + minWidthProperty().bind(rangesHBox.widthProperty()); final VBox internalVBox = new VBox(header, subNodePane); internalVBox.setAlignment(Pos.CENTER_LEFT); - final Color evtColor = eventStripe.getEventType().getColor(); - spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY)); - for (Range r : eventStripe.getRanges()) { - Region spanRegion = new Region(); - spanRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS - spanRegion.setBackground(spanFill); - spansHBox.getChildren().addAll(spanRegion, new Region()); + for (Range range : eventStripe.getRanges()) { + Region rangeRegion = new Region(); + rangeRegion.setStyle("-fx-border-width:2 1 2 1; -fx-border-radius: 1; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor.deriveColor(0, 1, 1, .3)) + ";"); // NON-NLS + rangeRegion.setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); + rangesHBox.getChildren().addAll(rangeRegion, new Region()); } - spansHBox.getChildren().remove(spansHBox.getChildren().size() - 1); - spansHBox.setMaxWidth(USE_PREF_SIZE); + rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1); + rangesHBox.setMaxWidth(USE_PREF_SIZE); setMaxWidth(USE_PREF_SIZE); - getChildren().addAll(spansHBox, internalVBox); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - minWidthProperty().bind(spansHBox.widthProperty()); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - - //set up subnode pane sizing contraints - subNodePane.setPrefHeight(USE_COMPUTED_SIZE); - subNodePane.setMinHeight(USE_PREF_SIZE); - subNodePane.setMinWidth(USE_PREF_SIZE); - subNodePane.setMaxHeight(USE_PREF_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setPickOnBounds(false); - - //setup description label - eventTypeImageView.setImage(eventStripe.getEventType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); - descrLabel.setPrefWidth(USE_COMPUTED_SIZE); - descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - - descrLabel.setMouseTransparent(true); - setDescriptionVisibility(chart.getDescrVisibility().get()); - setOnMouseClicked(new EventMouseHandler()); - - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart -// installTooltip(); - spansHBox.setEffect(new DropShadow(10, evtColor)); - show(spacer, true); - show(minusButton, true); - show(plusButton, true); - - toFront(); - }); - - setOnMouseExited((MouseEvent e) -> { - spansHBox.setEffect(null); - show(spacer, false); - show(minusButton, false); - show(plusButton, false); - - }); - - plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - minusButton.disableProperty().bind(descLOD.isEqualTo(eventStripe.getDescriptionLOD())); - - plusButton.setOnMouseClicked(e -> { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - }); - minusButton.setOnMouseClicked(e -> { - final DescriptionLOD previous = descLOD.get().previous(); - if (previous != null) { - loadSubClusters(previous); - descLOD.set(previous); - } - }); + getChildren().addAll(rangesHBox, internalVBox); } + /** + * + * @param showControls the value of par + */ @Override - public long getStartMillis() { - return eventStripe.getStartMillis(); + void showDescriptionLoDControls(final boolean showControls) { + super.showDescriptionLoDControls(showControls); + show(spacer, showControls); } @Override public void setSpanWidths(List spanWidths) { for (int i = 0; i < spanWidths.size(); i++) { - Region spanRegion = (Region) spansHBox.getChildren().get(i); + Region spanRegion = (Region) rangesHBox.getChildren().get(i); Double w = spanWidths.get(i); spanRegion.setPrefWidth(w); spanRegion.setMaxWidth(w); @@ -242,211 +71,42 @@ public class EventStripeNode extends StackPane implements DetailViewNode getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(EventStripeNode.class::cast) - .collect(Collectors.toList()); + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters.stream().collect( + Collectors.toMap( + EventCluster::getDescription, //key + EventStripe::new, //value + EventStripe::merge)//merge method + ).values(); } /** - * event handler used for mouse events on {@link AggregateEventNode}s - */ - private class EventMouseHandler implements EventHandler { - - @Override - public void handle(MouseEvent t) { - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedBundles.contains(eventStripe) == false) { - chart.selectedBundles.add(eventStripe); - } - } else if (t.isShortcutDown()) { - chart.selectedBundles.removeAll(eventStripe); - } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - } else { - chart.selectedBundles.setAll(eventStripe); - } - } - } - } - - @Override - public EventType getEventType() { - return eventStripe.getEventType(); - } - - @Override - public Set getEventIDs() { - return eventStripe.getEventIDs(); - } - private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - - /** - * apply the 'effect' to visually indicate selection * - * @param applied true to apply the selection 'effect', false to remove it + * @param showSpans the value of showSpans */ @Override - public void applySelectionEffect(boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - @Override - public synchronized void applyHighlightEffect(boolean applied) { - if (applied) { - descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS - spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); - spansHBox.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); - } else { - descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS - spanFill = new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - spansHBox.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(getEventType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - } + void showSpans(final boolean showSpans) { + rangesHBox.setVisible(showSpans); } @Override - public String getDescription() { - return eventStripe.getDescription(); + void installTooltip() { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public EventBundle getEventBundle() { - return getStripe(); + EventStripeNode getNodeForCluser(EventStripe cluster) { + return new EventStripeNode(cluster, this, getChart()); } - /** - * loads sub-clusters at the given Description LOD - * - * @param newDescriptionLOD - */ - synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - subNodePane.getChildren().clear(); - - if (newDescriptionLOD == eventStripe.getDescriptionLOD()) { - spansHBox.setVisible(true); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } else { - spansHBox.setVisible(false); - RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); - //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters - combinedFilter.getSubFilters().addAll(new TextFilter(eventStripe.getDescription()), - new TypeFilter(eventStripe.getEventType())); - - //make a new end inclusive span (to 'filter' with) - final Interval span = new Interval(eventStripe.getStartMillis(), eventStripe.getEndMillis() + 1000); - - //make a task to load the subnodes - LoggedTask> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - - @Override - protected List call() throws Exception { - //query for the sub-clusters - List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, - eventsModel.eventTypeZoomProperty().get(), - combinedFilter, - newDescriptionLOD)); - //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position - HashMap stripeDescMap = new HashMap<>(); - for (EventCluster subCluster : aggregatedEvents) { - stripeDescMap.merge(subCluster.getDescription(), - new EventStripe(subCluster), - (EventStripe u, EventStripe v) -> { - return EventStripe.merge(u, v); - } - ); - } - - return stripeDescMap.values().stream().map(subStripe -> { - EventStripeNode subNode = new EventStripeNode(subStripe, EventStripeNode.this, chart); - subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(subStripe.getStartMillis())) - getLayoutXCompensation()); - return subNode; - }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters - } - - @Override - protected void succeeded() { - try { - chart.setCursor(Cursor.WAIT); - //assign subNodes and request chart layout - subNodePane.getChildren().setAll(get()); - setDescriptionVisibility(descrVis); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - chart.setCursor(null); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - } - }; - - //start task - chart.getController().monitorTask(loggedTask); - } - } - - double getLayoutXCompensation() { - return (parentNode != null ? parentNode.getLayoutXCompensation() : 0) - + getBoundsInParent().getMinX(); - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 898917ae15..9971291863 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -41,9 +41,9 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; -import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewNode; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** @@ -91,8 +91,8 @@ public class NavPanel extends BorderPane implements TimeLineView { }); detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach((EventBundle t) -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t)); + detailViewPane.getSelectedNodes().forEach((DetailViewNode t) -> { + eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEventBundle())); }); }); @@ -131,7 +131,7 @@ public class NavPanel extends BorderPane implements TimeLineView { sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count)); sortByBox.getSelectionModel().select(TreeComparator.Description); sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { - ((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem()); + ((NavTreeItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem()); }); eventsTree.setShowRoot(false); eventsTree.setCellFactory((TreeView p) -> new EventTreeCell()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index e87b10cc17..cfd582433e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -20,8 +20,8 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; /** * A node in the nav tree. Manages inserts and resorts. Has parents and From 40d1ec9a5e10dacd5ce60f308e6913e37838af0e Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 15 Sep 2015 13:14:07 -0400 Subject: [PATCH 05/15] create some additional indexes; don't use group_concat uselessly --- .../autopsy/timeline/db/EventDB.java | 23 ++++++++++--------- .../autopsy/timeline/db/EventsRepository.java | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index ec72314dc4..be9d89d522 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -413,10 +413,7 @@ public class EventDB { try (ResultSet rs = getDataSourceIDsStmt.executeQuery()) { while (rs.next()) { long datasourceID = rs.getLong("datasource_id"); - //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. - if (datasourceID != 0) { - hashSet.add(datasourceID); - } + hashSet.add(datasourceID); } } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS @@ -583,6 +580,10 @@ public class EventDB { initializeTagsTable(); + createIndex("events", Arrays.asList("datasource_id")); + createIndex("events", Arrays.asList("event_id", "hash_hit")); + createIndex("events", Arrays.asList("event_id", "tagged")); + createIndex("events", Arrays.asList("file_id")); createIndex("events", Arrays.asList("file_id")); createIndex("events", Arrays.asList("artifact_id")); createIndex("events", Arrays.asList("time")); @@ -595,7 +596,7 @@ public class EventDB { "INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit, tagged) " // NON-NLS + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS getHashSetNamesStmt = prepareStatement("SELECT hash_set_id, hash_set_name FROM hash_sets"); // NON-NLS - getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events"); // NON-NLS + getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events WHERE datasource_id != 0"); // NON-NLS getMaxTimeStmt = prepareStatement("SELECT Max(time) AS max FROM events"); // NON-NLS getMinTimeStmt = prepareStatement("SELECT Min(time) AS min FROM events"); // NON-NLS getEventByIDStmt = prepareStatement("SELECT * FROM events WHERE event_id = ?"); // NON-NLS @@ -1041,7 +1042,7 @@ public class EventDB { * the supplied filter, aggregated according to the given event type * and description zoom levels */ - List getAggregatedEvents(ZoomParams params) { + List getClusteredEvents(ZoomParams params) { //unpack params Interval timeRange = params.getTimeRange(); RootFilter filter = params.getFilter(); @@ -1083,7 +1084,7 @@ public class EventDB { try (Statement createStatement = con.createStatement(); ResultSet rs = createStatement.executeQuery(query)) { while (rs.next()) { - events.add(aggregateEventHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter())); + events.add(eventClusterHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter())); } } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "Failed to get aggregate events with query: " + query, ex); // NON-NLS @@ -1091,11 +1092,11 @@ public class EventDB { DBLock.unlock(); } - return mergeAggregateEvents(rangeInfo.getPeriodSize().getPeriod(), events); + return mergeEventClusters(rangeInfo.getPeriodSize().getPeriod(), events); } /** - * map a single row in a ResultSet to an AggregateEvent + * map a single row in a ResultSet to an EventCluster * * @param rs the result set whose current row should be mapped * @param useSubTypes use the sub_type column if true, else use the @@ -1107,7 +1108,7 @@ public class EventDB { * * @throws SQLException */ - private EventCluster aggregateEventHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException { + 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); @@ -1134,7 +1135,7 @@ public class EventDB { * * @return */ - static private List mergeAggregateEvents(Period timeUnitLength, List preMergedEvents) { + static private List mergeEventClusters(Period timeUnitLength, List preMergedEvents) { //effectively map from type to (map from description to events) Map> typeMap = new HashMap<>(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 1a9e61c2d1..bae051b66e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -149,7 +149,7 @@ public class EventsRepository { aggregateEventsCache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterAccess(10, TimeUnit.MINUTES - ).build(CacheLoader.from(eventDB::getAggregatedEvents)); + ).build(CacheLoader.from(eventDB::getClusteredEvents)); maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime)); minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime)); this.modelInstance = new FilteredEventsModel(this, currentStateProperty); From eadd7266edb7e7ad91c46c8d38e0ec06159f8aeb Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 15 Sep 2015 16:11:30 -0400 Subject: [PATCH 06/15] use ControlsFX Actions to add expand/collapse menu items to AbstractDetailViewNode --- .../ui/detailview/AbstractDetailViewNode.java | 226 +++++++++++------- .../ui/detailview/EventDetailChart.java | 76 +++--- .../timeline/ui/detailview/GuideLine.java | 5 +- 3 files changed, 175 insertions(+), 132 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java index 743e49b476..56a0eca60c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -14,14 +14,17 @@ import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; +import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.effect.DropShadow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; @@ -43,6 +46,8 @@ import static javafx.scene.layout.Region.USE_PREF_SIZE; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import org.apache.commons.lang3.StringUtils; +import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; import org.joda.time.DateTime; import org.joda.time.Interval; import org.openide.util.NbBundle; @@ -66,7 +71,12 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS static final CornerRadii CORNER_RADII = new CornerRadii(3); - Map dropShadowMap = new HashMap<>(); + /** + * the border to apply when this node is 'selected' + */ + static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); + private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class + .getName()); static void configureLODButton(Button b) { b.setMinSize(16, 16); @@ -74,42 +84,14 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A b.setPrefSize(16, 16); show(b, false); } - /** - * the border to apply when this node is 'selected' - */ - static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - final Color evtColor; - - @Override - @SuppressWarnings("unchecked") - public List getSubNodes() { - return subNodePane.getChildrenUnmodifiable().stream() - .map(t -> (S) t) - .collect(Collectors.toList()); - } - - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - @Override - public void applySelectionEffect(boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } static void show(Node b, boolean show) { b.setVisible(show); b.setManaged(show); } - final ImageView hashIV = new ImageView(HASH_PIN); - final ImageView tagIV = new ImageView(TAG); + Map dropShadowMap = new HashMap<>(); + final Color evtColor; + private final S parentNode; DescriptionVisibility descrVis; @@ -142,45 +124,16 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A final SleuthkitCase sleuthkitCase; final FilteredEventsModel eventsModel; - final Button plusButton = new Button(null, new ImageView(PLUS)) { - { - configureLODButton(this); - } + final Button plusButton; + final Button minusButton; - }; - - final Button minusButton = new Button(null, new ImageView(MINUS)) { - { - configureLODButton(this); - } - }; - SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); final HBox header; - /** - * - * @param showControls the value of par - */ - void showDescriptionLoDControls(final boolean showControls) { - DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), - eventType -> new DropShadow(10, eventType.getColor())); - getSpanFillNode().setEffect(showControls ? dropShadow : null); - show(minusButton, showControls); - show(plusButton, showControls); - } final Region spacer = new Region(); - RootFilter getSubClusterFilter() { - RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); - //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters - combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()), - new TypeFilter(getEventType())); - return combinedFilter; - } - - abstract Collection makeBundlesFromClusters(List eventClusters); - - abstract void showSpans(final boolean showSpans); + private final CollapseClusterAction collapseClusterAction; + private final ExpandClusterAction expandClusterAction; public AbstractDetailViewNode(EventDetailChart chart, T bundle, S parentEventNode) { this.eventBundle = bundle; @@ -189,21 +142,34 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A descLOD.set(bundle.getDescriptionLOD()); sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); eventsModel = chart.getController().getEventsModel(); - + ImageView hashIV = new ImageView(HASH_PIN); + ImageView tagIV = new ImageView(TAG); if (eventBundle.getEventIDsWithHashHits().isEmpty()) { show(hashIV, false); } if (eventBundle.getEventIDsWithTags().isEmpty()) { show(tagIV, false); } + + expandClusterAction = new ExpandClusterAction(); + plusButton = ActionUtils.createButton(expandClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLODButton(plusButton); + + collapseClusterAction = new CollapseClusterAction(); + minusButton = ActionUtils.createButton(collapseClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLODButton(minusButton); + HBox.setHgrow(spacer, Priority.ALWAYS); - header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /*spacer,*/ minusButton, plusButton); + header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /* + * spacer, + */ minusButton, plusButton); header.setMinWidth(USE_PREF_SIZE); header.setPadding(new Insets(2, 5, 2, 5)); header.setAlignment(Pos.CENTER_LEFT); //setup description label evtColor = getEventType().getColor(); + eventTypeImageView.setImage(getEventType().getFXImage()); descrLabel.setGraphic(eventTypeImageView); descrLabel.setPrefWidth(USE_COMPUTED_SIZE); @@ -220,7 +186,6 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A setAlignment(Pos.TOP_LEFT); setMinHeight(24); - setPrefHeight(USE_COMPUTED_SIZE); setMaxHeight(USE_PREF_SIZE); setOnMouseClicked(new EventMouseHandler()); @@ -238,29 +203,59 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A }); setCursor(Cursor.HAND); - plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - minusButton.disableProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); - - plusButton.setOnMouseClicked(e -> { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - }); - minusButton.setOnMouseClicked(e -> { - final DescriptionLOD previous = descLOD.get().previous(); - if (previous != null) { - loadSubClusters(previous); - descLOD.set(previous); - } - }); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); setLayoutX(getChart().getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); } + @Override + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodePane.getChildrenUnmodifiable().stream() + .map(t -> (S) t) + .collect(Collectors.toList()); + } + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + @Override + public void applySelectionEffect(boolean applied) { + Platform.runLater(() -> { + if (applied) { + setBorder(selectionBorder); + } else { + setBorder(null); + } + }); + } + + /** + * + * @param showControls the value of par + */ + void showDescriptionLoDControls(final boolean showControls) { + DropShadow dropShadow = dropShadowMap.computeIfAbsent(getEventType(), + eventType -> new DropShadow(10, eventType.getColor())); + getSpanFillNode().setEffect(showControls ? dropShadow : null); + show(minusButton, showControls); + show(plusButton, showControls); + } + + RootFilter getSubClusterFilter() { + RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); + //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters + combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()), + new TypeFilter(getEventType())); + return combinedFilter; + } + + abstract Collection makeBundlesFromClusters(List eventClusters); + + abstract void showSpans(final boolean showSpans); + /** * @param w the maximum width the description label should have */ @@ -307,7 +302,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A return descrLabel; } - public final Label getCountLabel() { + final public Label getCountLabel() { return countLabel; } @@ -327,7 +322,6 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A public DescriptionLOD getDescLOD() { return descLOD.get(); } - private static final Logger LOGGER = Logger.getLogger(AbstractDetailViewNode.class.getName()); /** * loads sub-clusters at the given Description LOD @@ -358,7 +352,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A eventsModel.eventTypeZoomProperty().get(), combinedFilter, newDescriptionLOD)); - + return makeBundlesFromClusters(aggregatedEvents).stream() .map(aggEvent -> { return getNodeForCluser(aggEvent); @@ -392,7 +386,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } @Override - final public void setDescriptionVisibility(DescriptionVisibility descrVis) { + public final void setDescriptionVisibility(DescriptionVisibility descrVis) { this.descrVis = descrVis; final int size = getEventBundle().getEventIDs().size(); @@ -417,11 +411,15 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } } + abstract S getNodeForCluser(T cluster); + /** * event handler used for mouse events on {@link AggregateEventNode}s */ private class EventMouseHandler implements EventHandler { + private ContextMenu contextMenu; + @Override public void handle(MouseEvent t) { @@ -442,11 +440,55 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } else { chart.selectedNodes.setAll(AbstractDetailViewNode.this); } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + contextMenu.getItems().add(ActionUtils.createMenuItem(expandClusterAction)); + contextMenu.getItems().add(ActionUtils.createMenuItem(collapseClusterAction)); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(AbstractDetailViewNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); } } } - abstract S getNodeForCluser(T cluster); + private class ExpandClusterAction extends Action { + public ExpandClusterAction() { + super("Expand"); + setGraphic(new ImageView(PLUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLOD next = descLOD.get().next(); + if (next != null) { + loadSubClusters(next); + descLOD.set(next); + } + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + } + } + + private class CollapseClusterAction extends Action { + + public CollapseClusterAction() { + super("Collapse"); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLOD previous = descLOD.get().previous(); + if (previous != null) { + loadSubClusters(previous); + descLOD.set(previous); + } + }); + disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 5d9bd03eb4..ae826dd5e5 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.MissingResourceException; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -106,7 +107,6 @@ public final class EventDetailChart extends XYChart impl */ private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); - // I don't like having these package visible, but it was the easiest way to private ContextMenu chartContextMenu; private TimeLineController controller; @@ -239,36 +239,7 @@ public final class EventDetailChart extends XYChart impl chartContextMenu.hide(); } if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action( - NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) { - { - setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS - setEventHandler((ActionEvent t) -> { - if (guideLine == null) { - guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis); - guideLine.relocate(clickEvent.getX(), 0); - guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty()))); - - getChartChildren().add(guideLine); - - guideLine.setOnMouseClicked((MouseEvent event) -> { - if (event.getButton() == MouseButton.SECONDARY) { - clearGuideLine(); - event.consume(); - } - }); - } else { - guideLine.relocate(clickEvent.getX(), 0); - } - }); - } - - }, new ActionGroup( - NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"), - new Back(controller), - new Forward(controller)))); - chartContextMenu.setAutoHide(true); + getChartContextMenu(clickEvent); chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY()); clickEvent.consume(); } @@ -317,6 +288,42 @@ public final class EventDetailChart extends XYChart impl requestChartLayout(); } + ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action( + NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) { + { + setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS + setEventHandler((ActionEvent t) -> { + if (guideLine == null) { + guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); + guideLine.relocate(clickEvent.getX(), 0); + guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); + + getChartChildren().add(guideLine); + + guideLine.setOnMouseClicked((MouseEvent event) -> { + if (event.getButton() == MouseButton.SECONDARY) { + clearGuideLine(); + event.consume(); + } + }); + } else { + guideLine.relocate(clickEvent.getX(), 0); + } + }); + } + + }, new ActionGroup( + NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"), + new Back(controller), + new Forward(controller)))); + chartContextMenu.setAutoHide(true); + return chartContextMenu; + } + @Override public void clearIntervalSelector() { getChartChildren().remove(intervalSelector); @@ -736,13 +743,6 @@ public final class EventDetailChart extends XYChart impl return filteredEvents; } - /** - * @return the chartContextMenu - */ - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - Property alternateLayoutProperty() { return alternateLayout; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index ab8b65cd00..3a2dad163b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import javafx.scene.Cursor; +import javafx.scene.chart.Axis; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; @@ -32,7 +33,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; */ class GuideLine extends Line { - private final DateAxis dateAxis; + private final Axis dateAxis; private double startLayoutX; @@ -40,7 +41,7 @@ class GuideLine extends Line { private double dragStartX = 0; - GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) { + GuideLine(double startX, double startY, double endX, double endY, Axis axis) { super(startX, startY, endX, endY); dateAxis = axis; setCursor(Cursor.E_RESIZE); From 3c0f270ab0cf40cd01d06a5c3253f489dc6284de Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Sep 2015 14:30:06 -0400 Subject: [PATCH 07/15] add a description filter and use it rather than the looser text filter to do sub clustering --- .../autopsy/timeline/db/SQLHelper.java | 21 +++++++- .../timeline/filters/DescriptionFilter.java | 53 +++++++++++++++++++ .../autopsy/timeline/filters/Filter.java | 2 + .../ui/detailview/AbstractDetailViewNode.java | 6 +-- 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java index d807bcc741..9b25bd401b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java @@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.HashHitsFilter; import org.sleuthkit.autopsy.timeline.filters.HashSetFilter; @@ -105,10 +106,20 @@ public class SQLHelper { return getSQLWhere((IntersectionFilter) filter); } + /** + * NOTE: I don't like this if-else instance of chain, but I can't decide + * what to do instead -jm + * + * @param filter + * + * @return + */ private static String getSQLWhere(Filter filter) { String result = ""; if (filter == null) { return "1"; + } else if (filter instanceof DescriptionFilter) { + result = getSQLWhere((DescriptionFilter) filter); } else if (filter instanceof TagsFilter) { result = getSQLWhere((TagsFilter) filter); } else if (filter instanceof HashHitsFilter) { @@ -130,7 +141,7 @@ public class SQLHelper { } else if (filter instanceof UnionFilter) { result = getSQLWhere((UnionFilter) filter); } else { - return "1"; + throw new IllegalArgumentException("getSQLWhere not defined for " + filter.getClass().getCanonicalName()); } result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result; result = StringUtils.deleteWhitespace(result).equals("()") ? "1" : result; @@ -145,6 +156,14 @@ public class SQLHelper { } } + private static String getSQLWhere(DescriptionFilter filter) { + if (filter.isSelected()) { + return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + " LIKE '" + filter.getDescription() + "')"; // NON-NLS + } else { + return "1"; + } + } + private static String getSQLWhere(TagsFilter filter) { if (filter.isSelected() && (false == filter.isDisabled()) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java new file mode 100644 index 0000000000..8ee273f50a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java @@ -0,0 +1,53 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.timeline.filters; + +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; + +public class DescriptionFilter extends AbstractFilter { + + private final DescriptionLOD descriptionLoD; + + private final String description; + + public DescriptionFilter(DescriptionLOD descriptionLoD, String description) { + this.descriptionLoD = descriptionLoD; + this.description = description; + } + + @Override + public DescriptionFilter copyOf() { + DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription()); + filterCopy.setSelected(isSelected()); + filterCopy.setDisabled(isDisabled()); + return filterCopy; + } + + @Override + public String getDisplayName() { + return "description"; + } + + @Override + public String getHTMLReportString() { + return getDescriptionLoD().getDisplayName() + " " + getDisplayName() + " = " + getDescription(); + } + + /** + * @return the descriptionLoD + */ + public DescriptionLOD getDescriptionLoD() { + return descriptionLoD; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java index 95226e9371..709bf116d4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java @@ -79,4 +79,6 @@ public interface Filter { SimpleBooleanProperty getDisabledProperty(); boolean isDisabled(); + + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java index 56a0eca60c..0a720564c9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -57,8 +57,8 @@ import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; @@ -246,8 +246,8 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A RootFilter getSubClusterFilter() { RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); - //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters - combinedFilter.getSubFilters().addAll(new TextFilter(getEventBundle().getDescription()), + //make a new filter intersecting the global filter with description and type filters to restrict sub-clusters + combinedFilter.getSubFilters().addAll(new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()), new TypeFilter(getEventType())); return combinedFilter; } From 4dacbbe08347e4780aa93ae9c13816fdc9b8300e Mon Sep 17 00:00:00 2001 From: jmillman Date: Wed, 16 Sep 2015 17:02:09 -0400 Subject: [PATCH 08/15] fix description generation WIP --- .../autopsy/timeline/db/EventsRepository.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index bae051b66e..1e9571f144 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -487,26 +487,28 @@ public class EventsRepository { final String uniquePath = f.getUniquePath(); final String parentPath = f.getParentPath(); long datasourceID = f.getDataSource().getId(); - String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath); - String rootFolder = StringUtils.substringBetween(parentPath, "/", "/"); - String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, ""); - String medD = datasourceName + parentPath; + String datasourceName = StringUtils.substringBeforeLast(uniquePath, parentPath); + + String rootFolder = StringUtils.substringBefore(StringUtils.substringAfter(parentPath, "/"), "/"); + String shortDesc = datasourceName + "/" + StringUtils.defaultString(rootFolder); + String medDesc = datasourceName + parentPath; + final TskData.FileKnown known = f.getKnown(); Set hashSets = f.getHashSetNames(); List tags = tagsManager.getContentTagsByContent(f); //insert it into the db if time is > 0 => time is legitimate (drops logical files) if (f.getAtime() > 0) { - eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getMtime() > 0) { - eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getCtime() > 0) { - eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getCrtime() > 0) { - eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } publish(new ProgressWindow.ProgressUpdate(i, numFiles, From ce930506bb6cbd374e5cb8a38432237df87e3f2d Mon Sep 17 00:00:00 2001 From: jmillman Date: Thu, 17 Sep 2015 14:00:34 -0400 Subject: [PATCH 09/15] make sure short description (root folder) ends with "/" --- Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 1e9571f144..6248b99a97 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -491,6 +491,7 @@ public class EventsRepository { String rootFolder = StringUtils.substringBefore(StringUtils.substringAfter(parentPath, "/"), "/"); String shortDesc = datasourceName + "/" + StringUtils.defaultString(rootFolder); + shortDesc = shortDesc.endsWith("/") ? shortDesc : shortDesc + "/"; String medDesc = datasourceName + parentPath; final TskData.FileKnown known = f.getKnown(); From bd1e9a58d83fa5299736cb07a89742308c7a027d Mon Sep 17 00:00:00 2001 From: jmillman Date: Thu, 17 Sep 2015 14:06:29 -0400 Subject: [PATCH 10/15] fix description generation and clustering - rename getAggregatredEvents to getEventClusters - make more members private - improve logic to get subclusters - introduce notion of RelativeDetail for navigating DescriptionLoDs --- .../datamodel/FilteredEventsModel.java | 6 +- .../autopsy/timeline/db/EventsRepository.java | 12 +- .../ui/detailview/AbstractDetailViewNode.java | 178 ++++++++++++------ .../ui/detailview/EventClusterNode.java | 12 +- .../ui/detailview/EventStripeNode.java | 10 +- .../timeline/zooming/DescriptionLOD.java | 26 ++- 6 files changed, 160 insertions(+), 84 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 80aa51d571..664ae74ddd 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -337,7 +337,7 @@ public final class FilteredEventsModel { zoom = requestedTypeZoom.get(); lod = requestedLOD.get(); } - return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod)); + return repo.getEventClusters(new ZoomParams(range, zoom, filter, lod)); } /** @@ -347,8 +347,8 @@ public final class FilteredEventsModel { * range and pass the requested filter, using the given aggregation * to control the grouping of events */ - public List getAggregatedEvents(ZoomParams params) { - return repo.getAggregatedEvents(params); + public List getEventClusters(ZoomParams params) { + return repo.getEventClusters(params); } synchronized public boolean handleContentTagAdded(ContentTagAddedEvent evt) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 6248b99a97..c2a03545a6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -98,7 +98,7 @@ public class EventsRepository { private final LoadingCache idToEventCache; private final LoadingCache> eventCountsCache; - private final LoadingCache> aggregateEventsCache; + private final LoadingCache> eventClusterCache; private final ObservableMap datasourcesMap = FXCollections.observableHashMap(); private final ObservableMap hashSetMap = FXCollections.observableHashMap(); @@ -146,7 +146,7 @@ public class EventsRepository { .maximumSize(1000L) .expireAfterAccess(10, TimeUnit.MINUTES) .build(CacheLoader.from(eventDB::countEventsByType)); - aggregateEventsCache = CacheBuilder.newBuilder() + eventClusterCache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterAccess(10, TimeUnit.MINUTES ).build(CacheLoader.from(eventDB::getClusteredEvents)); @@ -206,8 +206,8 @@ public class EventsRepository { } - synchronized public List getAggregatedEvents(ZoomParams params) { - return aggregateEventsCache.getUnchecked(params); + synchronized public List getEventClusters(ZoomParams params) { + return eventClusterCache.getUnchecked(params); } synchronized public Map countEvents(ZoomParams params) { @@ -218,7 +218,7 @@ public class EventsRepository { minCache.invalidateAll(); maxCache.invalidateAll(); eventCountsCache.invalidateAll(); - aggregateEventsCache.invalidateAll(); + eventClusterCache.invalidateAll(); idToEventCache.invalidateAll(); } @@ -292,7 +292,7 @@ public class EventsRepository { synchronized private void invalidateCaches(Set updatedEventIDs) { eventCountsCache.invalidateAll(); - aggregateEventsCache.invalidateAll(); + eventClusterCache.invalidateAll(); idToEventCache.invalidateAll(updatedEventIDs); try { tagNames.setAll(autoCase.getSleuthkitCase().getTagNamesInUse()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java index 0a720564c9..e52cf31217 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -1,14 +1,29 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Autopsy Forensic Browser + * + * Copyright 2015 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.detailview; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import static java.util.Objects.nonNull; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.stream.Collectors; @@ -61,6 +76,7 @@ import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -89,11 +105,11 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A b.setVisible(show); b.setManaged(show); } - Map dropShadowMap = new HashMap<>(); + private final Map dropShadowMap = new HashMap<>(); final Color evtColor; private final S parentNode; - DescriptionVisibility descrVis; + private DescriptionVisibility descrVis; /** * Pane that contains AggregateEventNodes of any 'subevents' if they are @@ -102,7 +118,11 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A * //TODO: move more of the control of subnodes/events here and out of * EventDetail Chart */ - final Pane subNodePane = new Pane(); + private final Pane subNodePane = new Pane(); + + Pane getSubNodePane() { + return subNodePane; + } /** * The ImageView used to show the icon for this node's event's type @@ -121,16 +141,28 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A private final T eventBundle; private final EventDetailChart chart; - final SleuthkitCase sleuthkitCase; - final FilteredEventsModel eventsModel; + private final SleuthkitCase sleuthkitCase; - final Button plusButton; - final Button minusButton; + SleuthkitCase getSleuthkitCase() { + return sleuthkitCase; + } - final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + FilteredEventsModel getEventsModel() { + return eventsModel; + } + private final FilteredEventsModel eventsModel; + + private final Button plusButton; + private final Button minusButton; + + private final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); final HBox header; - final Region spacer = new Region(); + Region getSpacer() { + return spacer; + } + + private final Region spacer = new Region(); private final CollapseClusterAction collapseClusterAction; private final ExpandClusterAction expandClusterAction; @@ -160,9 +192,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A configureLODButton(minusButton); HBox.setHgrow(spacer, Priority.ALWAYS); - header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, /* - * spacer, - */ minusButton, plusButton); + header = new HBox(getDescrLabel(), getCountLabel(), hashIV, tagIV, minusButton, plusButton); header.setMinWidth(USE_PREF_SIZE); header.setPadding(new Insets(2, 5, 2, 5)); @@ -244,12 +274,17 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A show(plusButton, showControls); } + /** + * make a new filter intersecting the global filter with description and + * type filters to restrict sub-clusters + * + */ RootFilter getSubClusterFilter() { - RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); - //make a new filter intersecting the global filter with description and type filters to restrict sub-clusters - combinedFilter.getSubFilters().addAll(new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()), + RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); + subClusterFilter.getSubFilters().addAll( + new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()), new TypeFilter(getEventType())); - return combinedFilter; + return subClusterFilter; } abstract Collection makeBundlesFromClusters(List eventClusters); @@ -324,59 +359,84 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } /** - * loads sub-clusters at the given Description LOD + * loads sub-bundles at the given Description LOD, continues * - * @param newDescriptionLOD + * @param requestedDescrLoD + * @param expand */ - final synchronized void loadSubClusters(DescriptionLOD newDescriptionLOD) { + private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { subNodePane.getChildren().clear(); - if (newDescriptionLOD == getEventBundle().getDescriptionLOD()) { + if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) { + descLOD.set(getEventBundle().getDescriptionLOD()); showSpans(true); - getChart().setRequiresLayout(true); - getChart().requestChartLayout(); + chart.setRequiresLayout(true); + chart.requestChartLayout(); } else { showSpans(false); - RootFilter combinedFilter = getSubClusterFilter(); - //make a new end inclusive span (to 'filter' with) - final Interval span = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); + // make new ZoomParams to query with + final RootFilter subClusterFilter = getSubClusterFilter(); + /* + * We need to extend end time because for the query by one second, + * because it is treated as an open interval but we want to include + * events at exactly the time of the last event in this cluster + */ + final Interval subClusterSpan = new Interval(getEventBundle().getStartMillis(), getEventBundle().getEndMillis() + 1000); + final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescLOD()); - //make a task to load the subnodes - LoggedTask> loggedTask = new LoggedTask>( + LoggedTask> loggedTask; + loggedTask = new LoggedTask>( NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { + private Collection bundles; + private volatile DescriptionLOD loadedDescriptionLoD = getDescLOD().withRelativeDetail(relativeDetail); + private DescriptionLOD next = loadedDescriptionLoD; @Override protected List call() throws Exception { - //query for the sub-clusters - List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, - eventsModel.eventTypeZoomProperty().get(), - combinedFilter, - newDescriptionLOD)); + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) { + return Collections.emptyList(); + } + bundles = loadBundles(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); - return makeBundlesFromClusters(aggregatedEvents).stream() - .map(aggEvent -> { - return getNodeForCluser(aggEvent); - }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters + // return list of AbstractDetailViewNodes representing sub-bundles + return bundles.stream() + .map(AbstractDetailViewNode.this::getNodeForBundle) + .collect(Collectors.toList()); + } + + private Collection loadBundles() { + return makeBundlesFromClusters(eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD))); } @Override protected void succeeded() { + chart.setCursor(Cursor.WAIT); try { - getChart().setCursor(Cursor.WAIT); + List subBundleNodes = get(); + if (subBundleNodes.isEmpty()) { + showSpans(true); + } else { + showSpans(false); + } + descLOD.set(loadedDescriptionLoD); //assign subNodes and request chart layout - subNodePane.getChildren().setAll(get()); - setDescriptionVisibility(descrVis); - getChart().setRequiresLayout(true); - getChart().requestChartLayout(); - getChart().setCursor(null); + subNodePane.getChildren().setAll(subBundleNodes); + chart.setRequiresLayout(true); + chart.requestChartLayout(); } catch (InterruptedException | ExecutionException ex) { LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); } + chart.setCursor(null); } }; //start task - getChart().getController().monitorTask(loggedTask); + chart.getController().monitorTask(loggedTask); } } @@ -390,7 +450,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A this.descrVis = descrVis; final int size = getEventBundle().getEventIDs().size(); - switch (descrVis) { + switch (this.descrVis) { case COUNT_ONLY: descrLabel.setText(""); countLabel.setText(String.valueOf(size)); @@ -411,7 +471,7 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } } - abstract S getNodeForCluser(T cluster); + abstract S getNodeForBundle(T bundle); /** * event handler used for mouse events on {@link AggregateEventNode}s @@ -432,10 +492,10 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A } else if (t.isShortcutDown()) { chart.selectedNodes.removeAll(AbstractDetailViewNode.this); } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().next(); + final DescriptionLOD next = descLOD.get().moreDetailed(); if (next != null) { - loadSubClusters(next); - descLOD.set(next); + loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + } } else { chart.selectedNodes.setAll(AbstractDetailViewNode.this); @@ -461,14 +521,15 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A private class ExpandClusterAction extends Action { - public ExpandClusterAction() { + ExpandClusterAction() { super("Expand"); + setGraphic(new ImageView(PLUS)); setEventHandler((ActionEvent t) -> { - final DescriptionLOD next = descLOD.get().next(); + final DescriptionLOD next = descLOD.get().moreDetailed(); if (next != null) { - loadSubClusters(next); - descLOD.set(next); + loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + } }); disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); @@ -477,15 +538,14 @@ public abstract class AbstractDetailViewNode< T extends EventBundle, S extends A private class CollapseClusterAction extends Action { - public CollapseClusterAction() { + CollapseClusterAction() { super("Collapse"); setGraphic(new ImageView(MINUS)); setEventHandler((ActionEvent t) -> { - final DescriptionLOD previous = descLOD.get().previous(); + final DescriptionLOD previous = descLOD.get().lessDetailed(); if (previous != null) { - loadSubClusters(previous); - descLOD.set(previous); + loadSubBundles(DescriptionLOD.RelativeDetail.LESS); } }); disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java index b07adac60b..7a3dfc3ac3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -66,8 +66,8 @@ public class EventClusterNode extends AbstractDetailViewNode(); try { - for (TimeLineEvent tle : eventsModel.getEventsById(getEventCluster().getEventIDsWithHashHits())) { - Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); + for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) { + Set hashSetNames = getSleuthkitCase().getAbstractFileById(tle.getFileID()).getHashSetNames(); for (String hashSetName : hashSetNames) { hashSetCounts.merge(hashSetName, 1L, Long::sum); } @@ -99,7 +99,7 @@ public class EventClusterNode extends AbstractDetailViewNode tagCounts = new HashMap<>(); if (!getEventCluster().getEventIDsWithTags().isEmpty()) { - tagCounts.putAll(eventsModel.getTagCountsByTagName(getEventCluster().getEventIDsWithTags())); + tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().getEventIDsWithTags())); } @@ -174,7 +174,7 @@ public class EventClusterNode extends AbstractDetailViewNode { - private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); - private final HBox rangesHBox = new HBox(); EventStripeNode(EventStripe eventStripe, EventStripeNode parentNode, EventDetailChart chart) { super(chart, eventStripe, parentNode); minWidthProperty().bind(rangesHBox.widthProperty()); - final VBox internalVBox = new VBox(header, subNodePane); + final VBox internalVBox = new VBox(header, getSubNodePane()); internalVBox.setAlignment(Pos.CENTER_LEFT); for (Range range : eventStripe.getRanges()) { @@ -57,7 +54,7 @@ public class EventStripeNode extends AbstractDetailViewNode Date: Thu, 17 Sep 2015 16:18:31 -0400 Subject: [PATCH 11/15] fix guideLine placement when action is initiated over a DetailEventNode --- .../autopsy/timeline/ui/detailview/EventDetailChart.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index ae826dd5e5..3d7f7bc017 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -299,7 +299,8 @@ public final class EventDetailChart extends XYChart impl setEventHandler((ActionEvent t) -> { if (guideLine == null) { guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); - guideLine.relocate(clickEvent.getX(), 0); + + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); getChartChildren().add(guideLine); @@ -311,7 +312,7 @@ public final class EventDetailChart extends XYChart impl } }); } else { - guideLine.relocate(clickEvent.getX(), 0); + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); } }); } From 844bf393f0db73f4e4b50c649537c7f50cd81bca Mon Sep 17 00:00:00 2001 From: jmillman Date: Tue, 22 Sep 2015 11:41:16 -0400 Subject: [PATCH 12/15] resolve potentially ambiguous column in db ambiguous --- Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index be9d89d522..6b19122c8d 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -1068,8 +1068,8 @@ public class EventDB { //compose query string, new-lines only for nicer formatting if printing the entire query String query = "SELECT strftime('" + strfTimeFormat + "',time , 'unixepoch'" + timeZone + ") AS interval," // NON-NLS + "\n group_concat(events.event_id) as event_ids," - + "\n group_concat(CASE WHEN hash_hit = 1 THEN event_id ELSE NULL END) as hash_hits," - + "\n group_concat(CASE WHEN tagged = 1 THEN event_id ELSE NULL END) as taggeds," + + "\n group_concat(CASE WHEN hash_hit = 1 THEN events.event_id ELSE NULL END) as hash_hits," + + "\n group_concat(CASE WHEN tagged = 1 THEN events.event_id ELSE NULL END) as taggeds," + "\n min(time), max(time), " + typeColumn + ", " + descriptionColumn // NON-NLS + "\n FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) // NON-NLS + "\n WHERE time >= " + start + " AND time < " + end + " AND " + SQLHelper.getSQLWhere(filter) // NON-NLS From b21d0412b05ab2bdbbd80a97ba6416b633d2c530 Mon Sep 17 00:00:00 2001 From: millmanorama Date: Thu, 24 Sep 2015 15:55:57 -0400 Subject: [PATCH 13/15] fix path to guava jar --- CoreLibs/nbproject/project.properties | 2 +- CoreLibs/nbproject/project.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index fac6eca1cc..6bb8f91065 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -20,7 +20,7 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar file.reference.gson-1.4.jar=release/modules/ext/gson-1.4.jar file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar -file.reference.guava-18.0.jar=C:\\dev\\autopsy\\CoreLibs\\release\\modules\\ext\\guava-18.0.jar +file.reference.guava-18.0.jar=release/modules/ext/guava-18.0.jar file.reference.imageio-bmp-3.1.1.jar=release/modules/ext/imageio-bmp-3.1.1.jar file.reference.imageio-core-3.1.1.jar=release/modules/ext/imageio-core-3.1.1.jar file.reference.imageio-icns-3.1.1.jar=release/modules/ext/imageio-icns-3.1.1.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 09020ac24e..15a2b8da46 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -714,14 +714,14 @@ ext/imageio-pnm-3.1.1.jar release/modules/ext/imageio-pnm-3.1.1.jar - - ext/guava-18.0.jar - C:\dev\autopsy\CoreLibs\release\modules\ext\guava-18.0.jar - ext/common-lang-3.1.1.jar release/modules/ext/common-lang-3.1.1.jar + + ext/guava-18.0.jar + release/modules/ext/guava-18.0.jar + ext/slf4j-api-1.6.1.jar release/modules/ext/slf4j-api-1.6.1.jar From df60c51331defd011a84893aa3b0d18db2a2ab46 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 28 Sep 2015 16:10:25 -0400 Subject: [PATCH 14/15] remove UI access to old style clustering --- .../timeline/ui/detailview/DetailViewPane.java | 17 +++++------------ .../ui/detailview/DetailViewSettingsPane.fxml | 1 - 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index f40948c20b..d4b932f296 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -45,7 +45,6 @@ import javafx.scene.control.RadioButton; import javafx.scene.control.ScrollBar; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Slider; -import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.TreeItem; import javafx.scene.effect.Effect; @@ -212,8 +211,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (DetailViewNode n : chart.getNodes((DetailViewNode t) - -> t.getDescription().equals(tn.getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) -> + t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -236,8 +235,8 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (DetailViewNode n : chart.getNodes((DetailViewNode t) - -> t.getDescription().equals(tn.getValue().getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) -> + t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -363,9 +362,6 @@ public class DetailViewPane extends AbstractVisualization { - filteredEvents.refresh(); - }); + bandByTypeBox.selectedProperty().bindBidirectional(chart.bandByTypeProperty()); truncateAllBox.selectedProperty().bindBidirectional(chart.truncateAllProperty()); oneEventPerRowBox.selectedProperty().bindBidirectional(chart.oneEventPerRowProperty()); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml index 051067ce67..08e3964711 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewSettingsPane.fxml @@ -74,7 +74,6 @@ - From d3a589ec307e4afc51a24692a150c8c4c8afad80 Mon Sep 17 00:00:00 2001 From: jmillman Date: Mon, 28 Sep 2015 16:25:24 -0400 Subject: [PATCH 15/15] export the rest of guava --- CoreLibs/nbproject/project.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 15a2b8da46..eddc1b4f65 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -29,12 +29,22 @@ com.apple.eawt com.apple.eawt.event com.apple.eio + com.google.common.annotations com.google.common.base + com.google.common.base.internal com.google.common.cache com.google.common.collect + com.google.common.escape com.google.common.eventbus + com.google.common.hash + com.google.common.html com.google.common.io + com.google.common.math + com.google.common.net + com.google.common.primitives + com.google.common.reflect com.google.common.util.concurrent + com.google.common.xml com.google.gson com.google.gson.annotations com.google.gson.reflect