Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
EventClusterNode.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2013-18 Basis Technology Corp.
5 * Contact: carrier <at> sleuthkit <dot> org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.sleuthkit.autopsy.timeline.ui.detailview;
20
21import com.google.common.collect.Iterables;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collections;
25import java.util.List;
26import static java.util.Objects.nonNull;
27import java.util.Set;
28import java.util.concurrent.ExecutionException;
29import java.util.logging.Level;
30import java.util.stream.Collectors;
31import javafx.collections.ObservableList;
32import javafx.concurrent.Task;
33import javafx.event.EventHandler;
34import javafx.geometry.Pos;
35import javafx.scene.Cursor;
36import javafx.scene.control.Button;
37import javafx.scene.image.Image;
38import javafx.scene.image.ImageView;
39import javafx.scene.input.MouseEvent;
40import javafx.scene.layout.Border;
41import javafx.scene.layout.BorderStroke;
42import javafx.scene.layout.BorderStrokeStyle;
43import javafx.scene.layout.BorderWidths;
44import javafx.scene.layout.VBox;
45import org.controlsfx.control.action.Action;
46import org.controlsfx.control.action.ActionUtils;
47import org.joda.time.DateTime;
48import org.joda.time.Interval;
49import org.openide.util.NbBundle;
50import org.sleuthkit.autopsy.coreutils.LoggedTask;
51import org.sleuthkit.autopsy.coreutils.Logger;
52import org.sleuthkit.autopsy.coreutils.ThreadConfined;
53import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.configureActionButton;
54import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.DetailViewEvent;
55import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventCluster;
56import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.EventStripe;
57import org.sleuthkit.autopsy.timeline.ui.detailview.datamodel.SingleDetailsViewEvent;
58import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.SqlFilterState;
59import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.DescriptionFilter;
60import org.sleuthkit.autopsy.timeline.ui.filtering.datamodel.RootFilterState;
61import org.sleuthkit.autopsy.timeline.zooming.EventsModelParams;
62import org.sleuthkit.datamodel.TimelineLevelOfDetail;
63import org.sleuthkit.datamodel.TskCoreException;
64import org.sleuthkit.datamodel.TimelineEventType;
65import org.sleuthkit.datamodel.TimelineEvent;
66import org.sleuthkit.datamodel.TimelineFilter.EventTypeFilter;
67
71final class EventClusterNode extends MultiEventNodeBase<EventCluster, EventStripe, EventStripeNode> {
72
73 private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
74
78 private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1);
79
84 private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
85
89 private Button plusButton;
93 private Button minusButton;
94
102 EventClusterNode(DetailsChartLane<?> chartLane, EventCluster eventCluster, EventStripeNode parentNode) {
103 super(chartLane, eventCluster, parentNode);
104
105 subNodePane.setBorder(clusterBorder);
106 subNodePane.setBackground(defaultBackground);
107 subNodePane.setMinWidth(1);
108 subNodePane.setMaxWidth(USE_PREF_SIZE);
109 setMinHeight(24);
110 setAlignment(Pos.CENTER_LEFT);
111
112 setCursor(Cursor.HAND);
113 getChildren().addAll(subNodePane, infoHBox);
114
115 if (parentNode == null) {
116 setDescriptionVisibility(DescriptionVisibility.SHOWN);
117 }
118 }
119
125 Button getNewExpandButton() {
126 return ActionUtils.createButton(new ExpandClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
127 }
128
134 Button getNewCollapseButton() {
135 return ActionUtils.createButton(new CollapseClusterAction(this), ActionUtils.ActionTextBehavior.HIDE);
136 }
137
138 @Override
139 void installActionButtons() {
140 super.installActionButtons();
141 if (plusButton == null) {
142 plusButton = getNewExpandButton();
143 minusButton = getNewCollapseButton();
144 controlsHBox.getChildren().addAll(minusButton, plusButton);
145
146 configureActionButton(plusButton);
147 configureActionButton(minusButton);
148 }
149 }
150
151 @Override
152 void showFullDescription(final int size) {
153 if (getParentNode().isPresent()) {
154 showCountOnly(size);
155 } else {
156 super.showFullDescription(size);
157 }
158 }
159
166 @NbBundle.Messages(value = "EventClusterNode.loggedTask.name=Load sub events")
167 @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
168 private synchronized void loadSubStripes(RelativeDetail relativeDetail) {
169 getChartLane().setCursor(Cursor.WAIT);
170
171 /*
172 * Make new ZoomState to query with:
173 *
174 * We need to extend end time for the query by one second, because it is
175 * treated as an open interval but we want to include events at exactly
176 * the time of the last event in this cluster. Restrict the sub stripes
177 * to the type and description of this cluster by intersecting a new
178 * filter with the existing root filter.
179 */
180 RootFilterState subClusterFilter = eventsModel.getEventFilterState()
181 .intersect(new SqlFilterState<>(
182 new EventTypeFilter(getEventType()), true));
183 final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000);
184 final TimelineEventType.HierarchyLevel eventTypeZoomLevel = eventsModel.getEventTypeZoom();
185 final EventsModelParams zoom = new EventsModelParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLevel());
186
187 DescriptionFilter descriptionFilter = new DescriptionFilter(getEvent().getDescriptionLevel(), getDescription());
188 /*
189 * task to load sub-stripes in a background thread
190 */
191 Task<List<EventStripe>> loggedTask;
192 loggedTask = new LoggedTask<List<EventStripe>>(Bundle.EventClusterNode_loggedTask_name(), false) {
193
194 private volatile TimelineLevelOfDetail loadedDescriptionLevel = withRelativeDetail(getDescriptionLevel(), relativeDetail);
195
196 @Override
197 protected List<EventStripe> call() throws Exception {
198 //newly loaded substripes
199 List<EventStripe> stripes;
200 //next LoD in diraction of given relativeDetail
201 TimelineLevelOfDetail next = loadedDescriptionLevel;
202 do {
203 loadedDescriptionLevel = next;
204 if (loadedDescriptionLevel == getEvent().getDescriptionLevel()) {
205 //if we are back at the level of detail of the original cluster, return empty list to inidicate.
206 return Collections.emptyList();
207 }
208
209 //query for stripes at the desired level of detail
210 stripes = chartLane.getParentChart().getDetailsViewModel().getEventStripes(descriptionFilter, zoom.withDescrLOD(loadedDescriptionLevel));
211 //setup next for subsequent go through the "do" loop
212 next = withRelativeDetail(loadedDescriptionLevel, relativeDetail);
213 } while (stripes.size() == 1 && nonNull(next)); //keep going while there was only on stripe and we havne't reached the end of the LoD continuum.
214
215 // return list of EventStripes with parents set to this cluster
216 return stripes.stream()
217 .map(eventStripe -> eventStripe.withParent(getEvent()))
218 .collect(Collectors.toList());
219 }
220
221 @Override
222 protected void succeeded() {
223 ObservableList<DetailViewEvent> chartNestedEvents = getChartLane().getParentChart().getAllNestedEvents();
224
225 //clear the existing subnodes/events
226 chartNestedEvents.removeAll(StripeFlattener.flatten(subNodes));
227 subNodes.clear();
228
229 try {
230 setDescriptionLOD(loadedDescriptionLevel);
231 List<EventStripe> newSubStripes = get();
232 if (newSubStripes.isEmpty()) {
233 //restore original display
234 getChildren().setAll(subNodePane, infoHBox);
235 } else {
236 //display new sub stripes
237 List<EventNodeBase<?>> newSubNodes = new ArrayList<>();
238 for (EventStripe subStripe : newSubStripes) {//map stripes to nodes
239 newSubNodes.add(createChildNode(subStripe));
240 }
241 subNodes.addAll(newSubNodes);
242 chartNestedEvents.addAll(StripeFlattener.flatten(subNodes));
243 getChildren().setAll(new VBox(infoHBox, subNodePane));
244 }
245 } catch (TskCoreException | InterruptedException | ExecutionException ex) {
246 LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); //NON-NLS
247
248 }
249
250 getChartLane().requestChartLayout();
251 getChartLane().setCursor(null);
252 }
253 };
254
255 //start task
256 new Thread(loggedTask).start();
257 getChartLane().getController().monitorTask(loggedTask);
258 }
259
260 @Override
261 EventNodeBase<?> createChildNode(EventStripe stripe) throws TskCoreException {
262 Set<Long> eventIDs = stripe.getEventIDs();
263 if (eventIDs.size() == 1) {
264 //If the stripe is a single event, make a single event node rather than a stripe node.
265 TimelineEvent singleEvent = getController().getEventsModel().getEventById(Iterables.getOnlyElement(eventIDs));
266 SingleDetailsViewEvent singleDetailsEvent = new SingleDetailsViewEvent(singleEvent).withParent(stripe);
267 return new SingleEventNode(getChartLane(), singleDetailsEvent, this);
268 } else {
269 return new EventStripeNode(getChartLane(), stripe, this);
270 }
271 }
272
273 @Override
274 protected void layoutChildren() {
275 double chartX = getChartLane().getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
276 double width = getChartLane().getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
277 subNodePane.setPrefWidth(Math.max(1, width));
278 super.layoutChildren();
279 }
280
281 @Override
282 Iterable<? extends Action> getActions() {
283 return Iterables.concat(
284 super.getActions(),
285 Arrays.asList(new ExpandClusterAction(this), new CollapseClusterAction(this))
286 );
287 }
288
289 @Override
290 EventHandler<MouseEvent> getDoubleClickHandler() {
291 return mouseEvent -> new ExpandClusterAction(this).handle(null);
292 }
293
298 static private class ExpandClusterAction extends Action {
299
300 private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N
301
302 @NbBundle.Messages({"ExpandClusterAction.text=Expand"})
303 ExpandClusterAction(EventClusterNode node) {
304 super(Bundle.ExpandClusterAction_text());
305 setGraphic(new ImageView(PLUS));
306
307 setEventHandler(actionEvent -> {
308 if (node.getDescriptionLevel().moreDetailed() != null) {
309 node.loadSubStripes(RelativeDetail.MORE);
310 }
311 });
312
313 //disabled if the given node is already at full description level of detail
314 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(TimelineLevelOfDetail.HIGH));
315 }
316 }
317
322 static private class CollapseClusterAction extends Action {
323
324 private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N
325
326 @NbBundle.Messages({"CollapseClusterAction.text=Collapse"})
327 CollapseClusterAction(EventClusterNode node) {
328 super(Bundle.CollapseClusterAction_text());
329 setGraphic(new ImageView(MINUS));
330
331 setEventHandler(actionEvent -> {
332 if (node.getDescriptionLevel().lessDetailed() != null) {
333 node.loadSubStripes(RelativeDetail.LESS);
334 }
335 });
336
337 //disabled if node is at clusters level of detail
338 disabledProperty().bind(node.descriptionLoDProperty().isEqualTo(node.getEvent().getDescriptionLevel()));
339 }
340 }
341
342 private enum RelativeDetail {
343
347 }
348
349 private static TimelineLevelOfDetail withRelativeDetail(TimelineLevelOfDetail LoD, RelativeDetail relativeDetail) {
350 switch (relativeDetail) {
351 case EQUAL:
352 return LoD;
353 case MORE:
354 return LoD.moreDetailed();
355 case LESS:
356 return LoD.lessDetailed();
357 default:
358 throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail);
359 }
360 }
361
362}
TimelineEvent getEventById(Long eventID)

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.