Skip to content
Snippets Groups Projects
Commit dbf954d1 authored by Richard Cordovano's avatar Richard Cordovano
Browse files

Merge pull request #1615 from millmanorama/subcluster_expansion

Subcluster expansion
parents 8e345c0b bd9c9109
Branches
Tags
No related merge requests found
Showing
with 994 additions and 865 deletions
......@@ -18,16 +18,16 @@
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.Range;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
/**
*
* A interface for groups of events that share some attributes in common.
*/
public interface EventBundle {
public interface EventBundle<ParentType extends EventBundle<?>> {
String getDescription();
......@@ -35,7 +35,6 @@ public interface EventBundle {
Set<Long> getEventIDs();
Set<Long> getEventIDsWithHashHits();
Set<Long> getEventIDsWithTags();
......@@ -46,11 +45,11 @@ public interface EventBundle {
long getStartMillis();
Iterable<Range<Long>> getRanges();
Optional<ParentType> getParentBundle();
Optional<EventBundle> getParentBundle();
default long getCount() {
default long getCount() {
return getEventIDs().size();
}
SortedSet<EventCluster> getClusters();
}
......@@ -18,12 +18,14 @@
*/
package org.sleuthkit.autopsy.timeline.datamodel;
import com.google.common.collect.Range;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import javax.annotation.concurrent.Immutable;
import org.joda.time.Interval;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
......@@ -36,7 +38,7 @@
* designated 'zoom level', and be 'close together' in time.
*/
@Immutable
public class EventCluster implements EventBundle {
public class EventCluster implements EventBundle<EventStripe> {
/**
* merge two event clusters into one new event cluster.
......@@ -62,7 +64,7 @@ public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod);
}
final private EventBundle parent;
final private EventStripe parent;
/**
* the smallest time interval containing all the clustered events
......@@ -101,7 +103,7 @@ public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
*/
private final Set<Long> hashHits;
private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod, EventBundle parent) {
private EventCluster(Interval spanningInterval, EventType type, Set<Long> eventIDs, Set<Long> hashHits, Set<Long> tagged, String description, DescriptionLoD lod, EventStripe parent) {
this.span = spanningInterval;
this.type = type;
......@@ -118,7 +120,7 @@ public EventCluster(Interval spanningInterval, EventType type, Set<Long> eventID
}
@Override
public Optional<EventBundle> getParentBundle() {
public Optional<EventStripe> getParentBundle() {
return Optional.ofNullable(parent);
}
......@@ -166,19 +168,6 @@ public DescriptionLoD getDescriptionLoD() {
return lod;
}
Range<Long> getRange() {
if (getEndMillis() > getStartMillis()) {
return Range.closedOpen(getSpan().getStartMillis(), getSpan().getEndMillis());
} else {
return Range.singleton(getStartMillis());
}
}
@Override
public Iterable<Range<Long>> getRanges() {
return Collections.singletonList(getRange());
}
/**
* return a new EventCluster identical to this one, except with the given
* EventBundle as the parent.
......@@ -188,11 +177,15 @@ public Iterable<Range<Long>> getRanges() {
* @return a new EventCluster identical to this one, except with the given
* EventBundle as the parent.
*/
public EventCluster withParent(EventBundle parent) {
public EventCluster withParent(EventStripe parent) {
if (Objects.nonNull(this.parent)) {
throw new IllegalStateException("Event Cluster already has a 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();
}
}
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <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.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.Comparator;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.concurrent.Immutable;
import org.python.google.common.base.Objects;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
......@@ -22,10 +33,10 @@
/**
* A 'collection' of {@link EventCluster}s, all having the same type,
* description, and zoom levels.
* description, and zoom levels, but not necessarily close together in time.
*/
@Immutable
public final class EventStripe implements EventBundle {
public final class EventStripe implements EventBundle<EventCluster> {
public static EventStripe merge(EventStripe u, EventStripe v) {
Preconditions.checkNotNull(u);
......@@ -37,10 +48,9 @@ public static EventStripe merge(EventStripe u, EventStripe v) {
return new EventStripe(u, v);
}
private final EventBundle parent;
private final EventCluster parent;
private final RangeSet<Long> spans = TreeRangeSet.create();
private final RangeMap<Long, EventCluster> spanMap = TreeRangeMap.create();
private final SortedSet<EventCluster> clusters = new TreeSet<>(Comparator.comparing(EventCluster::getStartMillis));
/**
* the type of all the events
......@@ -73,23 +83,21 @@ public static EventStripe merge(EventStripe u, EventStripe v) {
*/
private final Set<Long> hashHits = new HashSet<>();
public EventStripe(EventCluster cluster) {
spans.add(cluster.getRange());
spanMap.put(cluster.getRange(), cluster);
public EventStripe(EventCluster cluster, EventCluster parent) {
clusters.add(cluster);
type = cluster.getEventType();
description = cluster.getDescription();
lod = cluster.getDescriptionLoD();
eventIDs.addAll(cluster.getEventIDs());
tagged.addAll(cluster.getEventIDsWithTags());
hashHits.addAll(cluster.getEventIDsWithHashHits());
parent = cluster.getParentBundle().orElse(null);
this.parent = parent;
}
private EventStripe(EventStripe u, EventStripe v) {
spans.addAll(u.spans);
spans.addAll(v.spans);
spanMap.putAll(u.spanMap);
spanMap.putAll(v.spanMap);
clusters.addAll(u.clusters);
clusters.addAll(v.clusters);
type = u.getEventType();
description = u.getDescription();
lod = u.getDescriptionLoD();
......@@ -99,11 +107,11 @@ private EventStripe(EventStripe u, EventStripe v) {
tagged.addAll(v.getEventIDsWithTags());
hashHits.addAll(u.getEventIDsWithHashHits());
hashHits.addAll(v.getEventIDsWithHashHits());
parent = u.getParentBundle().orElse(null);
parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null));
}
@Override
public Optional<EventBundle> getParentBundle() {
public Optional<EventCluster> getParentBundle() {
return Optional.ofNullable(parent);
}
......@@ -139,16 +147,15 @@ public Set<Long> getEventIDsWithTags() {
@Override
public long getStartMillis() {
return spans.span().lowerEndpoint();
return clusters.first().getStartMillis();
}
@Override
public long getEndMillis() {
return spans.span().upperEndpoint();
return clusters.last().getEndMillis();
}
@Override
public Iterable<Range<Long>> getRanges() {
return spans.asRanges();
public SortedSet< EventCluster> getClusters() {
return Collections.unmodifiableSortedSet(clusters);
}
}
......@@ -89,11 +89,12 @@
* EventTypeMap, and dataSets is all linked directly to the ClusterChart which
* must only be manipulated on the JavaFx thread.
*/
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventStripeNode, EventDetailChart> {
public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster, EventBundleNodeBase<?, ?, ?>, EventDetailChart> {
private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName());
private MultipleSelectionModel<TreeItem<EventBundle>> treeSelectionModel;
private MultipleSelectionModel<TreeItem<EventBundle<?>>> treeSelectionModel;
//these three could be injected from fxml but it was causing npe's
private final DateAxis dateAxis = new DateAxis();
......@@ -107,9 +108,9 @@ public class DetailViewPane extends AbstractVisualization<DateTime, EventCluster
private final Region region = new Region();
private final ObservableList<EventStripeNode> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ObservableList<EventBundleNodeBase<?, ?, ?>> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
public ObservableList<EventBundle> getEventBundles() {
public ObservableList<EventBundle<?>> getEventBundles() {
return chart.getEventBundles();
}
......@@ -136,7 +137,7 @@ public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) {
vertScrollBar.visibleAmountProperty().bind(chart.heightProperty().multiply(100).divide(chart.maxVScrollProperty()));
requestLayout();
highlightedNodes.addListener((ListChangeListener.Change<? extends EventStripeNode> change) -> {
highlightedNodes.addListener((ListChangeListener.Change<? extends EventBundleNodeBase<?, ?, ?>> change) -> {
while (change.next()) {
change.getAddedSubList().forEach(node -> {
......@@ -201,7 +202,7 @@ public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) {
highlightedNodes.clear();
selectedNodes.stream().forEach((tn) -> {
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
t.getDescription().equals(tn.getDescription()))) {
highlightedNodes.add(n);
}
......@@ -219,14 +220,14 @@ private void incrementScrollValue(int factor) {
vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() + factor * (chart.getHeight() / chart.maxVScrollProperty().get()))));
}
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle>> selectionModel) {
public void setSelectionModel(MultipleSelectionModel<TreeItem<EventBundle<?>>> selectionModel) {
this.treeSelectionModel = selectionModel;
treeSelectionModel.getSelectedItems().addListener((Observable observable) -> {
highlightedNodes.clear();
for (TreeItem<EventBundle> tn : treeSelectionModel.getSelectedItems()) {
for (TreeItem<EventBundle<?>> tn : treeSelectionModel.getSelectedItems()) {
for (EventStripeNode n : chart.getNodes((EventStripeNode t) ->
for (EventBundleNodeBase<?, ?, ?> n : chart.getNodes((EventBundleNodeBase<?, ?, ?> t) ->
t.getDescription().equals(tn.getValue().getDescription()))) {
highlightedNodes.add(n);
}
......@@ -351,7 +352,7 @@ protected Effect getSelectionEffect() {
}
@Override
protected void applySelectionEffect(EventStripeNode c1, Boolean selected) {
protected void applySelectionEffect(EventBundleNodeBase<?, ?, ?> c1, Boolean selected) {
c1.applySelectionEffect(selected);
}
......@@ -475,4 +476,6 @@ public Action newUnhideDescriptionAction(String description, DescriptionLoD desc
public Action newHideDescriptionAction(String description, DescriptionLoD descriptionLoD) {
return chart.new HideDescriptionAction(description, descriptionLoD);
}
}
/*
* Autopsy Forensic Browser
*
* Copyright 2015 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.ui.detailview;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.beans.Observable;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
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 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.joda.time.DateTime;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.timeline.TimeLineController;
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
*
*/
public abstract class EventBundleNodeBase<BundleType extends EventBundle<ParentType>, ParentType extends EventBundle<BundleType>, ParentNodeType extends EventBundleNodeBase<ParentType, BundleType, ?>> extends StackPane {
private static final Logger LOGGER = Logger.getLogger(EventBundleNodeBase.class.getName());
private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N
private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N
static final CornerRadii CORNER_RADII_3 = new CornerRadii(3);
static final CornerRadii CORNER_RADII_1 = new CornerRadii(1);
private final Border SELECTION_BORDER;
private static final Map<EventType, Effect> dropShadowMap = new ConcurrentHashMap<>();
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);
}
protected final EventDetailChart chart;
final SimpleObjectProperty<DescriptionLoD> descLOD = new SimpleObjectProperty<>();
final SimpleObjectProperty<DescriptionVisibility> descVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN);
protected final BundleType eventBundle;
protected final ParentNodeType parentNode;
final SleuthkitCase sleuthkitCase;
final FilteredEventsModel eventsModel;
final Background highlightedBackground;
final Background defaultBackground;
final Color evtColor;
final List<ParentNodeType> subNodes = new ArrayList<>();
final Pane subNodePane = new Pane();
final Label descrLabel = new Label();
final Label countLabel = new Label();
final ImageView hashIV = new ImageView(HASH_PIN);
final ImageView tagIV = new ImageView(TAG);
final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV);
private Tooltip tooltip;
public EventBundleNodeBase(EventDetailChart chart, BundleType eventBundle, ParentNodeType parentNode) {
this.eventBundle = eventBundle;
this.parentNode = parentNode;
this.chart = chart;
this.descLOD.set(eventBundle.getDescriptionLoD());
sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase();
eventsModel = chart.getController().getEventsModel();
evtColor = getEventType().getColor();
defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY));
highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY));
SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2)));
if (eventBundle.getEventIDsWithHashHits().isEmpty()) {
show(hashIV, false);
}
if (eventBundle.getEventIDsWithTags().isEmpty()) {
show(tagIV, false);
}
setBackground(defaultBackground);
setAlignment(Pos.TOP_LEFT);
setPrefHeight(USE_COMPUTED_SIZE);
heightProperty().addListener((Observable observable) -> {
chart.layoutPlotChildren();
});
setMaxHeight(USE_PREF_SIZE);
setMaxWidth(USE_PREF_SIZE);
setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation());
//initialize info hbox
infoHBox.setMinWidth(USE_PREF_SIZE);
infoHBox.setMaxWidth(USE_PREF_SIZE);
infoHBox.setPadding(new Insets(2, 5, 2, 5));
infoHBox.setAlignment(Pos.TOP_LEFT);
infoHBox.setPickOnBounds(true);
//set up subnode pane sizing contraints
subNodePane.setPrefHeight(USE_COMPUTED_SIZE);
subNodePane.setMaxHeight(USE_PREF_SIZE);
subNodePane.setPrefWidth(USE_COMPUTED_SIZE);
subNodePane.setMinWidth(USE_PREF_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
//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();
showHoverControls(true);
toFront();
});
setOnMouseExited((MouseEvent event) -> {
showHoverControls(false);
if (parentNode != null) {
parentNode.showHoverControls(true);
}
});
setDescriptionVisibility(DescriptionVisibility.SHOWN);
descVisibility.addListener((ObservableValue<? extends DescriptionVisibility> observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> {
setDescriptionVisibility(newValue);
});
}
final DescriptionLoD getDescriptionLoD() {
return descLOD.get();
}
public final BundleType getEventBundle() {
return eventBundle;
}
final double getLayoutXCompensation() {
return parentNode != null
? chart.getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis()))
: 0;
}
@NbBundle.Messages({"# {0} - counts",
"# {1} - event type",
"# {2} - description",
"# {3} - start date/time",
"# {4} - end date/time",
"EventBundleNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"})
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void installTooltip() {
if (tooltip == null) {
final Task<String> tooltTipTask = new Task<String>() {
@Override
protected String call() throws Exception {
HashMap<String, Long> hashSetCounts = new HashMap<>();
if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) {
try {
//TODO:push this to DB
for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) {
Set<String> 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);
}
}
String hashSetCountsString = hashSetCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
Map<String, Long> tagCounts = new HashMap<>();
if (eventBundle.getEventIDsWithTags().isEmpty() == false) {
tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags()));
}
String tagCountsString = tagCounts.entrySet().stream()
.map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
.collect(Collectors.joining("\n"));
return Bundle.EventBundleNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
TimeLineController.getZonedFormatter().print(getStartMillis()),
TimeLineController.getZonedFormatter().print(getEndMillis() + 1000))
+ (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString)
+ (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString);
}
@Override
protected void succeeded() {
super.succeeded();
try {
tooltip = new Tooltip(get());
tooltip.setAutoHide(true);
Tooltip.install(EventBundleNodeBase.this, tooltip);
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex);
Tooltip.uninstall(EventBundleNodeBase.this, tooltip);
tooltip = null;
}
}
};
chart.getController().monitorTask(tooltTipTask);
}
}
/**
* apply the 'effect' to visually indicate selection
*
* @param applied true to apply the selection 'effect', false to remove it
*/
public void applySelectionEffect(boolean applied) {
setBorder(applied ? SELECTION_BORDER : null);
}
/**
* apply the 'effect' to visually indicate highlighted nodes
*
* @param applied true to apply the highlight 'effect', false to remove it
*/
abstract void applyHighlightEffect(boolean applied);
@SuppressWarnings("unchecked")
public List<ParentNodeType> getSubNodes() {
return subNodes;
}
abstract void setDescriptionVisibility(DescriptionVisibility get);
void showHoverControls(final boolean showControls) {
Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
eventType -> new DropShadow(-10, eventType.getColor()));
setEffect(showControls ? dropShadow : null);
if (parentNode != null) {
parentNode.showHoverControls(false);
}
}
final EventType getEventType() {
return getEventBundle().getEventType();
}
final String getDescription() {
return getEventBundle().getDescription();
}
final long getStartMillis() {
return getEventBundle().getStartMillis();
}
final long getEndMillis() {
return getEventBundle().getEndMillis();
}
final Set<Long> getEventIDs() {
return getEventBundle().getEventIDs();
}
@Override
protected void layoutChildren() {
chart.layoutEventBundleNodes(subNodes, 0);
super.layoutChildren();
}
/**
* @param w the maximum width the description label should have
*/
abstract void setDescriptionWidth(double w);
void setDescriptionVisibilityLevel(DescriptionVisibility get) {
descVisibility.set(get);
}
}
/*
* Autopsy Forensic Browser
*
* Copyright 2013-15 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.ui.detailview;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import static java.util.Objects.nonNull;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.VBox;
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.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.EventCluster;
import org.sleuthkit.autopsy.timeline.datamodel.EventStripe;
import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter;
import org.sleuthkit.autopsy.timeline.filters.RootFilter;
import org.sleuthkit.autopsy.timeline.filters.TypeFilter;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show;
import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.autopsy.timeline.zooming.ZoomParams;
/**
*
*/
final public class EventClusterNode extends EventBundleNodeBase<EventCluster, EventStripe, EventStripeNode> {
private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName());
private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1);
private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N
private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N
private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS));
final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE);
public EventClusterNode(EventDetailChart chart, EventCluster eventCluster, EventStripeNode parentNode) {
super(chart, eventCluster, parentNode);
setMinHeight(24);
subNodePane.setBorder(clusterBorder);
subNodePane.setBackground(defaultBackground);
subNodePane.setMaxHeight(USE_COMPUTED_SIZE);
subNodePane.setMaxWidth(USE_PREF_SIZE);
subNodePane.setMinWidth(1);
setCursor(Cursor.HAND);
setOnMouseClicked(new MouseClickHandler());
configureLoDButton(plusButton);
configureLoDButton(minusButton);
setAlignment(Pos.CENTER_LEFT);
infoHBox.getChildren().addAll(minusButton, plusButton);
getChildren().addAll(subNodePane, infoHBox);
}
@Override
void showHoverControls(final boolean showControls) {
super.showHoverControls(showControls);
show(plusButton, showControls);
show(minusButton, showControls);
}
@Override
void applyHighlightEffect(boolean applied) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
void setDescriptionWidth(double max) {
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
@Override
public void setDescriptionVisibility(DescriptionVisibility descrVis) {
final int size = getEventBundle().getEventIDs().size();
switch (descrVis) {
case HIDDEN:
countLabel.setText("");
descrLabel.setText("");
break;
case COUNT_ONLY:
descrLabel.setText("");
countLabel.setText(String.valueOf(size));
break;
default:
case SHOWN:
countLabel.setText(String.valueOf(size));
break;
}
}
/**
* loads sub-bundles at the given Description LOD, continues
*
* @param requestedDescrLoD
* @param expand
*/
@NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters")
private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) {
chart.setCursor(Cursor.WAIT);
chart.getEventBundles().removeIf(bundle ->
subNodes.stream().anyMatch(subNode ->
bundle.equals(subNode.getEventStripe()))
);
subNodes.clear();
/*
* make new ZoomParams to query with
*
* 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 RootFilter subClusterFilter = getSubClusterFilter();
final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000);
final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get();
final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD());
Task<Collection<EventStripe>> loggedTask = new Task<Collection<EventStripe>>() {
private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail);
{
updateTitle(Bundle.EventStripeNode_loggedTask_name());
}
@Override
protected Collection<EventStripe> call() throws Exception {
Collection<EventStripe> bundles;
DescriptionLoD next = loadedDescriptionLoD;
do {
loadedDescriptionLoD = next;
if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) {
return Collections.emptySet();
}
bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream()
.collect(Collectors.toMap(EventCluster::getDescription, //key
(eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value
EventStripe::merge) //merge method
).values();
next = loadedDescriptionLoD.withRelativeDetail(relativeDetail);
} while (bundles.size() == 1 && nonNull(next));
// return list of AbstractEventStripeNodes representing sub-bundles
return bundles;
}
@Override
protected void succeeded() {
try {
Collection<EventStripe> bundles = get();
if (bundles.isEmpty()) {
subNodePane.getChildren().clear();
getChildren().setAll(subNodePane, infoHBox);
descLOD.set(getEventBundle().getDescriptionLoD());
} else {
chart.getEventBundles().addAll(bundles);
subNodes.addAll(bundles.stream()
.map(EventClusterNode.this::createStripeNode)
.sorted(Comparator.comparing(EventStripeNode::getStartMillis))
.collect(Collectors.toList()));
subNodePane.getChildren().setAll(subNodes);
getChildren().setAll(new VBox(infoHBox, subNodePane));
descLOD.set(loadedDescriptionLoD);
}
} catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.SEVERE, "Error loading subnodes", ex);
}
chart.layoutPlotChildren();
chart.setCursor(null);
}
};
//start task
chart.getController().monitorTask(loggedTask);
}
private EventStripeNode createStripeNode(EventStripe stripe) {
return new EventStripeNode(chart, stripe, this);
}
EventCluster getEventCluster() {
return getEventBundle();
}
@Override
protected void layoutChildren() {
double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis()));
double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX;
subNodePane.setPrefWidth(w);
subNodePane.setMinWidth(Math.max(1, w));
super.layoutChildren();
}
/**
* 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(), DescriptionFilter.FilterMode.INCLUDE),
new TypeFilter(getEventType()));
return subClusterFilter;
}
/**
* event handler used for mouse events on {@link EventStripeNode}s
*/
private class MouseClickHandler implements EventHandler<MouseEvent> {
private ContextMenu contextMenu;
@Override
public void handle(MouseEvent t) {
if (t.getButton() == MouseButton.PRIMARY) {
if (t.isShiftDown()) {
if (chart.selectedNodes.contains(EventClusterNode.this) == false) {
chart.selectedNodes.add(EventClusterNode.this);
}
} else if (t.isShortcutDown()) {
chart.selectedNodes.removeAll(EventClusterNode.this);
} else if (t.getClickCount() > 1) {
final DescriptionLoD next = descLOD.get().moreDetailed();
if (next != null) {
loadSubBundles(DescriptionLoD.RelativeDetail.MORE);
}
} else {
chart.selectedNodes.setAll(EventClusterNode.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(new ExpandClusterAction()));
contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction()));
contextMenu.getItems().add(new SeparatorMenuItem());
contextMenu.getItems().addAll(chartContextMenu.getItems());
}
contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY());
t.consume();
}
}
}
private class ExpandClusterAction extends Action {
@NbBundle.Messages(value = "ExpandClusterAction.text=Expand")
ExpandClusterAction() {
super(Bundle.ExpandClusterAction_text());
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 {
@NbBundle.Messages(value = "CollapseClusterAction.text=Collapse")
CollapseClusterAction() {
super(Bundle.CollapseClusterAction_text());
setGraphic(new ImageView(MINUS));
setEventHandler((ActionEvent t) -> {
final DescriptionLoD previous = descLOD.get().lessDetailed();
if (previous != null) {
loadSubBundles(DescriptionLoD.RelativeDetail.LESS);
}
});
disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD));
}
}
}
......@@ -35,13 +35,13 @@ class EventDescriptionTreeItem extends NavTreeItem {
* maps a description to the child item of this item with that description
*/
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
private final EventBundle bundle;
private final EventBundle<?> bundle;
public EventBundle getEventBundle() {
public EventBundle<?> getEventBundle() {
return bundle;
}
EventDescriptionTreeItem(EventBundle g) {
EventDescriptionTreeItem(EventBundle<?> g) {
bundle = g;
setValue(g);
}
......@@ -51,8 +51,8 @@ public long getCount() {
return getValue().getCount();
}
public void insert(Deque<EventBundle> path) {
EventBundle head = path.removeFirst();
public void insert(Deque<EventBundle<?>> path) {
EventBundle<?> head = path.removeFirst();
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
if (treeItem == null) {
treeItem = new EventDescriptionTreeItem(head);
......@@ -68,12 +68,12 @@ public void insert(Deque<EventBundle> path) {
}
@Override
public void resort(Comparator<TreeItem<EventBundle>> comp) {
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
FXCollections.sort(getChildren(), comp);
}
@Override
public NavTreeItem findTreeItemForEvent(EventBundle t) {
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
if (getValue().getEventType() == t.getEventType()
&& getValue().getDescription().equals(t.getDescription())) {
......
......@@ -33,9 +33,9 @@ class EventTypeTreeItem extends NavTreeItem {
*/
private final Map<String, EventDescriptionTreeItem> childMap = new ConcurrentHashMap<>();
private final Comparator<TreeItem<EventBundle>> comparator = TreeComparator.Description;
private final Comparator<TreeItem<EventBundle<?>>> comparator = TreeComparator.Description;
EventTypeTreeItem(EventBundle g) {
EventTypeTreeItem(EventBundle<?> g) {
setValue(g);
}
......@@ -44,8 +44,8 @@ public long getCount() {
return getValue().getCount();
}
public void insert(Deque<EventBundle> path) {
EventBundle head = path.removeFirst();
public void insert(Deque<EventBundle<?>> path) {
EventBundle<?> head = path.removeFirst();
EventDescriptionTreeItem treeItem = childMap.get(head.getDescription());
if (treeItem == null) {
treeItem = new EventDescriptionTreeItem(head);
......@@ -61,7 +61,7 @@ public void insert(Deque<EventBundle> path) {
}
@Override
public NavTreeItem findTreeItemForEvent(EventBundle t) {
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) {
for (EventDescriptionTreeItem child : childMap.values()) {
......@@ -75,7 +75,7 @@ public NavTreeItem findTreeItemForEvent(EventBundle t) {
}
@Override
public void resort(Comparator<TreeItem<EventBundle>> comp) {
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
FXCollections.sort(getChildren(), comp);
}
}
......@@ -68,13 +68,13 @@ public class NavPanel extends BorderPane implements TimeLineView {
private DetailViewPane detailViewPane;
@FXML
private TreeView<EventBundle> eventsTree;
private TreeView<EventBundle<?>> eventsTree;
@FXML
private Label eventsTreeLabel;
@FXML
private ComboBox<Comparator<TreeItem<EventBundle>>> sortByBox;
private ComboBox<Comparator<TreeItem<EventBundle<?>>>> sortByBox;
public NavPanel() {
FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS
......@@ -91,8 +91,8 @@ public void setDetailViewPane(DetailViewPane detailViewPane) {
detailViewPane.getSelectedNodes().addListener((Observable observable) -> {
eventsTree.getSelectionModel().clearSelection();
detailViewPane.getSelectedNodes().forEach(eventStripeNode -> {
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventStripeNode.getEventStripe()));
detailViewPane.getSelectedNodes().forEach(eventBundleNode -> {
eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle()));
});
});
......@@ -105,7 +105,7 @@ private NavTreeItem getRoot() {
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void setRoot() {
RootItem root = new RootItem();
for (EventBundle bundle : detailViewPane.getEventBundles()) {
for (EventBundle<?> bundle : detailViewPane.getEventBundles()) {
root.insert(bundle);
}
eventsTree.setRoot(root);
......@@ -134,7 +134,7 @@ void initialize() {
getRoot().resort(sortByBox.getSelectionModel().getSelectedItem());
});
eventsTree.setShowRoot(false);
eventsTree.setCellFactory((TreeView<EventBundle> p) -> new EventBundleTreeCell());
eventsTree.setCellFactory((TreeView<EventBundle<?>> p) -> new EventBundleTreeCell());
eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text"));
......@@ -144,7 +144,7 @@ void initialize() {
* A tree cell to display {@link EventBundle}s. Shows the description, and
* count, as well a a "legend icon" for the event type.
*/
private class EventBundleTreeCell extends TreeCell<EventBundle> {
private class EventBundleTreeCell extends TreeCell<EventBundle<?>> {
private static final double HIDDEN_MULTIPLIER = .6;
private final Rectangle rect = new Rectangle(24, 24);
......@@ -158,7 +158,7 @@ private class EventBundleTreeCell extends TreeCell<EventBundle> {
}
@Override
protected void updateItem(EventBundle item, boolean empty) {
protected void updateItem(EventBundle<?> item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
......@@ -177,7 +177,7 @@ protected void updateItem(EventBundle item, boolean empty) {
});
registerListeners(controller.getQuickHideFilters(), item);
String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS
TreeItem<EventBundle> parent = getTreeItem().getParent();
TreeItem<EventBundle<?>> parent = getTreeItem().getParent();
if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) {
text = StringUtils.substringAfter(text, parent.getValue().getDescription());
}
......@@ -189,7 +189,7 @@ protected void updateItem(EventBundle item, boolean empty) {
}
}
private void registerListeners(Collection<? extends DescriptionFilter> filters, EventBundle item) {
private void registerListeners(Collection<? extends DescriptionFilter> filters, EventBundle<?> item) {
for (DescriptionFilter filter : filters) {
if (filter.getDescription().equals(item.getDescription())) {
filter.activeProperty().addListener(filterStateChangeListener);
......@@ -205,8 +205,8 @@ private void deRegisterListeners(Collection<? extends DescriptionFilter> filters
}
}
private void updateHiddenState(EventBundle item) {
TreeItem<EventBundle> treeItem = getTreeItem();
private void updateHiddenState(EventBundle<?> item) {
TreeItem<EventBundle<?>> treeItem = getTreeItem();
ContextMenu newMenu;
if (controller.getQuickHideFilters().stream().
filter(AbstractFilter::isActive)
......
......@@ -28,11 +28,11 @@
* {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type,
* description , count, etc.
*/
abstract class NavTreeItem extends TreeItem<EventBundle> {
abstract class NavTreeItem extends TreeItem<EventBundle<?>> {
abstract long getCount();
abstract void resort(Comparator<TreeItem<EventBundle>> comp);
abstract void resort(Comparator<TreeItem<EventBundle<?>>> comp);
abstract NavTreeItem findTreeItemForEvent(EventBundle t);
abstract NavTreeItem findTreeItemForEvent(EventBundle<?> t);
}
......@@ -56,7 +56,7 @@ public long getCount() {
*
* @param g Group to add
*/
public void insert(EventBundle g) {
public void insert(EventBundle<?> g) {
EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(),
baseType -> {
......@@ -69,12 +69,12 @@ public void insert(EventBundle g) {
treeItem.insert(getTreePath(g));
}
static Deque<EventBundle> getTreePath(EventBundle g) {
Deque<EventBundle> path = new ArrayDeque<>();
Optional<EventBundle> p = Optional.of(g);
static Deque< EventBundle<?>> getTreePath(EventBundle<?> g) {
Deque<EventBundle<?>> path = new ArrayDeque<>();
Optional<? extends EventBundle<?>> p = Optional.of(g);
while (p.isPresent()) {
EventBundle parent = p.get();
EventBundle<?> parent = p.get();
path.addFirst(parent);
p = parent.getParentBundle();
}
......@@ -83,12 +83,12 @@ static Deque<EventBundle> getTreePath(EventBundle g) {
}
@Override
public void resort(Comparator<TreeItem<EventBundle>> comp) {
public void resort(Comparator<TreeItem<EventBundle<?>>> comp) {
childMap.values().forEach(ti -> ti.resort(comp));
}
@Override
public NavTreeItem findTreeItemForEvent(EventBundle t) {
public NavTreeItem findTreeItemForEvent(EventBundle<?> t) {
for (EventTypeTreeItem child : childMap.values()) {
final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t);
if (findTreeItemForEvent != null) {
......
......@@ -23,23 +23,23 @@
import org.sleuthkit.autopsy.timeline.datamodel.EventBundle;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
enum TreeComparator implements Comparator<TreeItem<EventBundle>> {
enum TreeComparator implements Comparator<TreeItem<EventBundle<?>>> {
Description {
@Override
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
return o1.getValue().getDescription().compareTo(o2.getValue().getDescription());
}
},
Count {
@Override
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
return Long.compare(o2.getValue().getCount(), o1.getValue().getCount());
}
},
Type {
@Override
public int compare(TreeItem<EventBundle> o1, TreeItem<EventBundle> o2) {
public int compare(TreeItem<EventBundle<?>> o1, TreeItem<EventBundle<?>> o2) {
return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType());
}
};
......
......@@ -43,7 +43,7 @@ public DescriptionLoD moreDetailed() {
try {
return values()[ordinal() + 1];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
return FULL;
}
}
......@@ -51,7 +51,7 @@ public DescriptionLoD lessDetailed() {
try {
return values()[ordinal() - 1];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
return SHORT;
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment