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..17d5a01e7b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -0,0 +1,36 @@ +/* + * 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.collect.Range; +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(); + + Set getEventIDs(); + + Set getEventIDsWithHashHits(); + + Set getEventIDsWithTags(); + + EventType getEventType(); + + long getEndMillis(); + + long getStartMillis(); + + Iterable> getRanges(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java similarity index 67% rename from Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java rename to Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java index 6ca47c55be..7533c486e1 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); } @@ -106,36 +115,51 @@ public class AggregateEvent { return description; } - public EventType getType() { + public EventType getEventType() { return type; } - public DescriptionLOD getLOD() { + @Override + public DescriptionLOD getDescriptionLOD() { return lod; } /** * 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 AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) { + public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { - if (aggEvent1.getType() != ag2.getType()) { + 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 AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), 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() { + if (getEndMillis() > getStartMillis()) { + return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis()); + } else { + 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 new file mode 100644 index 0000000000..e88e8ff623 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -0,0 +1,143 @@ +/* + * 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 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) { + spans.addAll(u.spans); + spans.addAll(v.spans); + spanMap.putAll(u.spanMap); + spanMap.putAll(v.spanMap); + type = u.getEventType(); + 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); + } + + @Override + public String getDescription() { + return description; + } + + @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/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 3de6dacc61..664ae74ddd 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; @@ -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/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 3c3ba4069a..6b19122c8d 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; @@ -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 @@ -1031,7 +1032,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 +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(); @@ -1067,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 @@ -1076,14 +1077,14 @@ 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(); 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 AggregateEvent 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); @@ -1117,7 +1118,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 +1135,36 @@ 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<>(); + Map> typeMap = new HashMap<>(); - for (AggregateEvent aggregateEvent : preMergedEvents) { - typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create()) + for (EventCluster aggregateEvent : preMergedEvents) { + typeMap.computeIfAbsent(aggregateEvent.getEventType(), 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..c2a03545a6 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> eventClusterCache; private final ObservableMap datasourcesMap = FXCollections.observableHashMap(); private final ObservableMap hashSetMap = FXCollections.observableHashMap(); @@ -146,10 +146,10 @@ 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::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); @@ -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()); @@ -487,26 +487,29 @@ 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); + shortDesc = shortDesc.endsWith("/") ? shortDesc : shortDesc + "/"; + 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, 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/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/AbstractDetailViewNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java new file mode 100644 index 0000000000..e52cf31217 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AbstractDetailViewNode.java @@ -0,0 +1,554 @@ +/* + * 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; +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; +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.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; +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.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; + +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); + /** + * 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); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } + private final Map dropShadowMap = new HashMap<>(); + final Color evtColor; + + private final S parentNode; + private 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 + */ + private final Pane subNodePane = new Pane(); + + Pane getSubNodePane() { + return subNodePane; + } + + /** + * 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; + private final SleuthkitCase sleuthkitCase; + + SleuthkitCase getSleuthkitCase() { + return sleuthkitCase; + } + + 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; + + Region getSpacer() { + return spacer; + } + + private final Region spacer = new Region(); + + private final CollapseClusterAction collapseClusterAction; + private final ExpandClusterAction expandClusterAction; + + 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(); + 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, 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); + + 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); + } + + /** + * make a new filter intersecting the global filter with description and + * type filters to restrict sub-clusters + * + */ + RootFilter getSubClusterFilter() { + RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); + subClusterFilter.getSubFilters().addAll( + new DescriptionFilter(getEventBundle().getDescriptionLOD(), getDescription()), + new TypeFilter(getEventType())); + return subClusterFilter; + } + + abstract Collection makeBundlesFromClusters(List eventClusters); + + abstract void showSpans(final boolean showSpans); + + /** + * @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; + } + + final public 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(); + } + + /** + * loads sub-bundles at the given Description LOD, continues + * + * @param requestedDescrLoD + * @param expand + */ + private synchronized void loadSubBundles(DescriptionLOD.RelativeDetail relativeDetail) { + subNodePane.getChildren().clear(); + if (descLOD.get().withRelativeDetail(relativeDetail) == getEventBundle().getDescriptionLOD()) { + descLOD.set(getEventBundle().getDescriptionLOD()); + showSpans(true); + chart.setRequiresLayout(true); + chart.requestChartLayout(); + } else { + showSpans(false); + + // 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()); + + 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 { + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == getEventBundle().getDescriptionLOD()) { + return Collections.emptyList(); + } + bundles = loadBundles(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); + + // 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 { + List subBundleNodes = get(); + if (subBundleNodes.isEmpty()) { + showSpans(true); + } else { + showSpans(false); + } + descLOD.set(loadedDescriptionLoD); + //assign subNodes and request chart layout + 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 + chart.getController().monitorTask(loggedTask); + } + } + + final double getLayoutXCompensation() { + return (getParentNode() != null ? getParentNode().getLayoutXCompensation() : 0) + + getBoundsInParent().getMinX(); + } + + @Override + public final void setDescriptionVisibility(DescriptionVisibility descrVis) { + this.descrVis = descrVis; + final int size = getEventBundle().getEventIDs().size(); + + switch (this.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; + } + } + + abstract S getNodeForBundle(T bundle); + + /** + * event handler used for mouse events on {@link AggregateEventNode}s + */ + private class EventMouseHandler implements EventHandler { + + private ContextMenu contextMenu; + + @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().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + + } + } 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(); + } + } + } + + private class ExpandClusterAction extends Action { + + ExpandClusterAction() { + super("Expand"); + + setGraphic(new ImageView(PLUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLOD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLOD.RelativeDetail.MORE); + + } + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); + } + } + + private class CollapseClusterAction extends Action { + + CollapseClusterAction() { + super("Collapse"); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLOD previous = descLOD.get().lessDetailed(); + if (previous != null) { + loadSubBundles(DescriptionLOD.RelativeDetail.LESS); + } + }); + disabledProperty().bind(descLOD.isEqualTo(getEventBundle().getDescriptionLOD())); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java deleted file mode 100644 index f28ec9269d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-15 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.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.AggregateEvent; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -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 AggregateEvent} in a {@link EventDetailChart}. - */ -public class AggregateEventNode extends StackPane { - - private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.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 AggregateEvent aggEvent; - - private final AggregateEventNode parentEventNode; - - /** - * the region that represents the time span of this node's event - */ - private final Region spanRegion = new Region(); - - /** - * The label used to display this node's event's description - */ - private final Label descrLabel = new Label(); - - /** - * The 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 - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final SimpleObjectProperty 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)) { - { - 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 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 AggregateEventNode(final AggregateEvent aggEvent, AggregateEventNode parentEventNode, EventDetailChart chart) { - this.aggEvent = aggEvent; - descLOD.set(aggEvent.getLOD()); - this.parentEventNode = parentEventNode; - this.chart = chart; - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - - 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); - - 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); - - 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.getType().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.getType().getColor(); - spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setCursor(Cursor.HAND); - spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS - spanRegion.setBackground(spanFill); - - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart - installTooltip(); - spanRegion.setEffect(new DropShadow(10, evtColor)); - 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.getLOD())); - - 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() { - //TODO: all this work should probably go on a background thread... - if (tooltip == null) { - HashMap hashSetCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithHashHits().isEmpty()) { - hashSetCounts = new HashMap<>(); - try { - for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.getEventIDsWithHashHits())) { - Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); - for (String hashSetName : hashSetNames) { - hashSetCounts.merge(hashSetName, 1L, Long::sum); - } - } - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); - } - } - - Map tagCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithTags().isEmpty()) { - tagCounts.putAll( eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags())); - - } - - String hashSetCountsString = hashSetCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - String tagCountsString = tagCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - - tooltip = new Tooltip( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", - getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(), - getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()), - getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) - ); - Tooltip.install(AggregateEventNode.this, tooltip); - } - } - - public Pane getSubNodePane() { - return subNodePane; - } - - synchronized public AggregateEvent getEvent() { - return aggEvent; - } - - /** - * sets the width of the {@link Region} with border and background used to - * indicate the temporal span of this aggregate event - * - * @param w - */ - public void setSpanWidth(double w) { - spanRegion.setPrefWidth(w); - spanRegion.setMaxWidth(w); - spanRegion.setMinWidth(Math.max(2, w)); - } - - /** - * - * @param w the maximum width the description label should have - */ - public void setDescriptionWidth(double w) { - descrLabel.setMaxWidth(w); - } - - /** - * @param descrVis the level of description that should be displayed - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) { - this.descrVis = descrVis; - final int size = aggEvent.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 = aggEvent.getDescription(); - description = parentEventNode != null - ? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription()) - : description; - descrLabel.setText(description); - countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS - break; - } - } - - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - void applySelectionEffect(final boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - synchronized void applyHighlightEffect(boolean applied) { - - if (applied) { - descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS - spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(aggEvent.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(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(aggEvent.getType().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(); - } - - /** - * @return the contextMenu - */ - public ContextMenu getContextMenu() { - return contextMenu.get(); - } - - /** - * @param contextMenu the contextMenu to set - */ - public void setContextMenu(ContextMenu contextMenu) { - this.contextMenu.set(contextMenu); - } - - /** - * loads sub-clusters at the given Description LOD - * - * @param newDescriptionLOD - */ - synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); - if (newDescriptionLOD == aggEvent.getLOD()) { - 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(aggEvent.getDescription()), - new TypeFilter(aggEvent.getType())); - - //make a new end inclusive span (to 'filter' with) - final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().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 - return aggregatedEvents.stream().map(aggEvent -> { - AggregateEventNode subNode = new AggregateEventNode(aggEvent, AggregateEventNode.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 - 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); - } - } - - /** - * 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(AggregateEventNode.this) == false) { - chart.selectedNodes.add(AggregateEventNode.this); - } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(AggregateEventNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - } else { - chart.selectedNodes.setAll(AggregateEventNode.this); - } - } - } - } -} 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 new file mode 100644 index 0000000000..5ae2ab2e0b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewNode.java @@ -0,0 +1,67 @@ +/* + * 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.Comparator; +import java.util.List; +import java.util.Set; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; + +/** + * + */ +public interface DetailViewNode> { + + public void setDescriptionVisibility(DescriptionVisibility get); + + public List getSubNodes(); + + public void setSpanWidths(List spanWidths); + + public void setDescriptionWidth(double max); + + public EventBundle getEventBundle(); + + /** + * 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); + + 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 e1a0ec1867..d4b932f296 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; @@ -70,7 +69,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 +99,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()); @@ -109,20 +108,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 +148,7 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { while (change.next()) { change.getAddedSubList().forEach(aeNode -> { aeNode.applyHighlightEffect(true); @@ -166,7 +165,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))); }); @@ -212,8 +211,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 (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 (AggregateEventNode n : chart.getNodes((AggregateEventNode t) - -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { + for (DetailViewNode n : chart.getNodes((DetailViewNode t) -> + t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -250,7 +249,7 @@ public class DetailViewPane extends AbstractVisualization getYAxis() { + protected Axis getYAxis() { return verticalAxis; } @@ -279,15 +278,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,17 +325,17 @@ 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) { - getSeries(e.getType()).getData().add(xyData); + getSeries(e.getEventType()).getData().add(xyData); } }); } @@ -359,8 +356,8 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { + chart.applySelectionEffect(c1, selected); } private class DetailViewSettingsPane extends HBox { @@ -435,9 +432,10 @@ public class DetailViewPane extends AbstractVisualization { 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/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java new file mode 100644 index 0000000000..7a3dfc3ac3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -0,0 +1,181 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-15 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ColorUtilities; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Represents an {@link EventCluster} in a {@link EventDetailChart}. + */ +public class EventClusterNode extends AbstractDetailViewNode { + + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); + + /** + * the region that represents the time span of this node's event + */ + private final Region spanRegion = new Region(); + + /** + * the context menu that with the slider that controls subnode/event display + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart + */ + private final SimpleObjectProperty contextMenu = new SimpleObjectProperty<>(); + + private Tooltip tooltip; + + public EventClusterNode(final EventCluster eventCluster, EventClusterNode parentEventNode, EventDetailChart chart) { + super(chart, eventCluster, parentEventNode); + minWidthProperty().bind(spanRegion.widthProperty()); + header.setPrefWidth(USE_COMPUTED_SIZE); + + final BorderPane borderPane = new BorderPane(getSubNodePane(), header, null, null, null); + BorderPane.setAlignment(getSubNodePane(), Pos.TOP_LEFT); + borderPane.setPrefWidth(USE_COMPUTED_SIZE); + + getChildren().addAll(spanRegion, borderPane); + + //setup backgrounds + spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS + spanRegion.setBackground(getBackground()); + + } + + @Override + synchronized void installTooltip() { + //TODO: all this work should probably go on a background thread... + if (tooltip == null) { + HashMap hashSetCounts = new HashMap<>(); + if (!getEventCluster().getEventIDsWithHashHits().isEmpty()) { + hashSetCounts = new HashMap<>(); + try { + for (TimeLineEvent tle : getEventsModel().getEventsById(getEventCluster().getEventIDsWithHashHits())) { + Set hashSetNames = getSleuthkitCase().getAbstractFileById(tle.getFileID()).getHashSetNames(); + for (String hashSetName : hashSetNames) { + hashSetCounts.merge(hashSetName, 1L, Long::sum); + } + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); + } + } + + Map tagCounts = new HashMap<>(); + if (!getEventCluster().getEventIDsWithTags().isEmpty()) { + tagCounts.putAll(getEventsModel().getTagCountsByTagName(getEventCluster().getEventIDsWithTags())); + + } + + String hashSetCountsString = hashSetCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + String tagCountsString = tagCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + tooltip = new Tooltip( + NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", + getEventCluster().getEventIDs().size(), getEventCluster().getEventType(), getEventCluster().getDescription(), + getEventCluster().getSpan().getStart().toString(TimeLineController.getZonedFormatter()), + getEventCluster().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())) + + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) + ); + Tooltip.install(EventClusterNode.this, tooltip); + } + } + + synchronized public EventCluster getEventCluster() { + return getEventBundle(); + } + + /** + * sets the width of the {@link Region} with border and background used to + * indicate the temporal span of this aggregate event + * + * @param 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)); + + } + + @Override + Region getSpanFillNode() { + return spanRegion; + } + + /** + * @return the contextMenu + */ + public ContextMenu getContextMenu() { + return contextMenu.get(); + } + + /** + * @param contextMenu the contextMenu to set + */ + public void setContextMenu(ContextMenu contextMenu) { + this.contextMenu.set(contextMenu); + } + + @Override + void showSpans(boolean showSpans) { + //no-op for now + } + + @Override + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters; + } + + @Override + EventClusterNode getNodeForBundle(EventCluster cluster) { + return new EventClusterNode(cluster, this, getChart()); + } + +} 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..3d7f7bc017 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,22 +18,27 @@ */ 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.MissingResourceException; +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; 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; @@ -41,9 +46,7 @@ import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; -import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; -import javafx.collections.ObservableMap; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -62,6 +65,7 @@ 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 +75,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 +95,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; @@ -102,7 +107,6 @@ public final class EventDetailChart extends XYChart im */ 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; @@ -151,17 +155,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 clusterNodeMap = 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 +165,7 @@ public final class EventDetailChart extends XYChart im */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap projectionMap = FXCollections.observableHashMap(); + private final Map, Line> projectionMap = new HashMap<>(); /** * flag indicating whether this chart actually needs a layout pass @@ -177,16 +173,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,12 +201,13 @@ 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); - //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 +221,7 @@ public final class EventDetailChart extends XYChart im //bind listener to events that should trigger layout widthProperty().addListener(layoutInvalidationListener); heightProperty().addListener(layoutInvalidationListener); -// boundsInLocalProperty().addListener(layoutInvalidationListener); + bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); @@ -242,36 +239,7 @@ public final class EventDetailChart extends XYChart im 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(); } @@ -285,51 +253,85 @@ public final class EventDetailChart extends XYChart im setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change 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) -> { + ListChangeListener.Change> c) -> { while (c.next()) { - c.getRemoved().forEach((AggregateEventNode t) -> { - projectionMap.remove(t); - }); - c.getAddedSubList().forEach((AggregateEventNode t) -> { - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(t, line); - }); + c.getRemoved().forEach((DetailViewNode t) -> { + t.getEventBundle().getRanges().forEach((Range t1) -> { + Line removedLine = projectionMap.remove(t1); + getChartChildren().removeAll(removedLine); + }); + }); + c.getAddedSubList().forEach((DetailViewNode t) -> { + + for (Range range : t.getEventBundle().getRanges()) { + + 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(range, line); + getChartChildren().add(line); + } + }); } this.controller.selectEventIDs(selectedNodes.stream() - .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream()) + .flatMap(detailNode -> detailNode.getEventIDs().stream()) .collect(Collectors.toList())); }); 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(sceneToLocal(clickEvent.getSceneX(), 0).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(sceneToLocal(clickEvent.getSceneX(), 0).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); intervalSelector = null; } - public synchronized SimpleBooleanProperty getBandByType() { + public synchronized SimpleBooleanProperty bandByTypeProperty() { return bandByType; } @@ -389,11 +391,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 +409,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.getEventType(), 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 { + clusterNodeMap.computeIfAbsent(aggEvent, (EventCluster t) -> { + EventClusterNode eventNode = new EventClusterNode(aggEvent, null, EventDetailChart.this); + eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); + clusterNodeMap.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 = clusterNodeMap.remove(aggEvent); + nodeGroup.getChildren().remove(removedNode); + + EventStripe removedCluster = stripeDescMap.remove(ImmutablePair.of(aggEvent.getEventType(), aggEvent.getDescription())); + removedNode = stripeNodeMap.remove(removedCluster); + nodeGroup.getChildren().remove(removedNode); + data.setNode(null); } @@ -465,17 +486,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()); + nodes.sort(Comparator.comparing(DetailViewNode::getStartMillis)); + layoutNodes(nodes, minY, 0); + } else { + List nodes = new ArrayList<>(clusterNodeMap.values()); + nodes.sort(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.getEventType(), 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(clusterNodeMap::get) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(EventClusterNode::getStartMillis)) + .collect(Collectors.toList()); + layoutNodes(nodes, minY, 0); + } minY = maxY.get(); } } @@ -486,7 +526,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 +535,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,7 +543,7 @@ public final class EventDetailChart extends XYChart im requiresLayout = true; } - synchronized SimpleObjectProperty getDescrVisibility() { + synchronized SimpleObjectProperty< DescriptionVisibility> getDescrVisibility() { return descrVisibility; } @@ -511,40 +551,39 @@ public final class EventDetailChart extends XYChart im return maxY.getReadOnlyProperty(); } - Iterable getNodes(Predicate p) { - List nodes = new ArrayList<>(); + Iterable> getNodes(Predicate> p) { + Collection> values = alternateLayout.get() + ? stripeNodeMap.values() + : clusterNodeMap.values(); - for (AggregateEventNode 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); } - 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) { - if (node != null) { - if (p.test(node)) { - nodes.add(node); - } - for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { - checkNode((AggregateEventNode) n, p, nodes); - } - } - } - private void clearGuideLine() { getChartChildren().remove(guideLine); guideLine = null; @@ -557,41 +596,75 @@ 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 (AbstractDetailViewNode node : nodes) { + node.setDescriptionVisibility(descrVisibility.get()); + double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(node.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; + List> subNodes = node.getSubNodes(); + if (subNodes.isEmpty() == false) { + subNodes.sort(new DetailViewNode.StartTimeComparator()); + layoutNodesResultHeight = layoutNodes(subNodes, 0, rawDisplayPosition); } - tlNode.autosize(); //compute size of tlNode based on constraints and event data + + if (alternateLayout.get() == false) { + double endX = getXAxis().getDisplayPosition(new DateTime(node.getEndMillis())) - xOffset; + span = endX - startX; + //size timespan border + node.setSpanWidths(Arrays.asList(span)); + } else { + + EventStripeNode stripeNode = (EventStripeNode) node; + List spanWidths = new ArrayList<>(); + double x = getXAxis().getDisplayPosition(new DateTime(stripeNode.getStartMillis()));; + double x2; + Iterator> ranges = stripeNode.getStripe().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); + if (ranges.hasNext() == false) { + x2 = getXAxis().getDisplayPosition(new DateTime(range.upperEndpoint())); + clusterSpan = x2 - x; + span += clusterSpan; + spanWidths.add(clusterSpan); + } + } + + } while (ranges.hasNext()); + + stripeNode.setSpanWidths(spanWidths); + } + if (truncateAll.get()) { //if truncate option is selected limit width of description label + node.setDescriptionWidth(Math.max(span, truncateWidth.get())); + } else { //else set it unbounded + node.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); + } + + 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 = xPos + tlNode.getWidth(); + double xRight = startX + node.getWidth(); //get the height of the node - final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; + final double h = layoutNodesResultHeight == 0 ? node.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; //initial test position double yPos = minY; @@ -612,7 +685,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 +703,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(node.layoutXProperty(), startX), + new KeyValue(node.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, Line> entry : projectionMap.entrySet()) { + final Range eventBundle = 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(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); } @@ -671,29 +744,8 @@ public final class EventDetailChart extends XYChart im return filteredEvents; } - /** - * @return the chartContextMenu - */ - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } - - 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())); - } - } - + Property alternateLayoutProperty() { + return alternateLayout; } private class DetailIntervalSelector extends IntervalSelector { @@ -730,4 +782,8 @@ public final class EventDetailChart extends XYChart im protected void requestChartLayout() { super.requestChartLayout(); } + + 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 new file mode 100644 index 0000000000..7b2ffb606a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -0,0 +1,108 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.stream.Collectors; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.VBox; +import org.sleuthkit.autopsy.coreutils.ColorUtilities; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import static org.sleuthkit.autopsy.timeline.ui.detailview.AbstractDetailViewNode.show; + +/** + * + */ +public class EventStripeNode extends AbstractDetailViewNode { + + 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, getSubNodePane()); + internalVBox.setAlignment(Pos.CENTER_LEFT); + + 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()); + } + rangesHBox.getChildren().remove(rangesHBox.getChildren().size() - 1); + rangesHBox.setMaxWidth(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + getChildren().addAll(rangesHBox, internalVBox); + } + + /** + * + * @param showControls the value of par + */ + @Override + void showDescriptionLoDControls(final boolean showControls) { + super.showDescriptionLoDControls(showControls); + show(getSpacer(), showControls); + } + + @Override + public void setSpanWidths(List spanWidths) { + for (int i = 0; i < spanWidths.size(); i++) { + Region spanRegion = (Region) rangesHBox.getChildren().get(i); + Double w = spanWidths.get(i); + spanRegion.setPrefWidth(w); + spanRegion.setMaxWidth(w); + spanRegion.setMinWidth(Math.max(2, w)); + } + } + + EventStripe getStripe() { + return getEventBundle(); + } + + @Override + HBox getSpanFillNode() { + return rangesHBox; + } + + @Override + Collection makeBundlesFromClusters(List eventClusters) { + return eventClusters.stream().collect( + Collectors.toMap( + EventCluster::getDescription, //key + EventStripe::new, //value + EventStripe::merge)//merge method + ).values(); + } + + /** + * + * @param showSpans the value of showSpans + */ + @Override + void showSpans(final boolean showSpans) { + rangesHBox.setVisible(showSpans); + } + + @Override + void installTooltip() { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + EventStripeNode getNodeForBundle(EventStripe cluster) { + return new EventStripeNode(cluster, this, getChart()); + } +} 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); 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..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 @@ -20,15 +20,16 @@ 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) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size())); + public EventDescriptionTreeItem(EventCluster g) { + setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getDescription(), g.getEventIDs().size())); } @Override @@ -37,9 +38,9 @@ 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))) { + if ((value.getType().getBaseType().equals(g.getEventType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { throw new IllegalArgumentException(); } @@ -52,8 +53,8 @@ class EventDescriptionTreeItem extends NavTreeItem { } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { - if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { + public TreeItem findTreeItemForEvent(EventBundle t) { + 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 ac16b31949..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 @@ -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,8 +36,8 @@ class EventTypeTreeItem extends NavTreeItem { private final Comparator> comparator = TreeComparator.Description; - EventTypeTreeItem(AggregateEvent g) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0)); + EventTypeTreeItem(EventCluster g) { + setValue(new NavTreeNode(g.getEventType().getBaseType(), g.getEventType().getBaseType().getDisplayName(), 0)); } @Override @@ -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,8 +78,8 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { - if (t.getType().getBaseType() == getValue().getType().getBaseType()) { + public TreeItem findTreeItemForEvent(EventBundle t) { + 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 361c8b3250..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.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.getEventBundle())); }); }); @@ -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); } } @@ -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 3f96c57a71..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,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.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; /** * 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..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 @@ -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,13 +57,13 @@ 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()); + 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(() -> { @@ -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/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java index d1bdb4e067..8990d2ce21 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java @@ -21,7 +21,7 @@ package org.sleuthkit.autopsy.timeline.zooming; import org.openide.util.NbBundle; /** - * + * Enumeration of all description levels of detail. */ public enum DescriptionLOD { @@ -39,7 +39,7 @@ public enum DescriptionLOD { this.displayName = displayName; } - public DescriptionLOD next() { + public DescriptionLOD moreDetailed() { try { return values()[ordinal() + 1]; } catch (ArrayIndexOutOfBoundsException e) { @@ -47,11 +47,31 @@ public enum DescriptionLOD { } } - public DescriptionLOD previous() { + public DescriptionLOD lessDetailed() { try { return values()[ordinal() - 1]; } catch (ArrayIndexOutOfBoundsException e) { return null; } } + + public DescriptionLOD withRelativeDetail(RelativeDetail relativeDetail) { + switch (relativeDetail) { + case EQUAL: + return this; + case MORE: + return moreDetailed(); + case LESS: + return lessDetailed(); + default: + throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail); + } + } + + public enum RelativeDetail { + + EQUAL, + MORE, + LESS; + } } 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..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-11.0.2.jar=release/modules/ext/guava-11.0.2.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 @@ -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..eddc1b4f65 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -34,13 +34,17 @@ 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 @@ -708,10 +712,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 @@ -728,6 +728,10 @@ 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