org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.java

Source

/*
 * Autopsy Forensic Browser
 *
 * Copyright 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.ui.detailview;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.eventbus.Subscribe;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
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.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.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
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.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.FilteredEventsModel;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.events.TagsAddedEvent;
import org.sleuthkit.autopsy.timeline.events.TagsDeletedEvent;
import org.sleuthkit.autopsy.timeline.ui.AbstractTimelineChart;
import org.sleuthkit.autopsy.timeline.ui.ContextMenuProvider;
import static org.sleuthkit.autopsy.timeline.ui.detailview.EventNodeBase.show;
import static org.sleuthkit.autopsy.timeline.ui.detailview.MultiEventNodeBase.CORNER_RADII_3;
import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;

/**
 *
 */
public abstract class EventNodeBase<Type extends TimeLineEvent> extends StackPane implements ContextMenuProvider {

    private static final Logger LOGGER = Logger.getLogger(EventNodeBase.class.getName());

    private static final Image HASH_HIT = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N NON-NLS
    private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N
    private static final Image PIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--plus.png"); // NON-NLS //NOI18N
    private static final Image UNPIN = new Image("/org/sleuthkit/autopsy/timeline/images/marker--minus.png"); // NON-NLS //NOI18N

    private static final Map<EventType, Effect> dropShadowMap = new ConcurrentHashMap<>();

    static void configureActionButton(ButtonBase b) {
        b.setMinSize(16, 16);
        b.setMaxSize(16, 16);
        b.setPrefSize(16, 16);
    }

    static void show(Node b, boolean show) {
        b.setVisible(show);
        b.setManaged(show);
    }

    private final Type tlEvent;

    private final EventNodeBase<?> parentNode;

    final DetailsChartLane<?> chartLane;
    final Background highlightedBackground;
    final Background defaultBackground;
    final Color evtColor;

    final Label countLabel = new Label();
    final Label descrLabel = new Label();
    final ImageView hashIV = new ImageView(HASH_HIT);
    final ImageView tagIV = new ImageView(TAG);
    final ImageView eventTypeImageView = new ImageView();

    final Tooltip tooltip = new Tooltip(Bundle.EventBundleNodeBase_toolTip_loading());

    final HBox controlsHBox = new HBox(5);
    final HBox infoHBox = new HBox(5, eventTypeImageView, hashIV, tagIV, descrLabel, countLabel, controlsHBox);
    final SleuthkitCase sleuthkitCase;
    final FilteredEventsModel eventsModel;
    private Timeline timeline;
    private Button pinButton;
    private final Border SELECTION_BORDER;

    EventNodeBase(Type tlEvent, EventNodeBase<?> parent, DetailsChartLane<?> chartLane) {
        this.chartLane = chartLane;
        this.tlEvent = tlEvent;
        this.parentNode = parent;

        sleuthkitCase = chartLane.getController().getAutopsyCase().getSleuthkitCase();
        eventsModel = chartLane.getController().getEventsModel();
        eventTypeImageView.setImage(getEventType().getFXImage());

        if (tlEvent.getEventIDsWithHashHits().isEmpty()) {
            show(hashIV, false);
        }

        if (tlEvent.getEventIDsWithTags().isEmpty()) {
            show(tagIV, false);
        }

        if (chartLane.getController().getEventsModel().getEventTypeZoom() == EventTypeZoomLevel.SUB_TYPE) {
            evtColor = getEventType().getColor();
        } else {
            evtColor = getEventType().getBaseType().getColor();
        }
        SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID,
                CORNER_RADII_3, new BorderWidths(2)));

        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));
        setBackground(defaultBackground);

        Tooltip.install(this, this.tooltip);

        //set up mouse hover effect and tooltip
        setOnMouseEntered(mouseEntered -> {
            Tooltip.uninstall(chartLane, AbstractTimelineChart.getDefaultTooltip());
            showHoverControls(true);
            toFront();
        });
        setOnMouseExited(mouseExited -> {
            showHoverControls(false);
            if (parentNode != null) {
                parentNode.showHoverControls(true);
            } else {
                Tooltip.install(chartLane, AbstractTimelineChart.getDefaultTooltip());
            }
        });
        setOnMouseClicked(new ClickHandler());
        show(controlsHBox, false);
    }

    public Type getEvent() {
        return tlEvent;
    }

    @Override
    public TimeLineController getController() {
        return chartLane.getController();
    }

    public Optional<EventNodeBase<?>> getParentNode() {
        return Optional.ofNullable(parentNode);
    }

    DetailsChartLane<?> getChartLane() {
        return chartLane;
    }

    /**
     * @param w the maximum width the description label should have
     */
    public void setMaxDescriptionWidth(double w) {
        descrLabel.setMaxWidth(w);
    }

    public abstract List<EventNodeBase<?>> getSubNodes();

    /**
     * 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);
    }

    protected void layoutChildren() {
        super.layoutChildren();
    }

    /**
     * Install whatever buttons are visible on hover for this node. likes
     * tooltips, this had a surprisingly large impact on speed of loading the
     * chart
     */
    void installActionButtons() {
        if (pinButton == null) {
            pinButton = new Button();
            controlsHBox.getChildren().add(pinButton);
            configureActionButton(pinButton);
        }
    }

    final void showHoverControls(final boolean showControls) {
        Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(),
                eventType -> new DropShadow(-10, eventType.getColor()));
        setEffect(showControls ? dropShadow : null);
        installTooltip();
        enableTooltip(showControls);
        installActionButtons();

        TimeLineController controller = getChartLane().getController();

        if (controller.getPinnedEvents().contains(tlEvent)) {
            pinButton.setOnAction(actionEvent -> {
                new UnPinEventAction(controller, tlEvent).handle(actionEvent);
                showHoverControls(true);
            });
            pinButton.setGraphic(new ImageView(UNPIN));
        } else {
            pinButton.setOnAction(actionEvent -> {
                new PinEventAction(controller, tlEvent).handle(actionEvent);
                showHoverControls(true);
            });
            pinButton.setGraphic(new ImageView(PIN));
        }

        show(controlsHBox, showControls);
        if (parentNode != null) {
            parentNode.showHoverControls(false);
        }
    }

    /**
     * defer tooltip content creation till needed, this had a surprisingly large
     * impact on speed of loading the chart
     */
    @NbBundle.Messages({ "# {0} - counts", "# {1} - event type", "# {2} - description", "# {3} - start date/time",
            "# {4} - end date/time", "EventNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand   \t{4}",
            "EventNodeBase.toolTip.loading2=loading tooltip", "# {0} - hash set count string",
            "EventNodeBase.toolTip.hashSetHits=\n\nHash Set Hits\n{0}", "# {0} - tag count string",
            "EventNodeBase.toolTip.tags=\n\nTags\n{0}" })
    @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
    void installTooltip() {
        if (tooltip.getText().equalsIgnoreCase(Bundle.EventBundleNodeBase_toolTip_loading())) {
            final Task<String> tooltTipTask = new Task<String>() {
                {
                    updateTitle(Bundle.EventNodeBase_toolTip_loading2());
                }

                @Override
                protected String call() throws Exception {
                    HashMap<String, Long> hashSetCounts = new HashMap<>();
                    if (tlEvent.getEventIDsWithHashHits().isEmpty() == false) {
                        try {
                            //TODO:push this to DB
                            for (SingleEvent tle : eventsModel.getEventsById(tlEvent.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); //NON-NLS
                        }
                    }
                    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 (tlEvent.getEventIDsWithTags().isEmpty() == false) {
                        tagCounts.putAll(eventsModel.getTagCountsByTagName(tlEvent.getEventIDsWithTags()));
                    }
                    String tagCountsString = tagCounts.entrySet().stream()
                            .map((Map.Entry<String, Long> t) -> t.getKey() + " : " + t.getValue())
                            .collect(Collectors.joining("\n"));

                    return Bundle.EventNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(),
                            TimeLineController.getZonedFormatter().print(getStartMillis()),
                            TimeLineController.getZonedFormatter().print(getEndMillis() + 1000))
                            + (hashSetCountsString.isEmpty() ? ""
                                    : Bundle.EventNodeBase_toolTip_hashSetHits(hashSetCountsString))
                            + (tagCountsString.isEmpty() ? "" : Bundle.EventNodeBase_toolTip_tags(tagCountsString));
                }

                @Override
                protected void succeeded() {
                    super.succeeded();
                    try {
                        tooltip.setText(get());
                        tooltip.setGraphic(null);
                    } catch (InterruptedException | ExecutionException ex) {
                        LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); //NON-NLS
                    }
                }
            };
            new Thread(tooltTipTask).start();
            chartLane.getController().monitorTask(tooltTipTask);
        }
    }

    void enableTooltip(boolean toolTipEnabled) {
        if (toolTipEnabled) {
            Tooltip.install(this, tooltip);
        } else {
            Tooltip.uninstall(this, tooltip);
        }
    }

    final EventType getEventType() {
        return tlEvent.getEventType();
    }

    long getStartMillis() {
        return tlEvent.getStartMillis();
    }

    final long getEndMillis() {
        return tlEvent.getEndMillis();
    }

    final double getLayoutXCompensation() {
        return parentNode != null
                ? getChartLane().getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis()))
                : 0;
    }

    abstract String getDescription();

    void animateTo(double xLeft, double yTop) {
        if (timeline != null) {
            timeline.stop();
            Platform.runLater(this::requestChartLayout);
        }
        timeline = new Timeline(new KeyFrame(Duration.millis(100), new KeyValue(layoutXProperty(), xLeft),
                new KeyValue(layoutYProperty(), yTop)));
        timeline.setOnFinished(finished -> Platform.runLater(this::requestChartLayout));
        timeline.play();
    }

    void requestChartLayout() {
        getChartLane().requestChartLayout();
    }

    @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
    void setDescriptionVisibility(DescriptionVisibility descrVis) {
        final int size = getEvent().getSize();
        switch (descrVis) {
        case HIDDEN:
            hideDescription();
            break;
        case COUNT_ONLY:
            showCountOnly(size);
            break;
        case SHOWN:
        default:
            showFullDescription(size);
            break;
        }
    }

    void showCountOnly(final int size) {
        descrLabel.setText("");
        countLabel.setText(String.valueOf(size));
    }

    void hideDescription() {
        countLabel.setText("");
        descrLabel.setText("");
    }

    /**
     * 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
            setBackground(highlightedBackground);
        } else {
            descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS
            setBackground(defaultBackground);
        }
    }

    void applyHighlightEffect() {
        applyHighlightEffect(true);
    }

    void clearHighlightEffect() {
        applyHighlightEffect(false);
    }

    abstract Collection<Long> getEventIDs();

    abstract EventHandler<MouseEvent> getDoubleClickHandler();

    Iterable<? extends Action> getActions() {
        if (getController().getPinnedEvents().contains(getEvent())) {
            return Arrays.asList(new UnPinEventAction(getController(), getEvent()));
        } else {
            return Arrays.asList(new PinEventAction(getController(), getEvent()));
        }
    }

    @Deprecated
    @Override
    final public void clearContextMenu() {
    }

    public ContextMenu getContextMenu(MouseEvent mouseEvent) {
        ContextMenu chartContextMenu = chartLane.getContextMenu(mouseEvent);

        ContextMenu contextMenu = ActionUtils.createContextMenu(Lists.newArrayList(getActions()));
        contextMenu.getItems().add(new SeparatorMenuItem());
        contextMenu.getItems().addAll(chartContextMenu.getItems());
        contextMenu.setAutoHide(true);
        return contextMenu;
    }

    void showFullDescription(final int size) {
        countLabel.setText((size == 1) ? "" : " (" + size + ")"); // NON-NLS
        String description = getParentNode()
                .map(pNode -> "    ..."
                        + StringUtils.substringAfter(getEvent().getDescription(), parentNode.getDescription()))
                .orElseGet(getEvent()::getDescription);

        descrLabel.setText(description);
    }

    @Subscribe
    public void handleTimeLineTagEvent(TagsAddedEvent event) {
        if (false == Sets.intersection(getEvent().getEventIDs(), event.getUpdatedEventIDs()).isEmpty()) {
            Platform.runLater(() -> {
                show(tagIV, true);
            });
        }
    }

    /**
     * TODO: this method implementation is wrong and just a place holder
     */
    @Subscribe
    public void handleTimeLineTagEvent(TagsDeletedEvent event) {
        Sets.SetView<Long> difference = Sets.difference(getEvent().getEventIDs(), event.getUpdatedEventIDs());

        if (false == difference.isEmpty()) {
            Platform.runLater(() -> {
                show(tagIV, true);
            });
        }
    }

    private static class PinEventAction extends Action {

        @NbBundle.Messages({ "PinEventAction.text=Pin" })
        PinEventAction(TimeLineController controller, TimeLineEvent event) {
            super(Bundle.PinEventAction_text());
            setEventHandler(actionEvent -> controller.pinEvent(event));
            setGraphic(new ImageView(PIN));
        }
    }

    private static class UnPinEventAction extends Action {

        @NbBundle.Messages({ "UnPinEventAction.text=Unpin" })
        UnPinEventAction(TimeLineController controller, TimeLineEvent event) {
            super(Bundle.UnPinEventAction_text());
            setEventHandler(actionEvent -> controller.unPinEvent(event));
            setGraphic(new ImageView(UNPIN));
        }
    }

    /**
     * event handler used for mouse events on {@link EventNodeBase}s
     */
    private class ClickHandler implements EventHandler<MouseEvent> {

        @Override
        public void handle(MouseEvent t) {
            if (t.getButton() == MouseButton.PRIMARY) {
                if (t.getClickCount() > 1) {
                    getDoubleClickHandler().handle(t);
                } else if (t.isShiftDown()) {
                    chartLane.getSelectedNodes().add(EventNodeBase.this);
                } else if (t.isShortcutDown()) {
                    chartLane.getSelectedNodes().removeAll(EventNodeBase.this);
                } else {
                    chartLane.getSelectedNodes().setAll(EventNodeBase.this);
                }
                t.consume();
            } else if (t.isPopupTrigger() && t.isStillSincePress()) {
                getContextMenu(t).show(EventNodeBase.this, t.getScreenX(), t.getScreenY());
                t.consume();
            }
        }
    }
}