org.sleuthkit.autopsy.timeline.ShowInTimelineDialog.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.timeline.ShowInTimelineDialog.java

Source

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

import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.util.converter.IntegerStringConverter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.controlsfx.validation.ValidationMessage;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.joda.time.Interval;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent;
import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType;
import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent;
import org.sleuthkit.autopsy.timeline.utils.IntervalUtils;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException;

/**
 * A Dialog that, given an AbstractFile or BlackBoardArtifact, allows the user
 * to choose a specific event and a time range around it to show in the Timeline
 * List View.
 */
final class ShowInTimelineDialog extends Dialog<ViewInTimelineRequestedEvent> {

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

    @NbBundle.Messages({ "ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline" })
    private static final ButtonType SHOW = new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(),
            ButtonBar.ButtonData.OK_DONE);

    /**
     * List of ChronoUnits the user can select from when choosing a time range
     * to show.
     */
    private static final List<ChronoField> SCROLL_BY_UNITS = Arrays.asList(ChronoField.YEAR,
            ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY,
            ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE);

    @FXML
    private TableView<SingleEvent> eventTable;

    @FXML
    private TableColumn<SingleEvent, EventType> typeColumn;

    @FXML
    private TableColumn<SingleEvent, Long> dateTimeColumn;

    @FXML
    private Spinner<Integer> amountSpinner;

    @FXML
    private ComboBox<ChronoField> unitComboBox;

    @FXML
    private Label chooseEventLabel;

    private final VBox contentRoot = new VBox();

    private final TimeLineController controller;

    private final ValidationSupport validationSupport = new ValidationSupport();

    /**
     * Common Private Constructor
     *
     * @param controller The controller for this Dialog.
     * @param eventIDS   A List of eventIDs to present to the user to choose
     *                   from.
     */
    @NbBundle.Messages({
            "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits." })
    private ShowInTimelineDialog(TimeLineController controller, List<Long> eventIDS) {
        this.controller = controller;

        //load dialog content fxml
        final String name = "nbres:/"
                + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(), ".", "/")
                + "/ShowInTimelineDialog.fxml"; // NON-NLS
        try {
            FXMLLoader fxmlLoader = new FXMLLoader(new URL(name));
            fxmlLoader.setRoot(contentRoot);
            fxmlLoader.setController(this);

            fxmlLoader.load();
        } catch (IOException ex) {
            LOGGER.log(Level.SEVERE, "Unable to load FXML, node initialization may not be complete.", ex); //NON-NLS
        }
        //assert that fxml loading happened correctly
        assert eventTable != null : "fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
        assert typeColumn != null : "fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
        assert dateTimeColumn != null : "fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
        assert amountSpinner != null : "fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";
        assert unitComboBox != null : "fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'.";

        //validat that spinner has a integer in the text field.
        validationSupport.registerValidator(amountSpinner.getEditor(), false, Validator.createPredicateValidator(
                NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message()));

        //configure dialog properties
        PromptDialogManager.setDialogIcons(this);
        initModality(Modality.APPLICATION_MODAL);

        //add scenegraph loaded from fxml to this dialog.
        DialogPane dialogPane = getDialogPane();
        dialogPane.setContent(contentRoot);
        //add buttons to dialog
        dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL);

        ///configure dialog controls
        amountSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000));
        amountSpinner.getValueFactory().setConverter(new IntegerStringConverter() {
            /**
             * Convert the String to an Integer using Integer.valueOf, but if
             * that throws a NumberFormatException, reset the spinner to the
             * last valid value.
             *
             * @param string The String to convert
             *
             * @return The Integer value of string.
             */
            @Override
            public Integer fromString(String string) {
                try {
                    return super.fromString(string);
                } catch (NumberFormatException ex) {
                    return amountSpinner.getValue();
                }
            }
        });

        unitComboBox.setButtonCell(new ChronoFieldListCell());
        unitComboBox.setCellFactory(comboBox -> new ChronoFieldListCell());
        unitComboBox.getItems().setAll(SCROLL_BY_UNITS);
        unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR);

        typeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getEventType()));
        typeColumn.setCellFactory(param -> new TypeTableCell<>());

        dateTimeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getStartMillis()));
        dateTimeColumn.setCellFactory(param -> new DateTimeTableCell<>());

        //add events to table
        eventTable.getItems().setAll(
                eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet()));
        eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28));
    }

    /**
     * Constructor for artifact based dialog. suppressed the choosing event
     * aspect as each artifact is assumed to have only one associated event.
     *
     * @param controller The controller for this Dialog
     * @param artifact   The BlackboardArtifact to configure this dialog for.
     */
    @NbBundle.Messages({ "ShowInTimelineDialog.artifactTitle=View Result in Timeline." })
    ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) {
        //get events IDs from artifact
        this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact));

        //hide instructional label and autoselect first(and only) event.
        chooseEventLabel.setVisible(false);
        chooseEventLabel.setManaged(false);
        eventTable.getSelectionModel().select(0);

        //require validation of ammount spinner to enable show button
        getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty());

        //set result converter that does not require selection.
        setResultConverter(
                buttonType -> (buttonType == SHOW) ? makeEventInTimeRange(eventTable.getItems().get(0)) : null);
        setTitle(Bundle.ShowInTimelineDialog_artifactTitle());
    }

    /**
     * Constructor for file based dialog. Allows the user to choose an event
     * (MAC time) derived from the given file
     *
     * @param controller The controller for this Dialog.
     * @param file       The AbstractFile to configure this dialog for.
     */
    @NbBundle.Messages({ "# {0} - file path", "ShowInTimelineDialog.fileTitle=View {0} in timeline.",
            "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event." })
    ShowInTimelineDialog(TimeLineController controller, AbstractFile file) {
        this(controller, controller.getEventsModel().getEventIDsForFile(file, false));

        /*
         * since ValidationSupport does not support list selection, we will
         * manually apply and remove decoration in response to selection
         * property changes.
         */
        eventTable.getSelectionModel().selectedItemProperty().isNull()
                .addListener((selectedItemNullProperty, wasNull, isNull) -> {
                    if (isNull) {
                        validationSupport.getValidationDecorator().applyValidationDecoration(ValidationMessage
                                .error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message()));
                    } else {
                        validationSupport.getValidationDecorator().removeDecorations(eventTable);
                    }
                });

        //require selection and validation of ammount spinner to enable show button
        getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or(validationSupport.invalidProperty(),
                eventTable.getSelectionModel().selectedItemProperty().isNull()));

        //set result converter that uses selection.
        setResultConverter(buttonType -> (buttonType == SHOW)
                ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem())
                : null);

        setTitle(Bundle.ShowInTimelineDialog_fileTitle(
                StringUtils.abbreviateMiddle(getContentPathSafe(file), " ... ", 50)));
    }

    /**
     * Get the unique path for the content, or if that fails, just return the
     * name.
     *
     * NOTE: This was copied from IamgeUtils and should be refactored to avoid
     * duplication.
     *
     * @param content
     *
     * @return the unique path for the content, or if that fails, just the name.
     */
    static String getContentPathSafe(Content content) {
        try {
            return content.getUniquePath();
        } catch (TskCoreException tskCoreException) {
            String contentName = content.getName();
            LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NON-NLS
            return contentName;
        }
    }

    /**
     * Construct this Dialog's "result" from the given event.
     *
     * @param selectedEvent The SingleEvent to include in the EventInTimeRange
     *
     * @return The EventInTimeRange that is the "result" of this dialog.
     */
    private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) {
        Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration()
                .multipliedBy(amountSpinner.getValue());
        Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()),
                selectedDuration);
        return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range);
    }

    /**
     * ListCell that shows a ChronoUnit
     */
    static private class ChronoUnitListCell extends ListCell<ChronoUnit> {

        @Override
        protected void updateItem(ChronoUnit item, boolean empty) {
            super.updateItem(item, empty);

            if (empty || item == null) {
                setText(null);
            } else {
                setText(WordUtils.capitalizeFully(item.toString()));
            }
        }
    }

    /**
     * TableCell that shows a formatted date/time for a given millisecond since
     * the unix epoch
     *
     * @param <X> Anything
     */
    static private class DateTimeTableCell<X> extends TableCell<X, Long> {

        @Override
        protected void updateItem(Long item, boolean empty) {
            super.updateItem(item, empty);

            if (item == null || empty) {
                setText(null);
            } else {
                setText(TimeLineController.getZonedFormatter().print(item));
            }
        }
    }

    /**
     * TableCell that shows a EventType including the associated icon.
     *
     * @param <X> Anything
     */
    static private class TypeTableCell<X> extends TableCell<X, EventType> {

        @Override
        protected void updateItem(EventType item, boolean empty) {
            super.updateItem(item, empty);

            if (item == null || empty) {
                setText(null);
                setGraphic(null);
            } else {
                setText(item.getDisplayName());
                setGraphic(new ImageView(item.getFXImage()));
            }
        }
    }
}