/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import java.util.Collection; import java.util.Comparator; import java.util.Objects; import java.util.Optional; import java.util.SortedSet; import javax.annotation.concurrent.Immutable; import org.joda.time.Interval; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * Represents a set of other events clustered together. All the sub events * should have the same type and matching descriptions at the designated "zoom * level", and be "close together" in time. */ @Immutable public class EventCluster implements MultiEvent<EventStripe> { /** * merge two event clusters into one new event cluster. * * @param cluster1 * @param cluster2 * * @return a new event cluster that is the result of merging the given * events clusters */ public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { if (cluster1.getEventType() != cluster2.getEventType()) { throw new IllegalArgumentException("event clusters are not compatible: they have different types"); } if (!cluster1.getDescription().equals(cluster2.getDescription())) { throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions"); } Sets.SetView<Long> idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); Sets.SetView<Long> hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); Sets.SetView<Long> taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags()); return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); } final private EventStripe parent; /** * the smallest time interval containing all the clustered events */ final private Interval span; /** * the type of all the clustered events */ final private EventType type; /** * the common description of all the clustered events */ final private String description; /** * the description level of detail that the events were clustered at. */ private final DescriptionLoD lod; /** * the set of ids of the clustered events */ final private ImmutableSet<Long> eventIDs; /** * the ids of the subset of clustered events that have at least one tag * applied to them */ private final ImmutableSet<Long> tagged; /** * the ids of the subset of clustered events that have at least one hash set * hit */ private final ImmutableSet<Long> hashHits; private EventCluster(Interval spanningInterval, EventType type, Collection<Long> eventIDs, Collection<Long> hashHits, Collection<Long> tagged, String description, DescriptionLoD lod, EventStripe parent) { this.span = spanningInterval; this.type = type; this.hashHits = ImmutableSet.copyOf(hashHits); this.tagged = ImmutableSet.copyOf(tagged); this.description = description; this.eventIDs = ImmutableSet.copyOf(eventIDs); this.lod = lod; this.parent = parent; } public EventCluster(Interval spanningInterval, EventType type, Collection<Long> eventIDs, Collection<Long> hashHits, Collection<Long> tagged, String description, DescriptionLoD lod) { this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null); } /** * get the EventStripe (if any) that contains this cluster * * @return an Optional containg the parent stripe of this cluster, or is * empty if the cluster has no parent set. */ @Override public Optional<EventStripe> getParent() { return Optional.ofNullable(parent); } /** * get the EventStripe (if any) that contains this cluster * * @return an Optional containg the parent stripe of this cluster, or is * empty if the cluster has no parent set. */ @Override public Optional<EventStripe> getParentStripe() { //since this clusters parent must be an event stripe just delegate to getParent(); return getParent(); } public Interval getSpan() { return span; } @Override public long getStartMillis() { return span.getStartMillis(); } @Override public long getEndMillis() { return span.getEndMillis(); } @Override public ImmutableSet<Long> getEventIDs() { return eventIDs; } @Override public ImmutableSet<Long> getEventIDsWithHashHits() { return hashHits; } @Override public ImmutableSet<Long> getEventIDsWithTags() { return tagged; } @Override public String getDescription() { return description; } @Override public EventType getEventType() { return type; } @Override public DescriptionLoD getDescriptionLoD() { return lod; } /** * return a new EventCluster identical to this one, except with the given * EventBundle as the parent. * * @param parent * * @return a new EventCluster identical to this one, except with the given * EventBundle as the parent. */ public EventCluster withParent(EventStripe parent) { return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent); } @Override public SortedSet<EventCluster> getClusters() { return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); } @Override public String toString() { return "EventCluster{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}'; } @Override public int hashCode() { int hash = 7; hash = 23 * hash + Objects.hashCode(this.type); hash = 23 * hash + Objects.hashCode(this.description); hash = 23 * hash + Objects.hashCode(this.lod); hash = 23 * hash + Objects.hashCode(this.eventIDs); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final EventCluster other = (EventCluster) obj; if (!Objects.equals(this.description, other.description)) { return false; } if (!Objects.equals(this.type, other.type)) { return false; } if (this.lod != other.lod) { return false; } if (!Objects.equals(this.eventIDs, other.eventIDs)) { return false; } return true; } }