/*
* 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.experimental.autoingest;
import java.io.Serializable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Comparator;
import java.util.Date;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor;
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
import org.sleuthkit.autopsy.ingest.IngestJob;
/**
* An automated ingest job for a manifest. The manifest specifies a co-located
* data source and a case to which the data source is to be added.
*/
@ThreadSafe
public final class AutoIngestJob implements Comparable<AutoIngestJob>, Serializable {
private static final long serialVersionUID = 1L;
private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName();
private final Manifest manifest;
private final String nodeName;
@GuardedBy("this")
private String caseDirectoryPath;
@GuardedBy("this")
private Integer priority;
@GuardedBy("this")
private Stage stage;
@GuardedBy("this")
private Date stageStartDate;
@GuardedBy("this")
transient private DataSourceProcessor dataSourceProcessor;
@GuardedBy("this")
transient private IngestJob ingestJob;
@GuardedBy("this")
transient private boolean cancelled; // RJCTODO: Document
@GuardedBy("this")
transient private boolean completed; // RJCTODO: Document
@GuardedBy("this")
private Date completedDate;
@GuardedBy("this")
private boolean errorsOccurred;
/**
* Constructs an automated ingest job for a manifest. The manifest specifies
* a co-located data source and a case to which the data source is to be
* added.
*
* @param manifest The manifest
* @param caseDirectoryPath The path to the case directory for the job, may
* be null.
* @param priority The priority of the job. The higher the number,
* the higher the priority.
* @param nodeName If the job is in progress, the node doing the
* processing, otherwise the locla host.
* @param stage The processing stage for display purposes.
* @param completedDate The date when the job was completed. Use the
* epoch (January 1, 1970, 00:00:00 GMT) to
* indicate the the job is not completed, i.e., new
* Date(0L).
*/
// RJCTODO: The null case directory is error-prone and the nodeName is confusing.
AutoIngestJob(Manifest manifest, Path caseDirectoryPath, int priority, String nodeName, Stage stage, Date completedDate, boolean errorsOccurred) {
this.manifest = manifest;
if (null != caseDirectoryPath) {
this.caseDirectoryPath = caseDirectoryPath.toString();
} else {
this.caseDirectoryPath = "";
}
this.priority = priority;
this.nodeName = nodeName;
this.stage = stage;
this.stageStartDate = manifest.getDateFileCreated();
this.completedDate = completedDate;
this.errorsOccurred = errorsOccurred;
}
/**
* Gets the auto ingest jobmanifest.
*
* @return The manifest.
*/
Manifest getManifest() {
return this.manifest;
}
/**
* Queries whether or not a case directory path has been set for this auto
* ingest job.
*
* @return True or false
*/
// RJCTODO: Use this or lose this
synchronized boolean hasCaseDirectoryPath() {
return (false == this.caseDirectoryPath.isEmpty());
}
/**
* Sets the path to the case directory of the case associated with this job.
*
* @param caseDirectoryPath The path to the case directory.
*/
synchronized void setCaseDirectoryPath(Path caseDirectoryPath) {
this.caseDirectoryPath = caseDirectoryPath.toString();
}
/**
* Gets the path to the case directory of the case associated with this job,
* may be null.
*
* @return The case directory path or null if the case directory has not
* been created yet.
*/
synchronized Path getCaseDirectoryPath() {
if (!caseDirectoryPath.isEmpty()) {
return Paths.get(caseDirectoryPath);
} else {
return null;
}
}
/**
* Sets the priority of the job. A higher number indicates a higher
* priority.
*
* @param priority The priority.
*/
synchronized void setPriority(Integer priority) {
this.priority = priority;
}
/**
* Gets the priority of the job. A higher number indicates a higher
* priority.
*
* @return The priority.
*/
synchronized Integer getPriority() {
return this.priority;
}
/**
* RJCTODO
*
* @param newStage
*/
synchronized void setStage(Stage newStage) {
setStage(newStage, Date.from(Instant.now()));
}
/**
* RJCTODO
*
* @param state
* @param stateStartedDate
*/
synchronized void setStage(Stage newState, Date stateStartedDate) {
if (Stage.CANCELLING == this.stage && Stage.COMPLETED != newState) {
return;
}
this.stage = newState;
this.stageStartDate = stateStartedDate;
}
/**
* RJCTODO:
*
* @return
*/
synchronized Stage getStage() {
return this.stage;
}
/**
* RJCTODO
*
* @return
*/
synchronized Date getStageStartDate() {
return this.stageStartDate;
}
/**
* RJCTODO
*
* @return
*/
synchronized StageDetails getStageDetails() {
String description;
Date startDate;
if (Stage.CANCELLING != this.stage && null != this.ingestJob) {
IngestJob.ProgressSnapshot progress = this.ingestJob.getSnapshot();
IngestJob.DataSourceIngestModuleHandle ingestModuleHandle = progress.runningDataSourceIngestModule();
if (null != ingestModuleHandle) {
/**
* A first or second stage data source level ingest module is
* running. Reporting this takes precedence over reporting
* generic file analysis.
*/
startDate = ingestModuleHandle.startTime();
if (!ingestModuleHandle.isCancelled()) {
description = ingestModuleHandle.displayName();
} else {
description = String.format(Stage.CANCELLING_MODULE.getDisplayText(), ingestModuleHandle.displayName()); // RJCTODO: FIx this
}
} else {
/**
* If no data source level ingest module is running, then either
* it is still the first stage of analysis and file level ingest
* modules are running or another ingest job is still running.
* Note that there can be multiple ingest jobs running in
* parallel. For example, there is an ingest job created to
* ingest each extracted virtual machine.
*/
description = Stage.ANALYZING_FILES.getDisplayText();
startDate = progress.fileIngestStartTime();
}
} else {
description = this.stage.getDisplayText();
startDate = this.stageStartDate;
}
return new StageDetails(description, startDate);
}
synchronized void setDataSourceProcessor(DataSourceProcessor dataSourceProcessor) {
this.dataSourceProcessor = dataSourceProcessor;
}
/**
* RJCTODO
*/
// RJCTODO: Consider moving this class into AIM and making this private
synchronized void setIngestJob(IngestJob ingestJob) {
this.ingestJob = ingestJob;
}
/**
* RJCTODO
*/
// RJCTODO: Consider moving this class into AIM and making this private.
// Or move the AID into a separate package. Or do not worry about it.
synchronized IngestJob getIngestJob() {
return this.ingestJob;
}
/**
* RJCTODO
*/
synchronized void cancel() {
setStage(Stage.CANCELLING);
cancelled = true;
errorsOccurred = true;
if (null != dataSourceProcessor) {
dataSourceProcessor.cancel();
}
if (null != ingestJob) {
ingestJob.cancel(IngestJob.CancellationReason.USER_CANCELLED);
}
}
/**
* RJCTODO
*/
synchronized boolean isCancelled() {
return cancelled;
}
/**
* RJCTODO
*/
synchronized void setCompleted() {
setStage(Stage.COMPLETED);
completed = true;
}
/**
* RJCTODO
*
* @return
*/
synchronized boolean isCompleted() {
return completed;
}
/**
* Sets the date the job was completed, with or without cancellation or
* errors.
*
* @param completedDate The completion date.
*/
synchronized void setCompletedDate(Date completedDate) {
this.completedDate = completedDate;
}
/**
* Gets the date the job was completed, with or without cancellation or
* errors.
*
* @return True or false.
*/
synchronized Date getCompletedDate() {
return completedDate; // RJCTODO: Consider returning null if == 0 (epoch)
}
/**
* Sets whether or not erros occurred during the processing of the job.
*
* @param errorsOccurred True or false;
*/
synchronized void setErrorsOccurred(boolean errorsOccurred) {
this.errorsOccurred = errorsOccurred;
}
/**
* Queries whether or not erros occurred during the processing of the job.
*
* @return True or false.
*/
synchronized boolean hasErrors() {
return this.errorsOccurred;
}
/**
* RJCTODO Gets name of the node associated with the job, possibly a remote
* hose if the job is in progress.
*
* @return The node name.
*/
String getNodeName() {
return nodeName;
}
/**
* RJCTODO
*
* @param obj
*
* @return
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AutoIngestJob)) {
return false;
}
if (obj == this) {
return true;
}
return this.getManifest().getFilePath().equals(((AutoIngestJob) obj).getManifest().getFilePath());
}
/**
* RJCTODO
*
* @return
*/
@Override
public int hashCode() {
// RJCTODO: Update this
int hash = 7;
// hash = 71 * hash + Objects.hashCode(this.dateCreated);
return hash;
}
/**
* RJCTODO Default sorting is by ready file creation date, descending
*
* @param o
*
* @return
*/
@Override
public int compareTo(AutoIngestJob o) {
return -this.getManifest().getDateFileCreated().compareTo(o.getManifest().getDateFileCreated());
}
/**
* Custom comparator that allows us to sort List<AutoIngestJob> on reverse
* chronological date modified (descending)
*/
static class ReverseDateCompletedComparator implements Comparator<AutoIngestJob> {
/**
* RJCTODO
*
* @param o1
* @param o2
*
* @return
*/
@Override
public int compare(AutoIngestJob o1, AutoIngestJob o2) {
return -o1.getStageStartDate().compareTo(o2.getStageStartDate());
}
}
/**
* Comparator that sorts auto ingest jobs by priority in descending order.
*/
public static class PriorityComparator implements Comparator<AutoIngestJob> {
/**
* RJCTODO
*
* @param job
* @param anotherJob
*
* @return
*/
@Override
public int compare(AutoIngestJob job, AutoIngestJob anotherJob) {
return -(job.getPriority().compareTo(anotherJob.getPriority()));
}
}
/**
* Custom comparator that allows us to sort List<AutoIngestJob> on case name
* alphabetically except for jobs for the current host, which are placed at
* the top of the list.
*/
static class AlphabeticalComparator implements Comparator<AutoIngestJob> {
/**
* RJCTODO
*
* @param o1
* @param o2
*
* @return
*/
@Override
public int compare(AutoIngestJob o1, AutoIngestJob o2) {
if (o1.getNodeName().equalsIgnoreCase(LOCAL_HOST_NAME)) {
return -1; // o1 is for current case, float to top
} else if (o2.getNodeName().equalsIgnoreCase(LOCAL_HOST_NAME)) {
return 1; // o2 is for current case, float to top
} else {
return o1.getManifest().getCaseName().compareToIgnoreCase(o2.getManifest().getCaseName());
}
}
}
/**
* RJCTODO
*/
// RJCTODO: Combine this enum with StageDetails to make a single class.
enum Stage {
PENDING("Pending"),
STARTING("Starting"),
UPDATING_SHARED_CONFIG("Updating shared configuration"),
CHECKING_SERVICES("Checking services"),
OPENING_CASE("Opening case"),
IDENTIFYING_DATA_SOURCE("Identifying data source type"),
ADDING_DATA_SOURCE("Adding data source"),
ANALYZING_DATA_SOURCE("Analyzing data source"),
ANALYZING_FILES("Analyzing files"),
EXPORTING_FILES("Exporting files"),
CANCELLING_MODULE("Cancelling module"),
CANCELLING("Cancelling"),
COMPLETED("Completed");
private final String displayText;
private Stage(String displayText) {
this.displayText = displayText;
}
String getDisplayText() {
return displayText;
}
}
/**
* RJCTODO
*/
@Immutable
static final class StageDetails {
private final String description;
private final Date startDate;
/**
* RJCTODO
*
* @param description
* @param startDate
*/
private StageDetails(String description, Date startDate) {
this.description = description;
this.startDate = startDate;
}
/**
* RJCTODO
*
* @return
*/
String getDescription() {
return this.description;
}
/**
* RJCTODO
*
* @return
*/
Date getStartDate() {
return this.startDate;
}
}
}