Merge branch 'develop' of github.com:sleuthkit/autopsy into develop
@ -45,6 +45,9 @@
|
||||
|
||||
<dependency conf="core->default" org="commons-validator" name="commons-validator" rev="1.6"/>
|
||||
<dependency conf="core->default" org="net.htmlparser.jericho" name="jericho-html" rev="3.3"/>
|
||||
<dependency org="com.squareup.okhttp" name="okhttp" rev="2.7.5"/>
|
||||
<!-- https://mvnrepository.com/artifact/javax.ws.rs/javax.ws.rs-api -->
|
||||
<dependency org="javax.ws.rs" name="javax.ws.rs-api" rev="2.0"/>
|
||||
<override org="jakarta.ws.rs" module="jakarta.ws.rs-api" rev="2.1.5"/>
|
||||
</dependencies>
|
||||
</ivy-module>
|
||||
|
@ -111,6 +111,10 @@ file.reference.grpc-context-1.19.0.jar=release/modules/ext/grpc-context-1.19.0.j
|
||||
file.reference.opencensus-api-0.19.2.jar=release/modules/ext/opencensus-api-0.19.2.jar
|
||||
file.reference.opencensus-contrib-http-util-0.19.2.jar=release/modules/ext/opencensus-contrib-http-util-0.19.2.jar
|
||||
file.reference.threetenbp-1.3.3.jar=release/modules/ext/threetenbp-1.3.3.jar
|
||||
file.reference.okhttp-2.7.5-javadoc.jar=release/modules/ext/okhttp-2.7.5-javadoc.jar
|
||||
file.reference.okhttp-2.7.5-sources.jar=release/modules/ext/okhttp-2.7.5-sources.jar
|
||||
file.reference.okhttp-2.7.5.jar=release/modules/ext/okhttp-2.7.5.jar
|
||||
file.reference.okio-1.6.0.jar=release/modules/ext/okio-1.6.0.jar
|
||||
javac.source=1.8
|
||||
javac.compilerargs=-Xlint -Xlint:-serial
|
||||
license.file=../LICENSE-2.0.txt
|
||||
|
@ -773,6 +773,14 @@
|
||||
<runtime-relative-path>ext/google-api-services-translate-v2-rev20170525-1.27.0.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/google-api-services-translate-v2-rev20170525-1.27.0.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/okhttp-2.7.5.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/okhttp-2.7.5.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
<class-path-extension>
|
||||
<runtime-relative-path>ext/okio-1.6.0.jar</runtime-relative-path>
|
||||
<binary-origin>release/modules/ext/okio-1.6.0.jar</binary-origin>
|
||||
</class-path-extension>
|
||||
</data>
|
||||
</configuration>
|
||||
</project>
|
||||
|
@ -66,6 +66,7 @@ DataSourceSummaryDialog.window.title=Data Sources Summary
|
||||
DataSourceSummaryNode.column.dataSourceName.header=Data Source Name
|
||||
DataSourceSummaryNode.column.files.header=Files
|
||||
DataSourceSummaryNode.column.results.header=Results
|
||||
DataSourceSummaryNode.column.status.header=Ingest Status
|
||||
DataSourceSummaryNode.column.tags.header=Tags
|
||||
DataSourceSummaryNode.column.type.header=Type
|
||||
DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source
|
||||
|
@ -37,8 +37,10 @@ import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.casemodule.datasourcesummary.DataSourceSummaryNode.DataSourceSummaryEntryNode;
|
||||
import static javax.swing.SwingConstants.RIGHT;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.TableColumn;
|
||||
import org.sleuthkit.datamodel.DataSource;
|
||||
import org.sleuthkit.datamodel.IngestJobInfo;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
@ -50,9 +52,10 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger logger = Logger.getLogger(DataSourceBrowser.class.getName());
|
||||
private static final int COUNT_COLUMN_WIDTH = 25;
|
||||
private static final int USAGE_COLUMN_WIDTH = 120;
|
||||
private static final int DATA_SOURCE_COLUMN_WIDTH = 325;
|
||||
private static final int COUNT_COLUMN_WIDTH = 20;
|
||||
private static final int INGEST_STATUS_WIDTH = 50;
|
||||
private static final int USAGE_COLUMN_WIDTH = 110;
|
||||
private static final int DATA_SOURCE_COLUMN_WIDTH = 280;
|
||||
private final Outline outline;
|
||||
private final org.openide.explorer.view.OutlineView outlineView;
|
||||
private final ExplorerManager explorerManager;
|
||||
@ -69,6 +72,7 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana
|
||||
outlineView = new org.openide.explorer.view.OutlineView();
|
||||
this.setVisible(true);
|
||||
outlineView.setPropertyColumns(
|
||||
Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(),
|
||||
Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(),
|
||||
Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(),
|
||||
Bundle.DataSourceSummaryNode_column_results_header(), Bundle.DataSourceSummaryNode_column_results_header(),
|
||||
@ -90,6 +94,8 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana
|
||||
column.setPreferredWidth(COUNT_COLUMN_WIDTH);
|
||||
} else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_type_header())) {
|
||||
column.setPreferredWidth(USAGE_COLUMN_WIDTH);
|
||||
} else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_status_header())) {
|
||||
column.setPreferredWidth(INGEST_STATUS_WIDTH);
|
||||
} else {
|
||||
column.setPreferredWidth(DATA_SOURCE_COLUMN_WIDTH);
|
||||
}
|
||||
@ -182,6 +188,47 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the DataSourceBrowser to display up to date status information for
|
||||
* the data sources.
|
||||
*
|
||||
* @param dataSourceId the ID of the data source which should be updated
|
||||
* @param newStatus the new status which the data source should have
|
||||
*/
|
||||
void refresh(long dataSourceId, IngestJobInfo.IngestJobStatusType newStatus) {
|
||||
|
||||
//attempt to update the status of any datasources that had status which was STARTED
|
||||
for (DataSourceSummary summary : dataSourceSummaryList) {
|
||||
if (summary.getDataSource().getId() == dataSourceId) {
|
||||
summary.setIngestStatus(newStatus);
|
||||
}
|
||||
}
|
||||
//figure out which nodes were previously selected
|
||||
Node[] selectedNodes = explorerManager.getSelectedNodes();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
explorerManager.setRootContext(new DataSourceSummaryNode(dataSourceSummaryList));
|
||||
List<Node> nodesToSelect = new ArrayList<>();
|
||||
for (Node node : explorerManager.getRootContext().getChildren().getNodes()) {
|
||||
if (node instanceof DataSourceSummaryEntryNode) {
|
||||
//there should only be one selected node as multi-select is disabled
|
||||
for (Node selectedNode : selectedNodes) {
|
||||
if (((DataSourceSummaryEntryNode) node).getDataSource().equals(((DataSourceSummaryEntryNode) selectedNode).getDataSource())) {
|
||||
nodesToSelect.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//reselect the previously selected Nodes
|
||||
try {
|
||||
explorerManager.setSelectedNodes(nodesToSelect.toArray(new Node[nodesToSelect.size()]));
|
||||
} catch (PropertyVetoException ex) {
|
||||
logger.log(Level.WARNING, "Error selecting previously selected nodes", ex);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from within the constructor to initialize the form.
|
||||
* WARNING: Do NOT modify this code. The content of this method is always
|
||||
|
@ -18,15 +18,27 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.casemodule.datasourcesummary;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.datamodel.CaseDbAccessManager;
|
||||
import org.sleuthkit.datamodel.DataSource;
|
||||
import org.sleuthkit.datamodel.IngestJobInfo.IngestJobStatusType;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Wrapper object for a DataSource and the information associated with it.
|
||||
*
|
||||
*/
|
||||
class DataSourceSummary {
|
||||
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DataSourceSummary.class.getName());
|
||||
private static final String INGEST_JOB_STATUS_QUERY = "status_id FROM ingest_jobs WHERE obj_id=";
|
||||
private final DataSource dataSource;
|
||||
private IngestJobStatusType status = null;
|
||||
private final String type;
|
||||
private final long filesCount;
|
||||
private final long resultsCount;
|
||||
@ -45,12 +57,26 @@ class DataSourceSummary {
|
||||
*/
|
||||
DataSourceSummary(DataSource dSource, String typeValue, Long numberOfFiles, Long numberOfResults, Long numberOfTags) {
|
||||
dataSource = dSource;
|
||||
getStatusFromDatabase();
|
||||
type = typeValue == null ? "" : typeValue;
|
||||
filesCount = numberOfFiles == null ? 0 : numberOfFiles;
|
||||
resultsCount = numberOfResults == null ? 0 : numberOfResults;
|
||||
tagsCount = numberOfTags == null ? 0 : numberOfTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of the ingest job from the case database
|
||||
*/
|
||||
private void getStatusFromDatabase() {
|
||||
try {
|
||||
IngestJobQueryCallback callback = new IngestJobQueryCallback();
|
||||
Case.getCurrentCaseThrows().getSleuthkitCase().getCaseDbAccessManager().select(INGEST_JOB_STATUS_QUERY + dataSource.getId(), callback);
|
||||
status = callback.getStatus();
|
||||
} catch (NoCurrentCaseException | TskCoreException ex) {
|
||||
logger.log(Level.WARNING, "Error getting status for data source from case database", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DataSource
|
||||
*
|
||||
@ -60,6 +86,16 @@ class DataSourceSummary {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually set the ingest job status
|
||||
*
|
||||
* @param ingestStatus the status which the ingest job should have
|
||||
* currently, null to display empty string
|
||||
*/
|
||||
void setIngestStatus(IngestJobStatusType ingestStatus) {
|
||||
status = ingestStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of this DataSource
|
||||
*
|
||||
@ -87,6 +123,16 @@ class DataSourceSummary {
|
||||
return resultsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IngestJobStatusType associated with this data source.
|
||||
*
|
||||
* @return the IngestJobStatusType associated with this data source. Can be
|
||||
* null if the IngestJobStatusType is not STARTED or COMPLETED.
|
||||
*/
|
||||
IngestJobStatusType getIngestStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of tagged content objects in this DataSource
|
||||
*
|
||||
@ -95,4 +141,40 @@ class DataSourceSummary {
|
||||
long getTagsCount() {
|
||||
return tagsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to parse result set, getting the status to be associated with
|
||||
* this data source
|
||||
*/
|
||||
class IngestJobQueryCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback {
|
||||
|
||||
private IngestJobStatusType jobStatus = null;
|
||||
|
||||
@Override
|
||||
public void process(ResultSet rs) {
|
||||
try {
|
||||
while (rs.next()) {
|
||||
IngestJobStatusType currentStatus = IngestJobStatusType.fromID(rs.getInt("status_id"));
|
||||
if (currentStatus == IngestJobStatusType.COMPLETED) {
|
||||
jobStatus = currentStatus;
|
||||
} else if (currentStatus == IngestJobStatusType.STARTED) {
|
||||
jobStatus = currentStatus;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
logger.log(Level.WARNING, "Error getting status for ingest job", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status which was determined for this callback
|
||||
*
|
||||
* @return the status of the data source which was queried for
|
||||
*/
|
||||
IngestJobStatusType getStatus() {
|
||||
return jobStatus;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,18 @@
|
||||
package org.sleuthkit.autopsy.casemodule.datasourcesummary;
|
||||
|
||||
import java.awt.Frame;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.util.Map;
|
||||
import java.util.Observable;
|
||||
import java.util.Observer;
|
||||
import java.util.logging.Logger;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel;
|
||||
import org.sleuthkit.autopsy.ingest.IngestManager;
|
||||
import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent;
|
||||
import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent.Reason;
|
||||
import org.sleuthkit.datamodel.DataSource;
|
||||
import org.sleuthkit.datamodel.IngestJobInfo;
|
||||
|
||||
/**
|
||||
* Dialog for displaying the Data Sources Summary information
|
||||
@ -38,7 +42,6 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser
|
||||
private final DataSourceSummaryDetailsPanel detailsPanel;
|
||||
private final DataSourceBrowser dataSourcesPanel;
|
||||
private final IngestJobInfoPanel ingestHistoryPanel;
|
||||
private static final Logger logger = Logger.getLogger(DataSourceSummaryDialog.class.getName());
|
||||
|
||||
/**
|
||||
* Creates new form DataSourceSummaryDialog for displaying a summary of the
|
||||
@ -73,6 +76,17 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser
|
||||
this.repaint();
|
||||
}
|
||||
});
|
||||
//add listener to refresh jobs with Started status when they complete
|
||||
IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {
|
||||
if (evt instanceof DataSourceAnalysisCompletedEvent) {
|
||||
DataSourceAnalysisCompletedEvent dsEvent = (DataSourceAnalysisCompletedEvent) evt;
|
||||
if (dsEvent.getResult() == Reason.ANALYSIS_COMPLETED) {
|
||||
dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), IngestJobInfo.IngestJobStatusType.COMPLETED);
|
||||
} else if (dsEvent.getResult() == Reason.ANALYSIS_CANCELLED) {
|
||||
dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), null);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.pack();
|
||||
}
|
||||
|
||||
|
@ -109,6 +109,7 @@ final class DataSourceSummaryNode extends AbstractNode {
|
||||
static final class DataSourceSummaryEntryNode extends AbstractNode {
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final String status;
|
||||
private final String type;
|
||||
private final long filesCount;
|
||||
private final long resultsCount;
|
||||
@ -124,6 +125,7 @@ final class DataSourceSummaryNode extends AbstractNode {
|
||||
DataSourceSummaryEntryNode(DataSourceSummary dataSourceSummary) {
|
||||
super(Children.LEAF);
|
||||
dataSource = dataSourceSummary.getDataSource();
|
||||
status = dataSourceSummary.getIngestStatus() == null ? "" : dataSourceSummary.getIngestStatus().getDisplayName();
|
||||
type = dataSourceSummary.getType();
|
||||
filesCount = dataSourceSummary.getFilesCount();
|
||||
resultsCount = dataSourceSummary.getResultsCount();
|
||||
@ -143,6 +145,7 @@ final class DataSourceSummaryNode extends AbstractNode {
|
||||
}
|
||||
|
||||
@Messages({"DataSourceSummaryNode.column.dataSourceName.header=Data Source Name",
|
||||
"DataSourceSummaryNode.column.status.header=Ingest Status",
|
||||
"DataSourceSummaryNode.column.type.header=Type",
|
||||
"DataSourceSummaryNode.column.files.header=Files",
|
||||
"DataSourceSummaryNode.column.results.header=Results",
|
||||
@ -157,6 +160,7 @@ final class DataSourceSummaryNode extends AbstractNode {
|
||||
}
|
||||
sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(),
|
||||
dataSource));
|
||||
sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), status));
|
||||
sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(),
|
||||
type));
|
||||
sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(),
|
||||
|
@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.casemodule.services;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -30,15 +31,18 @@ import java.util.logging.Level;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.datamodel.BlackboardArtifact;
|
||||
import org.sleuthkit.datamodel.BlackboardArtifactTag;
|
||||
import org.sleuthkit.datamodel.CaseDbAccessManager;
|
||||
import org.sleuthkit.datamodel.Content;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.TagName;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.TskData;
|
||||
import org.sleuthkit.datamodel.TskData.DbType;
|
||||
|
||||
/**
|
||||
* A per case Autopsy service that manages the addition of content and artifact
|
||||
@ -48,6 +52,32 @@ public class TagsManager implements Closeable {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(TagsManager.class.getName());
|
||||
private final SleuthkitCase caseDb;
|
||||
|
||||
static {
|
||||
//Create the contentviewer tags table (beta) if the current case does not
|
||||
//have the table present
|
||||
Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> {
|
||||
if (evt.getNewValue() != null) {
|
||||
Case currentCase = (Case) evt.getNewValue();
|
||||
try {
|
||||
CaseDbAccessManager caseDb = currentCase.getSleuthkitCase().getCaseDbAccessManager();
|
||||
if (caseDb.tableExists(ContentViewerTagManager.TABLE_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCase.getSleuthkitCase().getDatabaseType().equals(DbType.SQLITE)) {
|
||||
caseDb.createTable(ContentViewerTagManager.TABLE_NAME, ContentViewerTagManager.TABLE_SCHEMA_SQLITE);
|
||||
} else if (currentCase.getSleuthkitCase().getDatabaseType().equals(DbType.POSTGRESQL)) {
|
||||
caseDb.createTable(ContentViewerTagManager.TABLE_NAME, ContentViewerTagManager.TABLE_SCHEMA_POSTGRESQL);
|
||||
}
|
||||
} catch (TskCoreException ex) {
|
||||
LOGGER.log(Level.SEVERE,
|
||||
String.format("Unable to create the %s table for image tag storage.",
|
||||
ContentViewerTagManager.TABLE_NAME), ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether or not a given tag display name contains an illegal
|
||||
|
@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.casemodule.services.contentviewertags;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* A per case Autopsy service that manages the addition of content viewer tags
|
||||
* to the case database. This manager is also responsible for serializing and
|
||||
* deserializing instances of your tag data objects for persistence and
|
||||
* retrieval.
|
||||
*/
|
||||
public class ContentViewerTagManager {
|
||||
|
||||
//Used to convert Java beans into the physical representation that will be stored
|
||||
//in the database.
|
||||
private static final ObjectMapper SERIALIZER = new ObjectMapper();
|
||||
|
||||
public static final String TABLE_NAME = "beta_tag_app_data";
|
||||
public static final String TABLE_SCHEMA_SQLITE = "(app_data_id INTEGER PRIMARY KEY, "
|
||||
+ "content_tag_id INTEGER NOT NULL, app_data TEXT NOT NULL, "
|
||||
+ "FOREIGN KEY(content_tag_id) REFERENCES content_tags(tag_id))";
|
||||
public static final String TABLE_SCHEMA_POSTGRESQL = "(app_data_id BIGSERIAL PRIMARY KEY, "
|
||||
+ "content_tag_id INTEGER NOT NULL, app_data TEXT NOT NULL, "
|
||||
+ "FOREIGN KEY(content_tag_id) REFERENCES content_tags(tag_id))";
|
||||
|
||||
private static final String INSERT_TAG_DATA = "(content_tag_id, app_data) VALUES (%d, '%s')";
|
||||
private static final String UPDATE_TAG_DATA = "SET content_tag_id = %d, app_data = '%s' WHERE app_data_id = %d";
|
||||
private static final String SELECT_TAG_DATA = "* FROM " + TABLE_NAME + " WHERE content_tag_id = %d";
|
||||
private static final String DELETE_TAG_DATA = "WHERE app_data_id = %d";
|
||||
|
||||
/**
|
||||
* Creates and saves a new ContentViewerTag in the case database. The
|
||||
* generic tag data instance T will be automatically serialized into a
|
||||
* storable format.
|
||||
*
|
||||
* @param <T> Generic class type that will be serialized into a storable
|
||||
* format for persistence.
|
||||
* @param contentTag ContentTag that this ContentViewerTag is associated
|
||||
* with (1:1).
|
||||
* @param tagDataBean Data instance that contains the tag information to be
|
||||
* persisted.
|
||||
* @return An instance of a ContentViewerTag of type T, which contains all
|
||||
* the stored information.
|
||||
*
|
||||
* @throws SerializationException Thrown if the tag data instance T could
|
||||
* not be serialized into a storable format.
|
||||
* @throws TskCoreException Thrown if this operation did not successfully
|
||||
* persist in the case database.
|
||||
* @throws NoCurrentCaseException Thrown if invocation of this method occurs
|
||||
* when no case is open.
|
||||
*/
|
||||
public static <T> ContentViewerTag<T> saveTag(ContentTag contentTag, T tagDataBean)
|
||||
throws SerializationException, TskCoreException, NoCurrentCaseException {
|
||||
try {
|
||||
long contentTagId = contentTag.getId();
|
||||
String serialAppData = SERIALIZER.writeValueAsString(tagDataBean);
|
||||
String insertTemplateInstance = String.format(INSERT_TAG_DATA,
|
||||
contentTagId, serialAppData);
|
||||
long insertId = Case.getCurrentCaseThrows()
|
||||
.getSleuthkitCase()
|
||||
.getCaseDbAccessManager()
|
||||
.insert(TABLE_NAME, insertTemplateInstance);
|
||||
return new ContentViewerTag<>(insertId, contentTag, tagDataBean);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new SerializationException("Unable to convert object instance into a storable format", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the ContentViewerTag instance with the new tag data T and
|
||||
* persists the changes to the case database.
|
||||
*
|
||||
* @param <T> Generic class type that will be serialized into a storable
|
||||
* format.
|
||||
* @param oldTag ContentViewerTag instance to be updated
|
||||
* @param tagDataBean Data instance that contains the updated information to
|
||||
* be persisted.
|
||||
*
|
||||
* @throws SerializationException Thrown if the tag data instance T could
|
||||
* not be serialized into a storable format.
|
||||
* @throws TskCoreException Thrown if this operation did not successfully
|
||||
* persist in the case database.
|
||||
* @throws NoCurrentCaseException Thrown if invocation of this method occurs
|
||||
* when no case is open.
|
||||
*/
|
||||
public static <T> ContentViewerTag<T> updateTag(ContentViewerTag<T> oldTag, T tagDataBean)
|
||||
throws SerializationException, TskCoreException, NoCurrentCaseException {
|
||||
try {
|
||||
String serialAppData = SERIALIZER.writeValueAsString(tagDataBean);
|
||||
String updateTemplateInstance = String.format(UPDATE_TAG_DATA,
|
||||
oldTag.getContentTag().getId(), serialAppData, oldTag.getId());
|
||||
Case.getCurrentCaseThrows()
|
||||
.getSleuthkitCase()
|
||||
.getCaseDbAccessManager()
|
||||
.update(TABLE_NAME, updateTemplateInstance);
|
||||
return new ContentViewerTag<>(oldTag.getId(), oldTag.getContentTag(), tagDataBean);
|
||||
} catch (JsonProcessingException ex) {
|
||||
throw new SerializationException("Unable to convert object instance into a storable format", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a ContentViewerTag instance that is associated with the
|
||||
* specified ContentTag. The Java class T that represents the technical
|
||||
* details of the tag should be passed so that automatic binding can take
|
||||
* place.
|
||||
*
|
||||
* @param <T> Generic class type that will be instantiated and filled in
|
||||
* with data.
|
||||
* @param contentTag ContentTag that this ContentViewerTag is associated
|
||||
* with (1:1)
|
||||
* @param clazz Generic class that will be instantiated and filled in with
|
||||
* data.
|
||||
* @return ContentViewerTag with an instance of T as a member variable or
|
||||
* null if the content tag does not have an associated ContentViewerTag of
|
||||
* type T.
|
||||
*
|
||||
* @throws TskCoreException Thrown if this operation did not successfully
|
||||
* persist in the case database.
|
||||
* @throws NoCurrentCaseException Thrown if invocation of this method occurs
|
||||
* when no case is open.
|
||||
*/
|
||||
public static <T> ContentViewerTag<T> getTag(ContentTag contentTag, Class<T> clazz) throws TskCoreException, NoCurrentCaseException {
|
||||
String selectTemplateInstance = String.format(SELECT_TAG_DATA, contentTag.getId());
|
||||
final ResultWrapper<ContentViewerTag<T>> result = new ResultWrapper<>();
|
||||
Case.getCurrentCaseThrows()
|
||||
.getSleuthkitCase()
|
||||
.getCaseDbAccessManager()
|
||||
.select(selectTemplateInstance, (ResultSet rs) -> {
|
||||
try {
|
||||
if (rs.next()) {
|
||||
long tagId = rs.getLong(1);
|
||||
String appDetails = rs.getString(3);
|
||||
try {
|
||||
T instance = SERIALIZER.readValue(appDetails, clazz);
|
||||
result.setResult(new ContentViewerTag<>(tagId, contentTag, instance));
|
||||
} catch (IOException ex) {
|
||||
//Databind for type T failed. Not a system error
|
||||
//but rather a logic error on the part of the caller.
|
||||
result.setResult(null);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
result.setException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.hasException()) {
|
||||
throw new TskCoreException("Unable to select tag from case db", result.getException());
|
||||
}
|
||||
|
||||
return result.getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for holding state in the CaseDbAccessQueryCallback.
|
||||
* CaseDbAccessQueryCallback has no support for exception handling.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
private static class ResultWrapper<T> {
|
||||
|
||||
private T result;
|
||||
private SQLException ex = null;
|
||||
|
||||
public void setResult(T result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public void setException(SQLException ex) {
|
||||
this.ex = ex;
|
||||
}
|
||||
|
||||
public boolean hasException() {
|
||||
return this.ex != null;
|
||||
}
|
||||
|
||||
public SQLException getException() {
|
||||
return ex;
|
||||
}
|
||||
|
||||
public T getResult() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the content viewer tag with the specified id.
|
||||
*
|
||||
* @param contentViewerTag ContentViewerTag to delete
|
||||
* @throws TskCoreException Thrown if this operation did not successfully
|
||||
* persist in the case database.
|
||||
* @throws NoCurrentCaseException Thrown if invocation of this method occurs
|
||||
* when no case is open.
|
||||
*/
|
||||
public static <T> void deleteTag(ContentViewerTag<T> contentViewerTag) throws TskCoreException, NoCurrentCaseException {
|
||||
String deleteTemplateInstance = String.format(DELETE_TAG_DATA, contentViewerTag.getId());
|
||||
Case.getCurrentCaseThrows()
|
||||
.getSleuthkitCase()
|
||||
.getCaseDbAccessManager()
|
||||
.delete(TABLE_NAME, deleteTemplateInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a stored tag in the case database. It is a wrapper
|
||||
* for the tag id, the attached Content tag object, and the Java bean
|
||||
* instance that describes the technical details for reconstructing the tag.
|
||||
*
|
||||
* @param <T> Generic class type that will be instantiated and filled in
|
||||
* with data.
|
||||
*/
|
||||
public static class ContentViewerTag<T> {
|
||||
|
||||
private final long id;
|
||||
private final ContentTag contentTag;
|
||||
private final T details;
|
||||
|
||||
private ContentViewerTag(long id, ContentTag contentTag, T details) {
|
||||
this.id = id;
|
||||
this.contentTag = contentTag;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ContentTag getContentTag() {
|
||||
return contentTag;
|
||||
}
|
||||
|
||||
public T getDetails() {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System exception thrown in the event that class instance T could not be
|
||||
* properly serialized.
|
||||
*/
|
||||
public static class SerializationException extends Exception {
|
||||
|
||||
public SerializationException(String message, Exception source) {
|
||||
super(message, source);
|
||||
}
|
||||
}
|
||||
|
||||
//Prevent this class from being instantiated.
|
||||
private ContentViewerTagManager() {
|
||||
}
|
||||
}
|
@ -18,13 +18,10 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.communications.relationships;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.logging.Level;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openide.nodes.Sheet;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
|
||||
import org.sleuthkit.autopsy.core.UserPreferences;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode;
|
||||
@ -40,7 +37,6 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO
|
||||
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO;
|
||||
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT;
|
||||
import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
import org.sleuthkit.datamodel.TimeUtilities;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.autopsy.communications.Utils;
|
||||
@ -72,7 +68,6 @@ final class MessageNode extends BlackboardArtifactNode {
|
||||
@Override
|
||||
protected Sheet createSheet() {
|
||||
Sheet sheet = super.createSheet();
|
||||
List<Tag> tags = getAllTagsFromDatabase();
|
||||
Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES);
|
||||
if (sheetSet == null) {
|
||||
sheetSet = Sheet.createPropertiesSet();
|
||||
@ -81,17 +76,6 @@ final class MessageNode extends BlackboardArtifactNode {
|
||||
|
||||
sheetSet.put(new NodeProperty<>("Type", Bundle.MessageNode_Node_Property_Type(), "", getDisplayName())); //NON-NLS
|
||||
|
||||
addScoreProperty(sheetSet, tags);
|
||||
|
||||
CorrelationAttributeInstance correlationAttribute = null;
|
||||
if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) {
|
||||
correlationAttribute = getCorrelationAttributeInstance();
|
||||
}
|
||||
addCommentProperty(sheetSet, tags, correlationAttribute);
|
||||
|
||||
if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) {
|
||||
addCountProperty(sheetSet, correlationAttribute);
|
||||
}
|
||||
final BlackboardArtifact artifact = getArtifact();
|
||||
|
||||
BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID());
|
||||
|
@ -83,9 +83,10 @@ MediaViewImagePanel.zoomResetButton.text=Reset
|
||||
MediaViewImagePanel.zoomTextField.text=
|
||||
MediaViewImagePanel.rotationTextField.text=
|
||||
MediaViewImagePanel.rotateLeftButton.toolTipText=
|
||||
HtmlPanel.showImagesToggleButton.text=Show Images
|
||||
HtmlPanel.showImagesToggleButton.text=Download Images
|
||||
MediaPlayerPanel.audioSlider.toolTipText=
|
||||
MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume
|
||||
MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00
|
||||
MediaPlayerPanel.playButton.text=\u25ba
|
||||
MediaPlayerPanel.infoLabel.text=No Errors
|
||||
MediaViewImagePanel.tagsMenu.text_1=Tags Menu
|
||||
|
@ -22,7 +22,7 @@ GstVideoPanel.cannotProcFile.err=The media player cannot process this file.
|
||||
GstVideoPanel.noOpenCase.errMsg=No open case available.
|
||||
Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.
|
||||
HtmlPanel_showImagesToggleButton_hide=Hide Images
|
||||
HtmlPanel_showImagesToggleButton_show=Show Images
|
||||
HtmlPanel_showImagesToggleButton_show=Download Images
|
||||
HtmlViewer_file_error=This file is missing or unreadable.
|
||||
MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled.
|
||||
GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player.
|
||||
@ -44,7 +44,11 @@ MediaPlayerPanel.timeFormat=%02d:%02d:%02d
|
||||
MediaPlayerPanel.unknownTime=Unknown
|
||||
MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory.
|
||||
MediaViewImagePanel.errorLabel.text=Could not load file into Media View.
|
||||
MediaViewImagePanel.exportSaveText=Save
|
||||
MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E
|
||||
MediaViewImagePanel.fileChooserTitle=Choose a save location
|
||||
MediaViewImagePanel.successfulExport=Tagged image was successfully saved.
|
||||
MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.
|
||||
MediaViewVideoPanel.pauseButton.text=\u25ba
|
||||
MediaViewVideoPanel.progressLabel.text=00:00
|
||||
MediaViewVideoPanel.infoLabel.text=info
|
||||
@ -145,12 +149,13 @@ MediaViewImagePanel.zoomResetButton.text=Reset
|
||||
MediaViewImagePanel.zoomTextField.text=
|
||||
MediaViewImagePanel.rotationTextField.text=
|
||||
MediaViewImagePanel.rotateLeftButton.toolTipText=
|
||||
HtmlPanel.showImagesToggleButton.text=Show Images
|
||||
HtmlPanel.showImagesToggleButton.text=Download Images
|
||||
MediaPlayerPanel.audioSlider.toolTipText=
|
||||
MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume
|
||||
MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00
|
||||
MediaPlayerPanel.playButton.text=\u25ba
|
||||
MediaPlayerPanel.infoLabel.text=No Errors
|
||||
MediaViewImagePanel.tagsMenu.text_1=Tags Menu
|
||||
# {0} - tableName
|
||||
SQLiteViewer.readTable.errorText=Error getting rows for table: {0}
|
||||
# {0} - tableName
|
||||
|
@ -18,7 +18,7 @@
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="showImagesToggleButton" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace min="0" pref="95" max="32767" attributes="0"/>
|
||||
<EmptySpace min="0" pref="75" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="htmlJPanel" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
|
@ -18,6 +18,11 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.contentviewers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@ -25,8 +30,9 @@ import javafx.concurrent.Worker;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Node;
|
||||
import net.htmlparser.jericho.Attribute;
|
||||
import net.htmlparser.jericho.OutputDocument;
|
||||
import net.htmlparser.jericho.Source;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NodeList;
|
||||
@ -38,6 +44,7 @@ import org.w3c.dom.events.EventTarget;
|
||||
@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
|
||||
final class HtmlPanel extends javax.swing.JPanel {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(HtmlPanel.class.getName());
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String TEXT_TYPE = "text/plain";
|
||||
private final JFXPanel jfxPanel = new JFXPanel();
|
||||
@ -92,26 +99,76 @@ final class HtmlPanel extends javax.swing.JPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans out input HTML string
|
||||
* Cleans out input HTML string so it will not access resources over the internet
|
||||
*
|
||||
* @param htmlInString The HTML string to cleanse
|
||||
*
|
||||
* @return The cleansed HTML String
|
||||
*/
|
||||
private String cleanseHTML(String htmlInString) {
|
||||
org.jsoup.nodes.Document doc = Jsoup.parse(htmlInString);
|
||||
// remove all 'img' tags.
|
||||
doc.select("img").stream().forEach(Node::remove);
|
||||
// remove all 'span' tags, these are often images which are ads
|
||||
doc.select("span").stream().forEach(Node::remove);
|
||||
return doc.html();
|
||||
String returnString = "";
|
||||
try {
|
||||
Source source = new Source(new StringReader(htmlInString));
|
||||
OutputDocument document = new OutputDocument(source);
|
||||
//remove background images
|
||||
source.getAllTags().stream().filter((tag) -> (tag.toString().contains("background-image"))).forEachOrdered((tag) -> {
|
||||
document.remove(tag);
|
||||
});
|
||||
//remove images
|
||||
source.getAllElements("img").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove frames
|
||||
source.getAllElements("frame").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove iframes
|
||||
source.getAllElements("iframe").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove pictures
|
||||
source.getAllElements("picture").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove svg
|
||||
source.getAllElements("svg").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove audio
|
||||
source.getAllElements("audio").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove video
|
||||
source.getAllElements("video").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove tracks
|
||||
source.getAllElements("track").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove embeded external elements
|
||||
source.getAllElements("embed").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove linked elements
|
||||
source.getAllElements("link").forEach((element) -> {
|
||||
document.remove(element.getAllTags());
|
||||
});
|
||||
//remove other URI elements such as input boxes
|
||||
List<Attribute> attributesToRemove = source.getURIAttributes();
|
||||
document.remove(attributesToRemove);
|
||||
returnString = document.toString();
|
||||
} catch (IOException ex) {
|
||||
logger.log(Level.WARNING, "Unable to read html for cleaning out URI elements with Jericho", ex);
|
||||
}
|
||||
return returnString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the panel to reflect the current show/hide images setting.
|
||||
*/
|
||||
@Messages({
|
||||
"HtmlPanel_showImagesToggleButton_show=Show Images",
|
||||
"HtmlPanel_showImagesToggleButton_show=Download Images",
|
||||
"HtmlPanel_showImagesToggleButton_hide=Hide Images",
|
||||
"Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.",})
|
||||
private void refresh() {
|
||||
@ -164,7 +221,7 @@ final class HtmlPanel extends javax.swing.JPanel {
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(showImagesToggleButton)
|
||||
.addGap(0, 95, Short.MAX_VALUE))
|
||||
.addGap(0, 75, Short.MAX_VALUE))
|
||||
.addComponent(htmlJPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
|
@ -19,7 +19,7 @@
|
||||
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
|
||||
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/>
|
||||
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,100,0,0,3,117"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout">
|
||||
@ -200,6 +200,44 @@
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="zoomResetButtonActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Component class="javax.swing.Box$Filler" name="filler1">
|
||||
</Component>
|
||||
<Component class="javax.swing.Box$Filler" name="filler2">
|
||||
<Properties>
|
||||
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[32767, 0]"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="classDetails" type="java.lang.String" value="Box.Filler.HorizontalGlue"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Container class="javax.swing.JPanel" name="jPanel1">
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/>
|
||||
</Container>
|
||||
<Component class="javax.swing.JButton" name="tagsMenu">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/contentviewers/Bundle.properties" key="MediaViewImagePanel.tagsMenu.text_1" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="focusable" type="boolean" value="false"/>
|
||||
<Property name="horizontalTextPosition" type="int" value="0"/>
|
||||
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[75, 21]"/>
|
||||
</Property>
|
||||
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[75, 21]"/>
|
||||
</Property>
|
||||
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
|
||||
<Dimension value="[75, 21]"/>
|
||||
</Property>
|
||||
<Property name="verticalTextPosition" type="int" value="3"/>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="mousePressed" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="tagsMenuMousePressed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Container>
|
||||
</SubComponents>
|
||||
|
@ -20,18 +20,31 @@ package org.sleuthkit.autopsy.contentviewers;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyChangeSupport;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import static java.util.Objects.nonNull;
|
||||
import java.util.SortedSet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener.Change;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
@ -45,19 +58,39 @@ import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Translate;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFileChooser;
|
||||
import javafx.scene.Node;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.SwingWorker;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.controlsfx.control.MaskerPane;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.python.google.common.collect.Lists;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
|
||||
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog.TagNameAndComment;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager;
|
||||
import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag;
|
||||
import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.SerializationException;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagsUtility;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagControls;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagCreator;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTag;
|
||||
import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagsGroup;
|
||||
import org.sleuthkit.autopsy.coreutils.ImageUtils;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.datamodel.FileNode;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.datamodel.AbstractFile;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Image viewer part of the Media View layered pane. Uses JavaFX to display the
|
||||
@ -70,17 +103,30 @@ import org.sleuthkit.datamodel.AbstractFile;
|
||||
class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPanel {
|
||||
|
||||
private static final Image EXTERNAL = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm());
|
||||
private final static Logger LOGGER = Logger.getLogger(MediaViewImagePanel.class.getName());
|
||||
|
||||
private final boolean fxInited;
|
||||
|
||||
private JFXPanel fxPanel;
|
||||
private Group imageGroup;
|
||||
private ImageTaggingTool tagger;
|
||||
private AbstractFile file;
|
||||
private Group masterGroup;
|
||||
private ImageTagsGroup tagsGroup;
|
||||
private ImageTagCreator imageTagCreator;
|
||||
private ImageView fxImageView;
|
||||
private ScrollPane scrollPane;
|
||||
private final ProgressBar progressBar = new ProgressBar();
|
||||
private final MaskerPane maskerPane = new MaskerPane();
|
||||
|
||||
private final JPopupMenu popupMenu = new JPopupMenu();
|
||||
private final JMenuItem createTagMenuItem;
|
||||
private final JMenuItem deleteTagMenuItem;
|
||||
private final JMenuItem hideTagsMenuItem;
|
||||
private final JMenuItem exportTagsMenuItem;
|
||||
|
||||
private final JFileChooser exportChooser;
|
||||
|
||||
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
|
||||
|
||||
private double zoomRatio;
|
||||
private double rotation; // Can be 0, 90, 180, and 270.
|
||||
|
||||
@ -116,33 +162,163 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
public MediaViewImagePanel() {
|
||||
initComponents();
|
||||
fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited();
|
||||
|
||||
exportChooser = new JFileChooser();
|
||||
exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle());
|
||||
|
||||
//Build popupMenu when Tags Menu button is pressed.
|
||||
createTagMenuItem = new JMenuItem("Create");
|
||||
createTagMenuItem.addActionListener((event) -> createTag());
|
||||
popupMenu.add(createTagMenuItem);
|
||||
|
||||
popupMenu.add(new JSeparator());
|
||||
|
||||
deleteTagMenuItem = new JMenuItem("Delete");
|
||||
deleteTagMenuItem.addActionListener((event) -> deleteTag());
|
||||
popupMenu.add(deleteTagMenuItem);
|
||||
|
||||
popupMenu.add(new JSeparator());
|
||||
|
||||
hideTagsMenuItem = new JMenuItem("Hide");
|
||||
hideTagsMenuItem.addActionListener((event) -> showOrHideTags());
|
||||
popupMenu.add(hideTagsMenuItem);
|
||||
|
||||
popupMenu.add(new JSeparator());
|
||||
|
||||
exportTagsMenuItem = new JMenuItem("Export");
|
||||
exportTagsMenuItem.addActionListener((event) -> exportTags());
|
||||
popupMenu.add(exportTagsMenuItem);
|
||||
|
||||
popupMenu.setPopupSize(300, 150);
|
||||
|
||||
if (fxInited) {
|
||||
Platform.runLater(() -> {
|
||||
Platform.runLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// build jfx ui (we could do this in FXML?)
|
||||
fxImageView = new ImageView(); // will hold image
|
||||
masterGroup = new Group(fxImageView);
|
||||
tagsGroup = new ImageTagsGroup(fxImageView);
|
||||
tagsGroup.getChildren().addListener((Change<? extends Node> c) -> {
|
||||
if (c.getList().isEmpty()) {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.EMPTY));
|
||||
}
|
||||
});
|
||||
|
||||
// build jfx ui (we could do this in FXML?)
|
||||
fxImageView = new ImageView(); // will hold image
|
||||
imageGroup = new Group();
|
||||
imageGroup.getChildren().add(fxImageView);
|
||||
scrollPane = new ScrollPane(imageGroup); // scrolls and sizes imageview
|
||||
scrollPane.getStyleClass().add("bg"); //NOI18N
|
||||
scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
|
||||
subscribeTagMenuItemsToStateChanges();
|
||||
|
||||
fxPanel = new JFXPanel(); // bridge jfx-swing
|
||||
Scene scene = new Scene(scrollPane); //root of jfx tree
|
||||
scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N
|
||||
fxPanel.setScene(scene);
|
||||
masterGroup.getChildren().add(tagsGroup);
|
||||
|
||||
fxImageView.setSmooth(true);
|
||||
fxImageView.setCache(true);
|
||||
//Update buttons when users select (or unselect) image tags.
|
||||
tagsGroup.addFocusChangeListener((event) -> {
|
||||
if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) {
|
||||
if (masterGroup.getChildren().contains(imageTagCreator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
add(fxPanel);//add jfx ui to JPanel
|
||||
});
|
||||
if (tagsGroup.getChildren().isEmpty()) {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.EMPTY));
|
||||
} else {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.CREATE));
|
||||
}
|
||||
} else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.SELECTED));
|
||||
}
|
||||
});
|
||||
|
||||
scrollPane = new ScrollPane(masterGroup); // scrolls and sizes imageview
|
||||
scrollPane.getStyleClass().add("bg"); //NOI18N
|
||||
scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
|
||||
scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
|
||||
|
||||
fxPanel = new JFXPanel(); // bridge jfx-swing
|
||||
Scene scene = new Scene(scrollPane); //root of jfx tree
|
||||
scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N
|
||||
fxPanel.setScene(scene);
|
||||
|
||||
fxImageView.setSmooth(true);
|
||||
fxImageView.setCache(true);
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
add(fxPanel);//add jfx ui to JPanel
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tags menu item enabling and disabling given the state of the
|
||||
* content viewer. For example, when the tags group is empty (no tags on image),
|
||||
* disable delete menu item, hide menu item, and export menu item.
|
||||
*/
|
||||
private void subscribeTagMenuItemsToStateChanges() {
|
||||
pcs.addPropertyChangeListener((event) -> {
|
||||
State currentState = (State) event.getNewValue();
|
||||
switch (currentState) {
|
||||
case CREATE:
|
||||
createTagMenuItem.setEnabled(true);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(true);
|
||||
exportTagsMenuItem.setEnabled(true);
|
||||
break;
|
||||
case SELECTED:
|
||||
if (masterGroup.getChildren().contains(imageTagCreator)) {
|
||||
imageTagCreator.disconnect();
|
||||
masterGroup.getChildren().remove(imageTagCreator);
|
||||
}
|
||||
createTagMenuItem.setEnabled(false);
|
||||
deleteTagMenuItem.setEnabled(true);
|
||||
hideTagsMenuItem.setEnabled(true);
|
||||
exportTagsMenuItem.setEnabled(true);
|
||||
break;
|
||||
case HIDDEN:
|
||||
createTagMenuItem.setEnabled(false);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(true);
|
||||
hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
|
||||
exportTagsMenuItem.setEnabled(false);
|
||||
break;
|
||||
case VISIBLE:
|
||||
createTagMenuItem.setEnabled(true);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(true);
|
||||
hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
|
||||
exportTagsMenuItem.setEnabled(true);
|
||||
break;
|
||||
case DEFAULT:
|
||||
case EMPTY:
|
||||
if (masterGroup.getChildren().contains(imageTagCreator)) {
|
||||
imageTagCreator.disconnect();
|
||||
}
|
||||
createTagMenuItem.setEnabled(true);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
|
||||
exportTagsMenuItem.setEnabled(false);
|
||||
break;
|
||||
case NONEMPTY:
|
||||
createTagMenuItem.setEnabled(true);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(true);
|
||||
exportTagsMenuItem.setEnabled(true);
|
||||
break;
|
||||
case DISABLE:
|
||||
createTagMenuItem.setEnabled(false);
|
||||
deleteTagMenuItem.setEnabled(false);
|
||||
hideTagsMenuItem.setEnabled(false);
|
||||
exportTagsMenuItem.setEnabled(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isInited() {
|
||||
return fxInited;
|
||||
}
|
||||
@ -154,10 +330,11 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
Platform.runLater(() -> {
|
||||
fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0));
|
||||
fxImageView.setImage(null);
|
||||
tagger.defaultSettings();
|
||||
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.DEFAULT));
|
||||
masterGroup.getChildren().clear();
|
||||
scrollPane.setContent(null);
|
||||
scrollPane.setContent(imageGroup);
|
||||
scrollPane.setContent(masterGroup);
|
||||
});
|
||||
}
|
||||
|
||||
@ -205,14 +382,31 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
|
||||
try {
|
||||
Image fxImage = readImageTask.get();
|
||||
masterGroup.getChildren().clear();
|
||||
tagsGroup.getChildren().clear();
|
||||
this.file = file;
|
||||
if (nonNull(fxImage)) {
|
||||
// We have a non-null image, so let's show it.
|
||||
fxImageView.setImage(fxImage);
|
||||
imageGroup.getChildren().remove(tagger);
|
||||
tagger = new ImageTaggingTool(fxImageView, Color.RED);
|
||||
imageGroup.getChildren().add(tagger);
|
||||
resetView();
|
||||
scrollPane.setContent(imageGroup);
|
||||
masterGroup.getChildren().add(fxImageView);
|
||||
masterGroup.getChildren().add(tagsGroup);
|
||||
|
||||
try {
|
||||
List<ContentTag> tags = Case.getCurrentCase().getServices()
|
||||
.getTagsManager().getContentTagsByContent(file);
|
||||
|
||||
List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
|
||||
//Add all image tags
|
||||
tagsGroup = buildImageTagsGroup(contentViewerTags);
|
||||
if (!tagsGroup.getChildren().isEmpty()) {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.NONEMPTY));
|
||||
}
|
||||
} catch (TskCoreException | NoCurrentCaseException ex) {
|
||||
LOGGER.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS
|
||||
}
|
||||
scrollPane.setContent(masterGroup);
|
||||
} else {
|
||||
showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file);
|
||||
}
|
||||
@ -252,6 +446,52 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all ContentViewerTags that are of type 'ImageTagRegion' for the
|
||||
* current file.
|
||||
*
|
||||
* @param contentTags
|
||||
* @return
|
||||
* @throws TskCoreException
|
||||
* @throws NoCurrentCaseException
|
||||
*/
|
||||
private List<ContentViewerTag<ImageTagRegion>> getContentViewerTags(List<ContentTag> contentTags)
|
||||
throws TskCoreException, NoCurrentCaseException {
|
||||
List<ContentViewerTag<ImageTagRegion>> contentViewerTags = new ArrayList<>();
|
||||
for (ContentTag contentTag : contentTags) {
|
||||
ContentViewerTag<ImageTagRegion> contentViewerTag = ContentViewerTagManager
|
||||
.getTag(contentTag, ImageTagRegion.class);
|
||||
if (contentViewerTag == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
contentViewerTags.add(contentViewerTag);
|
||||
}
|
||||
return contentViewerTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds ImageTag instances from stored ContentViewerTags of the
|
||||
* appropriate type.
|
||||
*
|
||||
* @param contentTags
|
||||
* @return
|
||||
* @throws TskCoreException
|
||||
* @throws NoCurrentCaseException
|
||||
*/
|
||||
private ImageTagsGroup buildImageTagsGroup(List<ContentViewerTag<ImageTagRegion>> contentViewerTags) {
|
||||
|
||||
contentViewerTags.forEach(contentViewerTag -> {
|
||||
/**
|
||||
* Build the image tag, add an edit event call back to persist all
|
||||
* edits made on this image tag instance.
|
||||
*/
|
||||
tagsGroup.getChildren().add(buildImageTag(contentViewerTag));
|
||||
});
|
||||
|
||||
return tagsGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return supported mime types
|
||||
*/
|
||||
@ -303,6 +543,10 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
zoomInButton = new javax.swing.JButton();
|
||||
jSeparator2 = new javax.swing.JToolBar.Separator();
|
||||
zoomResetButton = new javax.swing.JButton();
|
||||
filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0));
|
||||
filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
|
||||
jPanel1 = new javax.swing.JPanel();
|
||||
tagsMenu = new javax.swing.JButton();
|
||||
|
||||
setBackground(new java.awt.Color(0, 0, 0));
|
||||
addComponentListener(new java.awt.event.ComponentAdapter() {
|
||||
@ -406,6 +650,23 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
}
|
||||
});
|
||||
toolbar.add(zoomResetButton);
|
||||
toolbar.add(filler1);
|
||||
toolbar.add(filler2);
|
||||
toolbar.add(jPanel1);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.tagsMenu.text_1")); // NOI18N
|
||||
tagsMenu.setFocusable(false);
|
||||
tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
|
||||
tagsMenu.setMaximumSize(new java.awt.Dimension(75, 21));
|
||||
tagsMenu.setMinimumSize(new java.awt.Dimension(75, 21));
|
||||
tagsMenu.setPreferredSize(new java.awt.Dimension(75, 21));
|
||||
tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
|
||||
tagsMenu.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
public void mousePressed(java.awt.event.MouseEvent evt) {
|
||||
tagsMenuMousePressed(evt);
|
||||
}
|
||||
});
|
||||
toolbar.add(tagsMenu);
|
||||
|
||||
add(toolbar);
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
@ -450,12 +711,228 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
updateView();
|
||||
}//GEN-LAST:event_formComponentResized
|
||||
|
||||
/**
|
||||
* Deletes the selected tag when the Delete button is pressed in the Tag
|
||||
* Menu.
|
||||
*/
|
||||
private void deleteTag() {
|
||||
Platform.runLater(() -> {
|
||||
ImageTag tagInFocus = tagsGroup.getFocus();
|
||||
if (tagInFocus == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ContentViewerTag<ImageTagRegion> contentViewerTag = tagInFocus.getContentViewerTag();
|
||||
scrollPane.setCursor(Cursor.WAIT);
|
||||
ContentViewerTagManager.deleteTag(contentViewerTag);
|
||||
Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag());
|
||||
tagsGroup.getChildren().remove(tagInFocus);
|
||||
} catch (TskCoreException | NoCurrentCaseException ex) {
|
||||
LOGGER.log(Level.WARNING, "Could not delete image tag in case db", ex); //NON-NLS
|
||||
}
|
||||
|
||||
scrollPane.setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.CREATE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables create tag logic when the Create button is pressed in the Tags
|
||||
* Menu.
|
||||
*/
|
||||
private void createTag() {
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.DISABLE));
|
||||
imageTagCreator = new ImageTagCreator(fxImageView);
|
||||
|
||||
PropertyChangeListener newTagListener = (event) -> {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ImageTagRegion tag = (ImageTagRegion) event.getNewValue();
|
||||
//Ask the user for tag name and comment
|
||||
TagNameAndComment result = GetTagNameAndCommentDialog.doDialog();
|
||||
if (result != null) {
|
||||
//Persist and build image tag
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
scrollPane.setCursor(Cursor.WAIT);
|
||||
ContentViewerTag<ImageTagRegion> contentViewerTag = storeImageTag(tag, result);
|
||||
ImageTag imageTag = buildImageTag(contentViewerTag);
|
||||
tagsGroup.getChildren().add(imageTag);
|
||||
} catch (TskCoreException | SerializationException | NoCurrentCaseException ex) {
|
||||
LOGGER.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS
|
||||
}
|
||||
|
||||
scrollPane.setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.CREATE));
|
||||
});
|
||||
|
||||
//Remove image tag creator from panel
|
||||
Platform.runLater(() -> {
|
||||
imageTagCreator.disconnect();
|
||||
masterGroup.getChildren().remove(imageTagCreator);
|
||||
});
|
||||
};
|
||||
|
||||
imageTagCreator.addNewTagListener(newTagListener);
|
||||
Platform.runLater(() -> masterGroup.getChildren().add(imageTagCreator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ImageTag instance from the ContentViewerTag.
|
||||
*
|
||||
* @param contentViewerTag
|
||||
* @return
|
||||
*/
|
||||
private ImageTag buildImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag) {
|
||||
ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView);
|
||||
|
||||
//Automatically persist edits made by user
|
||||
imageTag.subscribeToEditEvents((edit) -> {
|
||||
try {
|
||||
scrollPane.setCursor(Cursor.WAIT);
|
||||
ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue();
|
||||
ContentViewerTagManager.updateTag(contentViewerTag, newRegion);
|
||||
} catch (SerializationException | TskCoreException | NoCurrentCaseException ex) {
|
||||
LOGGER.log(Level.WARNING, "Could not save edit for image tag in case db", ex); //NON-NLS
|
||||
}
|
||||
scrollPane.setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
return imageTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the image tag by creating a ContentTag instance and associating
|
||||
* the ImageTagRegion data with it in the case database.
|
||||
*
|
||||
* @param data
|
||||
* @param result
|
||||
*/
|
||||
private ContentViewerTag<ImageTagRegion> storeImageTag(ImageTagRegion data, TagNameAndComment result)
|
||||
throws TskCoreException, SerializationException, NoCurrentCaseException {
|
||||
scrollPane.setCursor(Cursor.WAIT);
|
||||
try {
|
||||
ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager()
|
||||
.addContentTag(file, result.getTagName(), result.getComment());
|
||||
return ContentViewerTagManager.saveTag(contentTag, data);
|
||||
} finally {
|
||||
scrollPane.setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides or show tags when the Hide or Show button is pressed in the Tags
|
||||
* Menu.
|
||||
*/
|
||||
private void showOrHideTags() {
|
||||
Platform.runLater(() -> {
|
||||
if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) {
|
||||
//Temporarily remove the tags group and update buttons
|
||||
masterGroup.getChildren().remove(tagsGroup);
|
||||
hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName());
|
||||
tagsGroup.clearFocus();
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.HIDDEN));
|
||||
} else {
|
||||
//Add tags group back in and update buttons
|
||||
masterGroup.getChildren().add(tagsGroup);
|
||||
hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName());
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
"state", null, State.VISIBLE));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NbBundle.Messages({
|
||||
"MediaViewImagePanel.exportSaveText=Save",
|
||||
"MediaViewImagePanel.successfulExport=Tagged image was successfully saved.",
|
||||
"MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.",
|
||||
"MediaViewImagePanel.fileChooserTitle=Choose a save location"
|
||||
})
|
||||
private void exportTags() {
|
||||
tagsGroup.clearFocus();
|
||||
exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||
//Always base chooser location to export folder
|
||||
exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
|
||||
int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText());
|
||||
if (returnVal == JFileChooser.APPROVE_OPTION) {
|
||||
new SwingWorker<Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
try {
|
||||
List<ContentTag> tags = Case.getCurrentCase().getServices()
|
||||
.getTagsManager().getContentTagsByContent(file);
|
||||
List<ContentViewerTag<ImageTagRegion>> contentViewerTags = getContentViewerTags(tags);
|
||||
Collection<ImageTagRegion> regions = contentViewerTags.stream()
|
||||
.map(cvTag -> cvTag.getDetails()).collect(Collectors.toList());
|
||||
byte[] jpgImage = ImageTagsUtility.exportTags(file, regions, ".jpg");
|
||||
Path output = Paths.get(exportChooser.getSelectedFile().getPath(),
|
||||
FilenameUtils.getBaseName(file.getName()) + "-with_tags.jpg"); //NON-NLS
|
||||
Files.write(output, jpgImage);
|
||||
JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport());
|
||||
} catch (TskCoreException | NoCurrentCaseException | IOException ex) {
|
||||
LOGGER.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS
|
||||
JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed
|
||||
popupMenu.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3);
|
||||
}//GEN-LAST:event_tagsMenuMousePressed
|
||||
|
||||
/**
|
||||
* Display states for the show/hide tags button.
|
||||
*/
|
||||
enum DisplayOptions {
|
||||
HIDE_TAGS("Hide"),
|
||||
SHOW_TAGS("Show");
|
||||
|
||||
private final String name;
|
||||
|
||||
DisplayOptions(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Different states that the content viewer can be in. These states drive
|
||||
* which buttons are enabled for tagging.
|
||||
*/
|
||||
enum State {
|
||||
HIDDEN,
|
||||
VISIBLE,
|
||||
SELECTED,
|
||||
CREATE,
|
||||
EMPTY,
|
||||
NONEMPTY,
|
||||
DEFAULT,
|
||||
DISABLE;
|
||||
}
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.Box.Filler filler1;
|
||||
private javax.swing.Box.Filler filler2;
|
||||
private javax.swing.JPanel jPanel1;
|
||||
private javax.swing.JToolBar.Separator jSeparator1;
|
||||
private javax.swing.JToolBar.Separator jSeparator2;
|
||||
private javax.swing.JButton rotateLeftButton;
|
||||
private javax.swing.JButton rotateRightButton;
|
||||
private javax.swing.JTextField rotationTextField;
|
||||
private javax.swing.JButton tagsMenu;
|
||||
private javax.swing.JToolBar toolbar;
|
||||
private javax.swing.JButton zoomInButton;
|
||||
private javax.swing.JButton zoomOutButton;
|
||||
@ -601,8 +1078,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
// Add the transforms in reverse order of intended execution.
|
||||
// Note: They MUST be added in this order to ensure translate is
|
||||
// executed last.
|
||||
imageGroup.getTransforms().clear();
|
||||
imageGroup.getTransforms().addAll(translate, rotate, scale);
|
||||
masterGroup.getTransforms().clear();
|
||||
masterGroup.getTransforms().addAll(translate, rotate, scale);
|
||||
|
||||
// Adjust scroll bar positions for view changes.
|
||||
if (viewportWidth > fxPanel.getWidth()) {
|
||||
@ -618,107 +1095,4 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan
|
||||
rotationTextField.setText((int) rotation + "°");
|
||||
zoomTextField.setText((Math.round(zoomRatio * 100.0)) + "%");
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables users to 'tag' a region of an image by clicking and dragging a
|
||||
* rectangle overtop.
|
||||
*/
|
||||
class ImageTaggingTool extends Rectangle {
|
||||
|
||||
private final double imageWidth;
|
||||
private final double imageHeight;
|
||||
private final double imageOriginX;
|
||||
private final double imageOriginY;
|
||||
|
||||
//Origin of the drag event.
|
||||
private double rectangleOriginX;
|
||||
private double rectangleOriginY;
|
||||
|
||||
//Rectangle lines should be 1.5% of the image. This level of thickness has
|
||||
//a good balance between visual acuity and loss of selection at the borders
|
||||
//of the image.
|
||||
private double lineThicknessAsPercent = 1.5;
|
||||
|
||||
/**
|
||||
* Adds tagging support to an image, where the 'tag' rectangle will be
|
||||
* the specified color.
|
||||
*
|
||||
* @param image Image to tag
|
||||
* @param color Color of the 'tag' rectangle
|
||||
*/
|
||||
private ImageTaggingTool(ImageView image, Color color) {
|
||||
defaultSettings();
|
||||
|
||||
imageWidth = image.getImage().getWidth();
|
||||
imageHeight = image.getImage().getHeight();
|
||||
imageOriginX = image.getX();
|
||||
imageOriginY = image.getY();
|
||||
|
||||
setStroke(color);
|
||||
setFill(color.deriveColor(0, 0, 0, 0));
|
||||
|
||||
//Calculate how many pixels the stroke width should be to guarentee
|
||||
//a consistent % of image consumed by the rectangle border.
|
||||
double min = Math.min(imageWidth, imageHeight);
|
||||
double lineThicknessPixels = min * lineThicknessAsPercent / 100.0;
|
||||
setStrokeWidth(lineThicknessPixels);
|
||||
setVisible(false);
|
||||
|
||||
//Create a rectangle by left clicking on the image
|
||||
image.setOnMousePressed((MouseEvent event) -> {
|
||||
if (event.isSecondaryButtonDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Reset box on new click.
|
||||
defaultSettings();
|
||||
|
||||
rectangleOriginX = event.getX();
|
||||
rectangleOriginY = event.getY();
|
||||
|
||||
setX(rectangleOriginX);
|
||||
setY(rectangleOriginY);
|
||||
});
|
||||
|
||||
//Adjust the rectangle by dragging the left mouse button
|
||||
image.setOnMouseDragged((MouseEvent event) -> {
|
||||
if (event.isSecondaryButtonDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the rectangle is contained within image boundaries and
|
||||
* that the line thickness is kept within bounds.
|
||||
*/
|
||||
double newX = Math.min(Math.max(event.getX(), imageOriginX)
|
||||
+ lineThicknessPixels / 2, imageWidth - lineThicknessPixels / 2);
|
||||
double newY = Math.min(Math.max(event.getY(), imageOriginY)
|
||||
+ lineThicknessPixels / 2, imageHeight - lineThicknessPixels / 2);
|
||||
|
||||
setVisible(true);
|
||||
double offsetX = newX - rectangleOriginX;
|
||||
if (offsetX < 0) {
|
||||
setX(newX);
|
||||
}
|
||||
setWidth(Math.abs(offsetX));
|
||||
|
||||
double offsetY = newY - rectangleOriginY;
|
||||
if (offsetY < 0) {
|
||||
setY(newY);
|
||||
}
|
||||
setHeight(Math.abs(offsetY));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the rectangle to default dimensions.
|
||||
*/
|
||||
public final void defaultSettings() {
|
||||
setX(0);
|
||||
setY(0);
|
||||
setWidth(0);
|
||||
setHeight(0);
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
343
Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTag.java
Executable file
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
import com.sun.javafx.event.EventDispatchChainImpl;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyChangeSupport;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag;
|
||||
|
||||
/**
|
||||
* A tagged region displayed over an image. This class contains a "physical tag"
|
||||
* and 8 edit "handles". The physical tag is a plain old rectangle that defines
|
||||
* the tag boundaries. The edit handles serve two purposes. One is to represent
|
||||
* selection. All 8 edit handles will become visible overtop the physical tag
|
||||
* when the user clicks on the rectangle. The other purpose is to allow the user to edit
|
||||
* and manipulate the physical tag boundaries (hence the name, edit handle).
|
||||
* This class should be treated as a logical image tag.
|
||||
*/
|
||||
public final class ImageTag extends Group {
|
||||
|
||||
// Used to tell the 8 edit handles to hide if this tag is no longer selected
|
||||
private final EventDispatchChainImpl ALL_CHILDREN;
|
||||
|
||||
//Notifies listeners that the user has editted the tag boundaries
|
||||
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
|
||||
|
||||
//The underlying presistent tag details that this image tag originates from
|
||||
private final ContentViewerTag<ImageTagRegion> appTag;
|
||||
|
||||
public ImageTag(ContentViewerTag<ImageTagRegion> contentViewerTag, ImageView image) {
|
||||
ALL_CHILDREN = new EventDispatchChainImpl();
|
||||
this.appTag = contentViewerTag;
|
||||
|
||||
this.getChildren().addListener((ListChangeListener<Node>) change -> {
|
||||
change.next();
|
||||
change.getAddedSubList().forEach((node) -> ALL_CHILDREN.append(node.getEventDispatcher()));
|
||||
});
|
||||
|
||||
ImageTagRegion details = contentViewerTag.getDetails();
|
||||
PhysicalTag physicalTag = new PhysicalTag(details);
|
||||
|
||||
//Defines the max allowable boundary that a user may drag any given handle.
|
||||
Boundary dragBoundary = (x, y) -> {
|
||||
double boundingX = image.getX();
|
||||
double boundingY = image.getY();
|
||||
double width = image.getImage().getWidth();
|
||||
double height = image.getImage().getHeight();
|
||||
|
||||
return x > boundingX + details.getStrokeThickness() / 2
|
||||
&& x < boundingX + width - details.getStrokeThickness() / 2
|
||||
&& y > boundingY + details.getStrokeThickness() / 2
|
||||
&& y < boundingY + height - details.getStrokeThickness() / 2;
|
||||
};
|
||||
|
||||
EditHandle bottomLeft = new EditHandle(physicalTag)
|
||||
.setPosition(Position.bottom(), Position.left())
|
||||
.setDrag(dragBoundary, Draggable.bottom(), Draggable.left());
|
||||
|
||||
EditHandle bottomRight = new EditHandle(physicalTag)
|
||||
.setPosition(Position.bottom(), Position.right())
|
||||
.setDrag(dragBoundary, Draggable.bottom(), Draggable.right());
|
||||
|
||||
EditHandle topLeft = new EditHandle(physicalTag)
|
||||
.setPosition(Position.top(), Position.left())
|
||||
.setDrag(dragBoundary, Draggable.top(), Draggable.left());
|
||||
|
||||
EditHandle topRight = new EditHandle(physicalTag)
|
||||
.setPosition(Position.top(), Position.right())
|
||||
.setDrag(dragBoundary, Draggable.top(), Draggable.right());
|
||||
|
||||
EditHandle bottomMiddle = new EditHandle(physicalTag)
|
||||
.setPosition(Position.bottom(), Position.xMiddle())
|
||||
.setDrag(dragBoundary, Draggable.bottom());
|
||||
|
||||
EditHandle topMiddle = new EditHandle(physicalTag)
|
||||
.setPosition(Position.top(), Position.xMiddle())
|
||||
.setDrag(dragBoundary, Draggable.top());
|
||||
|
||||
EditHandle rightMiddle = new EditHandle(physicalTag)
|
||||
.setPosition(Position.right(), Position.yMiddle())
|
||||
.setDrag(dragBoundary, Draggable.right());
|
||||
|
||||
EditHandle leftMiddle = new EditHandle(physicalTag)
|
||||
.setPosition(Position.left(), Position.yMiddle())
|
||||
.setDrag(dragBoundary, Draggable.left());
|
||||
|
||||
//The "logical" tag is the Group
|
||||
this.getChildren().addAll(physicalTag, bottomLeft, bottomRight, topLeft,
|
||||
topRight, bottomMiddle, topMiddle, rightMiddle, leftMiddle);
|
||||
|
||||
Tooltip.install(this, new Tooltip(contentViewerTag.getContentTag()
|
||||
.getName().getDisplayName()));
|
||||
|
||||
this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> ALL_CHILDREN.dispatchEvent(event));
|
||||
this.addEventHandler(ImageTagControls.FOCUSED, event -> ALL_CHILDREN.dispatchEvent(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new listener for edit events. These events are generated when a
|
||||
* user drags on one of the edit "knobs" of the tag.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void subscribeToEditEvents(PropertyChangeListener listener) {
|
||||
pcs.addPropertyChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content viewer tag that this class represents.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ContentViewerTag<ImageTagRegion> getContentViewerTag() {
|
||||
return appTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plain old rectangle that represents an unselected Image Tag
|
||||
*/
|
||||
class PhysicalTag extends Rectangle {
|
||||
|
||||
public PhysicalTag(ImageTagRegion details) {
|
||||
this.setStroke(Color.RED);
|
||||
this.setFill(Color.RED.deriveColor(0, 0, 0, 0));
|
||||
this.setStrokeWidth(details.getStrokeThickness());
|
||||
|
||||
setX(details.getX());
|
||||
setY(details.getY());
|
||||
setWidth(details.getWidth());
|
||||
setHeight(details.getHeight());
|
||||
|
||||
this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> this.setOpacity(1));
|
||||
this.addEventHandler(ImageTagControls.FOCUSED, event -> this.setOpacity(0.5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a portable description of the tag region.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ImageTagRegion getState() {
|
||||
return new ImageTagRegion()
|
||||
.setX(this.getX())
|
||||
.setY(this.getY())
|
||||
.setWidth(this.getWidth())
|
||||
.setHeight(this.getHeight())
|
||||
.setStrokeThickness(this.getStrokeWidth());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draggable "knob" used to manipulate the physical tag boundaries.
|
||||
*/
|
||||
class EditHandle extends Circle {
|
||||
|
||||
private final PhysicalTag parent;
|
||||
|
||||
public EditHandle(PhysicalTag parent) {
|
||||
this.setVisible(false);
|
||||
|
||||
//Hide when the tag is not selected.
|
||||
this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> this.setVisible(false));
|
||||
this.addEventHandler(ImageTagControls.FOCUSED, event -> this.setVisible(true));
|
||||
|
||||
this.setRadius(parent.getStrokeWidth());
|
||||
this.setFill(parent.getStroke());
|
||||
|
||||
this.setOnDragDetected(event -> {
|
||||
this.getParent().setCursor(Cursor.CLOSED_HAND);
|
||||
});
|
||||
|
||||
this.setOnMouseReleased(event -> {
|
||||
this.getParent().setCursor(Cursor.DEFAULT);
|
||||
pcs.firePropertyChange(new PropertyChangeEvent(this, "Tag Edit", null, parent.getState()));
|
||||
});
|
||||
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the positioning of this edit handle on the physical tag.
|
||||
*
|
||||
* @param vals
|
||||
* @return
|
||||
*/
|
||||
public EditHandle setPosition(Position... vals) {
|
||||
for (Position pos : vals) {
|
||||
parent.widthProperty().addListener((obs, oldVal, newVal) -> pos.set(parent, this));
|
||||
parent.heightProperty().addListener((obs, oldVal, newVal) -> pos.set(parent, this));
|
||||
pos.set(parent, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the drag capabilities for manipulating the physical tag.
|
||||
*
|
||||
* @param bounds
|
||||
* @param vals
|
||||
* @return
|
||||
*/
|
||||
public EditHandle setDrag(Boundary bounds, Draggable... vals) {
|
||||
this.setOnMouseDragged((event) -> {
|
||||
for (Draggable drag : vals) {
|
||||
drag.perform(parent, event, bounds);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Position strategies for "sticking" to a location on the physical tag when
|
||||
* it is resized.
|
||||
*/
|
||||
static interface Position {
|
||||
|
||||
void set(PhysicalTag parent, Circle knob);
|
||||
|
||||
static Position left() {
|
||||
return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty());
|
||||
}
|
||||
|
||||
static Position right() {
|
||||
return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty().add(parent.getWidth()));
|
||||
}
|
||||
|
||||
static Position top() {
|
||||
return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty());
|
||||
}
|
||||
|
||||
static Position bottom() {
|
||||
return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty().add(parent.getHeight()));
|
||||
}
|
||||
|
||||
static Position xMiddle() {
|
||||
return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty().add(parent.getWidth() / 2));
|
||||
}
|
||||
|
||||
static Position yMiddle() {
|
||||
return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty().add(parent.getHeight() / 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the bounding box for which dragging is allowable.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
static interface Boundary {
|
||||
|
||||
boolean isPointInBounds(double x, double y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag strategies for manipulating the physical tag from a given side of
|
||||
* the rectangle.
|
||||
*/
|
||||
static interface Draggable {
|
||||
|
||||
void perform(PhysicalTag parent, MouseEvent event, Boundary b);
|
||||
|
||||
static Draggable bottom() {
|
||||
return (parent, event, bounds) -> {
|
||||
if (!bounds.isPointInBounds(event.getX(), event.getY())) {
|
||||
return;
|
||||
}
|
||||
|
||||
double deltaY = event.getY() - parent.getY();
|
||||
if (deltaY > 0) {
|
||||
parent.setHeight(deltaY);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Draggable top() {
|
||||
return (parent, event, bounds) -> {
|
||||
if (!bounds.isPointInBounds(event.getX(), event.getY())) {
|
||||
return;
|
||||
}
|
||||
|
||||
double deltaY = parent.getY() + parent.getHeight() - event.getY();
|
||||
if (deltaY < parent.getY() + parent.getHeight() && deltaY > 0) {
|
||||
parent.setHeight(deltaY);
|
||||
parent.setY(event.getY());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Draggable left() {
|
||||
return (parent, event, bounds) -> {
|
||||
if (!bounds.isPointInBounds(event.getX(), event.getY())) {
|
||||
return;
|
||||
}
|
||||
|
||||
double deltaX = parent.getX() + parent.getWidth() - event.getX();
|
||||
if (deltaX < parent.getX() + parent.getWidth() && deltaX > 0) {
|
||||
parent.setWidth(deltaX);
|
||||
parent.setX(event.getX());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Draggable right() {
|
||||
return (parent, event, bounds) -> {
|
||||
if (!bounds.isPointInBounds(event.getX(), event.getY())) {
|
||||
return;
|
||||
}
|
||||
|
||||
double deltaX = event.getX() - parent.getX();
|
||||
if (deltaX > 0) {
|
||||
parent.setWidth(deltaX);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
|
||||
/**
|
||||
* Focus events for ImageTags to consume. These events trigger selection behavior
|
||||
* on ImageTags and are originated from the ImageTagsGroup class.
|
||||
*/
|
||||
public class ImageTagControls {
|
||||
public static final EventType<Event> NOT_FOCUSED = new EventType<>("NOT_FOCUSED");
|
||||
public static final EventType<Event> FOCUSED = new EventType<>("FOCUSED");
|
||||
}
|
180
Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagCreator.java
Executable file
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyChangeSupport;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
/**
|
||||
* Creates image tags. This class attaches itself to a source image, waiting
|
||||
* for mouse press, mouse drag, and mouse release events. Upon a mouse release
|
||||
* event, any listeners are updated with the portable description of the new tag
|
||||
* boundaries (ImageTagRegion).
|
||||
*/
|
||||
public final class ImageTagCreator extends Rectangle {
|
||||
|
||||
//Origin of the drag event.
|
||||
private double rectangleOriginX, rectangleOriginY;
|
||||
|
||||
//Rectangle lines should be 1.5% of the image. This level of thickness has
|
||||
//a good balance between visual acuity and loss of selection at the borders
|
||||
//of the image.
|
||||
private final static double lineThicknessAsPercent = 1.5;
|
||||
private final double minArea;
|
||||
|
||||
//Used to update listeners of the new tag boundaries
|
||||
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
|
||||
|
||||
private final EventHandler<MouseEvent> mousePressed;
|
||||
private final EventHandler<MouseEvent> mouseDragged;
|
||||
private final EventHandler<MouseEvent> mouseReleased;
|
||||
|
||||
//Handles the unregistering this ImageTagCreator from mouse press, mouse drag,
|
||||
//and mouse release events of the source image.
|
||||
private final Runnable disconnectRunnable;
|
||||
|
||||
/**
|
||||
* Adds tagging support to an image, where the 'tag' rectangle will be the
|
||||
* specified color.
|
||||
*
|
||||
* @param image Image to tag
|
||||
*/
|
||||
public ImageTagCreator(ImageView image) {
|
||||
setStroke(Color.RED);
|
||||
setFill(Color.RED.deriveColor(0, 0, 0, 0));
|
||||
|
||||
//Calculate how many pixels the stroke width should be to guarentee
|
||||
//a consistent % of image consumed by the rectangle border.
|
||||
double min = Math.min(image.getImage().getWidth(), image.getImage().getHeight());
|
||||
double lineThicknessPixels = min * lineThicknessAsPercent / 100.0;
|
||||
setStrokeWidth(lineThicknessPixels);
|
||||
minArea = lineThicknessPixels * lineThicknessPixels;
|
||||
setVisible(false);
|
||||
|
||||
this.mousePressed = (MouseEvent event) -> {
|
||||
if (event.isSecondaryButtonDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Reset box on new click.
|
||||
defaultSettings();
|
||||
rectangleOriginX = event.getX();
|
||||
rectangleOriginY = event.getY();
|
||||
|
||||
setX(rectangleOriginX);
|
||||
setY(rectangleOriginY);
|
||||
};
|
||||
|
||||
image.addEventHandler(MouseEvent.MOUSE_PRESSED, this.mousePressed);
|
||||
|
||||
this.mouseDragged = (MouseEvent event) -> {
|
||||
if (event.isSecondaryButtonDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double currentX = event.getX(), currentY = event.getY();
|
||||
|
||||
/**
|
||||
* Ensure the rectangle is contained within image boundaries and
|
||||
* that the line thickness is kept within bounds.
|
||||
*/
|
||||
double newX = Math.min(Math.max(currentX, image.getX())
|
||||
+ lineThicknessPixels / 2, image.getImage().getWidth() - lineThicknessPixels / 2);
|
||||
double newY = Math.min(Math.max(currentY, image.getY())
|
||||
+ lineThicknessPixels / 2, image.getImage().getHeight() - lineThicknessPixels / 2);
|
||||
|
||||
setVisible(true);
|
||||
double offsetX = newX - rectangleOriginX;
|
||||
if (offsetX < 0) {
|
||||
setX(newX);
|
||||
}
|
||||
setWidth(Math.abs(offsetX));
|
||||
|
||||
double offsetY = newY - rectangleOriginY;
|
||||
if (offsetY < 0) {
|
||||
setY(newY);
|
||||
}
|
||||
setHeight(Math.abs(offsetY));
|
||||
};
|
||||
|
||||
image.addEventHandler(MouseEvent.MOUSE_DRAGGED, this.mouseDragged);
|
||||
|
||||
this.mouseReleased = event -> {
|
||||
//Reject any drags that are too small to count as a meaningful tag.
|
||||
//Meaningful is described as having an area that is visible that is
|
||||
//not consumed by the thickness of the stroke.
|
||||
if ((this.getWidth() - this.getStrokeWidth())
|
||||
* (this.getHeight() - this.getStrokeWidth()) <= minArea) {
|
||||
defaultSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pcs.firePropertyChange(new PropertyChangeEvent(this, "New Tag",
|
||||
null, new ImageTagRegion()
|
||||
.setX(this.getX())
|
||||
.setY(this.getY())
|
||||
.setWidth(this.getWidth())
|
||||
.setHeight(this.getHeight())
|
||||
.setStrokeThickness(lineThicknessPixels)));
|
||||
};
|
||||
|
||||
image.addEventHandler(MouseEvent.MOUSE_RELEASED, this.mouseReleased);
|
||||
|
||||
//Used to remove itself from mouse events on the source image
|
||||
disconnectRunnable = () -> {
|
||||
defaultSettings();
|
||||
image.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleased);
|
||||
image.removeEventHandler(MouseEvent.MOUSE_DRAGGED, mouseDragged);
|
||||
image.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressed);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a PCL for new tag events. Listeners are updated with a portable
|
||||
* description (ImageTagRegion) of the new tag, which represent the
|
||||
* rectangle boundaries.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void addNewTagListener(PropertyChangeListener listener) {
|
||||
this.pcs.addPropertyChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes itself from mouse events on the source image.
|
||||
*/
|
||||
public void disconnect() {
|
||||
this.disconnectRunnable.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the rectangle to default dimensions.
|
||||
*/
|
||||
private void defaultSettings() {
|
||||
setWidth(0);
|
||||
setHeight(0);
|
||||
setVisible(false);
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
/**
|
||||
* Bean representation of an image tag. This class is used for storage and
|
||||
* retrieval of ImageTags from the case database.
|
||||
*/
|
||||
public class ImageTagRegion {
|
||||
|
||||
/**
|
||||
* These fields will be serialized and stored in the case database by the
|
||||
* ContentViewerTagManager.
|
||||
*/
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
|
||||
private double strokeThickness;
|
||||
|
||||
public ImageTagRegion setStrokeThickness(double thickness) {
|
||||
this.strokeThickness = thickness;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageTagRegion setX(double x) {
|
||||
this.x = x;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageTagRegion setWidth(double width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageTagRegion setY(double y) {
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageTagRegion setHeight(double height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public double getStrokeThickness() {
|
||||
return strokeThickness;
|
||||
}
|
||||
}
|
134
Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsGroup.java
Executable file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
import com.sun.javafx.event.EventDispatchChainImpl;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.beans.PropertyChangeSupport;
|
||||
import javafx.event.Event;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
/**
|
||||
* Manages the focus and z-ordering of ImageTags. Only one image tag may be
|
||||
* selected at a time. Image tags show their 8 edit "handles" upon selection
|
||||
* (see ImageTag class for more details) and get the highest z-ordering to make
|
||||
* editing easier. This class is responsible for setting and dropping focus as
|
||||
* the user navigates from tag to tag. The ImageTag is treated as a logical
|
||||
* unit, however it's underlying representation consists of the physical
|
||||
* rectangle and the 8 edit handles. JavaFX will report selection on the Node
|
||||
* level (so either the Rectangle, or a singe edit handle), which makes keeping
|
||||
* the entire image tag in focus a non-trivial problem.
|
||||
*/
|
||||
public final class ImageTagsGroup extends Group {
|
||||
|
||||
private final EventDispatchChainImpl NO_OP_CHAIN = new EventDispatchChainImpl();
|
||||
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
|
||||
|
||||
private volatile ImageTag currentFocus;
|
||||
|
||||
public ImageTagsGroup(Node backDrop) {
|
||||
|
||||
//Reset focus of current selection if the back drop has focus.
|
||||
backDrop.setOnMousePressed((mouseEvent) -> {
|
||||
if (currentFocus != null) {
|
||||
currentFocus.getEventDispatcher().dispatchEvent(
|
||||
new Event(ImageTagControls.NOT_FOCUSED), NO_OP_CHAIN);
|
||||
}
|
||||
|
||||
this.pcs.firePropertyChange(new PropertyChangeEvent(this,
|
||||
ImageTagControls.NOT_FOCUSED.getName(), currentFocus, null));
|
||||
currentFocus = null;
|
||||
});
|
||||
|
||||
//Set the focus of selected tag
|
||||
this.addEventFilter(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
|
||||
if (!e.isPrimaryButtonDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Pull out the logical image tag that this node is associated with
|
||||
Node topLevelChild = e.getPickResult().getIntersectedNode();
|
||||
while (!this.getChildren().contains(topLevelChild)) {
|
||||
topLevelChild = topLevelChild.getParent();
|
||||
}
|
||||
|
||||
requestFocus((ImageTag) topLevelChild);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to focus change events on Image tags.
|
||||
*
|
||||
* @param fcl PCL to be notified which Image tag has been selected.
|
||||
*/
|
||||
public void addFocusChangeListener(PropertyChangeListener fcl) {
|
||||
this.pcs.addPropertyChangeListener(fcl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image tag that current has focus.
|
||||
*
|
||||
* @return ImageTag instance or null if no tag is in focus.
|
||||
*/
|
||||
public ImageTag getFocus() {
|
||||
return currentFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current focus
|
||||
*/
|
||||
public void clearFocus() {
|
||||
if(currentFocus != null) {
|
||||
resetFocus(currentFocus);
|
||||
currentFocus = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the logical image tag that it is no longer in focus.
|
||||
*
|
||||
* @param n
|
||||
*/
|
||||
private void resetFocus(ImageTag n) {
|
||||
n.getEventDispatcher().dispatchEvent(new Event(ImageTagControls.NOT_FOCUSED), NO_OP_CHAIN);
|
||||
this.pcs.firePropertyChange(new PropertyChangeEvent(this, ImageTagControls.NOT_FOCUSED.getName(), n, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the logical image that it is in focus.
|
||||
*
|
||||
* @param n
|
||||
*/
|
||||
private void requestFocus(ImageTag n) {
|
||||
if (n.equals(currentFocus)) {
|
||||
return;
|
||||
} else if (currentFocus != null && !currentFocus.equals(n)) {
|
||||
resetFocus(currentFocus);
|
||||
}
|
||||
|
||||
n.getEventDispatcher().dispatchEvent(new Event(ImageTagControls.FOCUSED), NO_OP_CHAIN);
|
||||
this.pcs.firePropertyChange(new PropertyChangeEvent(this, ImageTagControls.FOCUSED.getName(), currentFocus, n));
|
||||
|
||||
currentFocus = n;
|
||||
n.toFront();
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.contentviewers.imagetagging;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.MatOfByte;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.highgui.Highgui;
|
||||
import org.sleuthkit.datamodel.AbstractFile;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
|
||||
/**
|
||||
* Utility class for handling content viewer tags on images.
|
||||
*/
|
||||
public final class ImageTagsUtility {
|
||||
|
||||
/**
|
||||
* Embeds the tag regions into an image (represented as an AbstractFile).
|
||||
*
|
||||
* @param file Base Image
|
||||
* @param tagRegions Tag regions to be saved into the image
|
||||
* @param outputEncoding Output file type encoding (ex. .jpg, .png)
|
||||
* @return output image in byte array
|
||||
* @throws TskCoreException
|
||||
*/
|
||||
public static byte[] exportTags(AbstractFile file, Collection<ImageTagRegion> tagRegions, String outputEncoding) throws TskCoreException {
|
||||
byte[] imageInMemory = new byte[(int) file.getSize()];
|
||||
file.read(imageInMemory, 0, file.getSize());
|
||||
Mat originalImage = Highgui.imdecode(new MatOfByte(imageInMemory), Highgui.IMREAD_UNCHANGED);
|
||||
|
||||
tagRegions.forEach((region) -> {
|
||||
Core.rectangle(
|
||||
originalImage, //Matrix obj of the image
|
||||
new Point(region.getX(), region.getY()), //p1
|
||||
new Point(region.getX() + region.getWidth(), region.getY() + region.getHeight()), //p2
|
||||
new Scalar(0, 0, 255), //Scalar object for color
|
||||
(int) Math.rint(region.getStrokeThickness())
|
||||
);
|
||||
});
|
||||
|
||||
MatOfByte matOfByte = new MatOfByte();
|
||||
Highgui.imencode(outputEncoding, originalImage, matOfByte);
|
||||
|
||||
originalImage.release();
|
||||
byte[] output = matOfByte.toArray();
|
||||
matOfByte.release();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private ImageTagsUtility(){
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import org.openide.nodes.Node;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.openide.util.lookup.ServiceProvider;
|
||||
import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer;
|
||||
import org.sleuthkit.datamodel.AbstractFile;
|
||||
|
||||
/**
|
||||
* A DataContentViewer that displays text with the TextViewers available.
|
||||
@ -90,6 +91,17 @@ public class TextContentViewer implements DataContentViewer {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
// get the node's File, if it has one
|
||||
AbstractFile file = node.getLookup().lookup(AbstractFile.class);
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disable the text content viewer for directories and empty files
|
||||
if (file.isDir() || file.getSize() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return panel.isSupported(node);
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ public class TextContentViewerPanel extends javax.swing.JPanel implements DataCo
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterime wether the content viewer which displays this panel isSupported.
|
||||
* Determine whether the content viewer which displays this panel isSupported.
|
||||
* This panel is supported if any of the TextViewer's displayed in it are
|
||||
* supported.
|
||||
*
|
||||
|
@ -504,10 +504,10 @@ public final class UserPreferences {
|
||||
/**
|
||||
* Get the maximum number of results to display in a result table.
|
||||
*
|
||||
* @return Saved value or default (0) which indicates no max.
|
||||
* @return Saved value or default (10,000).
|
||||
*/
|
||||
public static int getResultsTablePageSize() {
|
||||
return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 0);
|
||||
return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 10_000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,4 +216,5 @@ DataResultViewerTable.pagesLabel.text=Pages:
|
||||
DataResultViewerTable.pageNumLabel.text=
|
||||
DataResultViewerTable.pageLabel.text=Page:
|
||||
ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table:
|
||||
ViewPreferencesPanel.maxResultsLabel.toolTipText=<html>\nAll results are shown in the results table if this value is 0 (default).\n<br>You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n</html>
|
||||
ViewPreferencesPanel.maxResultsLabel.toolTipText=<html>\nSetting this value to 0 will display all results in the results table.\n<br>Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n</html>
|
||||
DataResultViewerTable.exportCSVButton.text=Save table as CSV
|
||||
|
@ -32,6 +32,7 @@ DataResultViewerTable.commentRenderer.noComment.toolTip=No comments found
|
||||
DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on associated tag(s)
|
||||
DataResultViewerTable.countRender.name=O
|
||||
DataResultViewerTable.countRender.toolTip=O(ccurrences) indicates the number of data sources containing the item in the Central Repository
|
||||
DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export
|
||||
DataResultViewerTable.firstColLbl=Name
|
||||
DataResultViewerTable.goToPageTextField.err=Invalid page number
|
||||
# {0} - totalPages
|
||||
@ -269,4 +270,5 @@ DataResultViewerTable.pagesLabel.text=Pages:
|
||||
DataResultViewerTable.pageNumLabel.text=
|
||||
DataResultViewerTable.pageLabel.text=Page:
|
||||
ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table:
|
||||
ViewPreferencesPanel.maxResultsLabel.toolTipText=<html>\nAll results are shown in the results table if this value is 0 (default).\n<br>You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n</html>
|
||||
ViewPreferencesPanel.maxResultsLabel.toolTipText=<html>\nSetting this value to 0 will display all results in the results table.\n<br>Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n</html>
|
||||
DataResultViewerTable.exportCSVButton.text=Save table as CSV
|
||||
|
@ -16,13 +16,13 @@
|
||||
<Layout>
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="outlineView" alignment="0" max="32767" attributes="0"/>
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace pref="608" max="32767" attributes="0"/>
|
||||
<Component id="outlineView" pref="904" max="32767" attributes="0"/>
|
||||
<Group type="102" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="pageLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="pageNumLabel" min="-2" pref="53" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="14" max="-2" attributes="0"/>
|
||||
<Component id="pagesLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Component id="pagePrevButton" linkSize="1" min="-2" pref="16" max="-2" attributes="0"/>
|
||||
@ -32,14 +32,15 @@
|
||||
<Component id="gotoPageLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="gotoPageTextField" min="-2" pref="33" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
<Component id="exportCSVButton" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="3" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="2" attributes="0">
|
||||
<Component id="pageLabel" alignment="2" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="pageNumLabel" alignment="2" min="-2" max="-2" attributes="0"/>
|
||||
@ -48,9 +49,10 @@
|
||||
<Component id="pageNextButton" linkSize="2" alignment="2" min="-2" pref="15" max="-2" attributes="0"/>
|
||||
<Component id="gotoPageLabel" alignment="2" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="gotoPageTextField" alignment="2" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="exportCSVButton" alignment="2" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
<Component id="outlineView" pref="324" max="32767" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="3" max="-2" attributes="0"/>
|
||||
<Component id="outlineView" pref="321" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
@ -164,5 +166,15 @@
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="gotoPageTextFieldActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="exportCSVButton">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/corecomponents/Bundle.properties" key="DataResultViewerTable.exportCSVButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="exportCSVButtonActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Form>
|
||||
|
@ -77,6 +77,7 @@ import org.openide.util.lookup.ServiceProvider;
|
||||
import org.sleuthkit.autopsy.core.UserPreferences;
|
||||
import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
|
||||
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
|
||||
import org.sleuthkit.autopsy.datamodel.NodeProperty;
|
||||
import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo;
|
||||
@ -176,6 +177,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
initComponents();
|
||||
|
||||
initializePagingSupport();
|
||||
|
||||
/*
|
||||
* Disable the CSV export button for the common properties results
|
||||
*/
|
||||
if (this instanceof org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributesSearchResultsViewerTable) {
|
||||
exportCSVButton.setEnabled(false);
|
||||
}
|
||||
|
||||
/*
|
||||
* Configure the child OutlineView (explorer view) component.
|
||||
@ -1291,6 +1299,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL);
|
||||
gotoPageLabel = new javax.swing.JLabel();
|
||||
gotoPageTextField = new javax.swing.JTextField();
|
||||
exportCSVButton = new javax.swing.JButton();
|
||||
|
||||
pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageLabel.text")); // NOI18N
|
||||
|
||||
@ -1338,17 +1347,24 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
}
|
||||
});
|
||||
|
||||
exportCSVButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.exportCSVButton.text")); // NOI18N
|
||||
exportCSVButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
exportCSVButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||
.addContainerGap(608, Short.MAX_VALUE)
|
||||
.addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 904, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(pageLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGap(14, 14, 14)
|
||||
.addComponent(pagesLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
@ -1358,7 +1374,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
.addComponent(gotoPageLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addContainerGap())
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(exportCSVButton))
|
||||
);
|
||||
|
||||
layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {pageNextButton, pagePrevButton});
|
||||
@ -1366,7 +1383,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGap(3, 3, 3)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER)
|
||||
.addComponent(pageLabel)
|
||||
.addComponent(pageNumLabel)
|
||||
@ -1374,9 +1391,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
.addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(gotoPageLabel)
|
||||
.addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 324, Short.MAX_VALUE)
|
||||
.addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(exportCSVButton))
|
||||
.addGap(3, 3, 3)
|
||||
.addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 321, Short.MAX_VALUE)
|
||||
.addContainerGap())
|
||||
);
|
||||
|
||||
@ -1397,7 +1415,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer {
|
||||
pagingSupport.gotoPage();
|
||||
}//GEN-LAST:event_gotoPageTextFieldActionPerformed
|
||||
|
||||
@NbBundle.Messages({"DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export"
|
||||
})
|
||||
private void exportCSVButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportCSVButtonActionPerformed
|
||||
Node currentRoot = this.getExplorerManager().getRootContext();
|
||||
if (currentRoot != null && currentRoot.getChildren().getNodesCount() > 0) {
|
||||
org.sleuthkit.autopsy.directorytree.ExportCSVAction.saveNodesToCSV(java.util.Arrays.asList(currentRoot.getChildren().getNodes()), this);
|
||||
} else {
|
||||
MessageNotifyUtil.Message.info(Bundle.DataResultViewerTable_exportCSVButtonActionPerformed_empty());
|
||||
}
|
||||
}//GEN-LAST:event_exportCSVButtonActionPerformed
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JButton exportCSVButton;
|
||||
private javax.swing.JLabel gotoPageLabel;
|
||||
private javax.swing.JTextField gotoPageTextField;
|
||||
private org.openide.explorer.view.OutlineView outlineView;
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2011-2018 Basis Technology Corp.
|
||||
* Copyright 2011-2019 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datamodel;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.lang.ref.WeakReference;
|
||||
@ -27,8 +26,6 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
@ -65,6 +62,7 @@ import org.sleuthkit.datamodel.AbstractFile;
|
||||
import org.sleuthkit.datamodel.BlackboardArtifact;
|
||||
import org.sleuthkit.datamodel.Content;
|
||||
import org.sleuthkit.datamodel.ContentTag;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.TskData;
|
||||
|
||||
@ -76,15 +74,10 @@ import org.sleuthkit.datamodel.TskData;
|
||||
public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends AbstractContentNode<T> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AbstractAbstractFileNode.class.getName());
|
||||
@NbBundle.Messages("AbstractAbstractFileNode.addFileProperty.desc=no description")
|
||||
private static final String NO_DESCR = AbstractAbstractFileNode_addFileProperty_desc();
|
||||
|
||||
private static final Set<Case.Events> CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE,
|
||||
Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, Case.Events.CR_COMMENT_CHANGED);
|
||||
|
||||
private static final ExecutorService translationPool;
|
||||
private static final Integer MAX_POOL_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param abstractFile file to wrap
|
||||
*/
|
||||
@ -101,7 +94,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
}
|
||||
|
||||
if (UserPreferences.displayTranslatedFileNames()) {
|
||||
AbstractAbstractFileNode.translationPool.submit(new TranslationTask(
|
||||
backgroundTasksPool.submit(new TranslationTask(
|
||||
new WeakReference<>(this), weakPcl));
|
||||
}
|
||||
|
||||
@ -109,14 +102,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
// or when tags are added.
|
||||
Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl);
|
||||
}
|
||||
|
||||
static {
|
||||
//Initialize this pool only once! This will be used by every instance of AAFN
|
||||
//to do their heavy duty SCO column and translation updates.
|
||||
translationPool = Executors.newFixedThreadPool(MAX_POOL_SIZE,
|
||||
new ThreadFactoryBuilder().setNameFormat("translation-task-thread-%d").build());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The finalizer removes event listeners as the BlackboardArtifactNode is
|
||||
* being garbage collected. Yes, we know that finalizers are considered to
|
||||
@ -137,16 +123,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event signals to indicate the background tasks have completed processing.
|
||||
* Currently, we have one property task in the background:
|
||||
*
|
||||
* 1) Retreiving the translation of the file name
|
||||
*/
|
||||
enum NodeSpecificEvents {
|
||||
TRANSLATION_AVAILABLE,
|
||||
}
|
||||
|
||||
|
||||
private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> {
|
||||
String eventType = evt.getPropertyName();
|
||||
|
||||
@ -190,7 +167,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
} else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) {
|
||||
ContentTagAddedEvent event = (ContentTagAddedEvent) evt;
|
||||
if (event.getAddedTag().getContent().equals(content)) {
|
||||
List<ContentTag> tags = getContentTagsFromDatabase();
|
||||
List<Tag> tags = this.getAllTagsFromDatabase();
|
||||
Pair<Score, String> scorePropAndDescr = getScorePropertyAndDescription(tags);
|
||||
Score value = scorePropAndDescr.getLeft();
|
||||
String descr = scorePropAndDescr.getRight();
|
||||
@ -202,7 +179,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
} else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) {
|
||||
ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt;
|
||||
if (event.getDeletedTagInfo().getContentID() == content.getId()) {
|
||||
List<ContentTag> tags = getContentTagsFromDatabase();
|
||||
List<Tag> tags = getAllTagsFromDatabase();
|
||||
Pair<Score, String> scorePropAndDescr = getScorePropertyAndDescription(tags);
|
||||
Score value = scorePropAndDescr.getLeft();
|
||||
String descr = scorePropAndDescr.getRight();
|
||||
@ -214,7 +191,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
} else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) {
|
||||
CommentChangedEvent event = (CommentChangedEvent) evt;
|
||||
if (event.getContentID() == content.getId()) {
|
||||
List<ContentTag> tags = getContentTagsFromDatabase();
|
||||
List<Tag> tags = getAllTagsFromDatabase();
|
||||
CorrelationAttributeInstance attribute = getCorrelationAttributeInstance();
|
||||
updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, attribute)));
|
||||
}
|
||||
@ -223,6 +200,18 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
//Set the tooltip
|
||||
this.setShortDescription(content.getName());
|
||||
updateSheet(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, content.getName()));
|
||||
} else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString())) {
|
||||
SCOData scoData = (SCOData)evt.getNewValue();
|
||||
if (scoData.getScoreAndDescription() != null) {
|
||||
updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft()));
|
||||
}
|
||||
if (scoData.getComment() != null) {
|
||||
updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, scoData.getComment()));
|
||||
}
|
||||
if (scoData.getCountAndDescription() != null &&
|
||||
!UserPreferences.hideCentralRepoCommentsAndOccurrences()) {
|
||||
updateSheet(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft()));
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
@ -235,38 +224,6 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
*/
|
||||
private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null);
|
||||
|
||||
/**
|
||||
* Updates the values of the properties in the current property sheet with
|
||||
* the new properties being passed in. Only if that property exists in the
|
||||
* current sheet will it be applied. That way, we allow for subclasses to
|
||||
* add their own (or omit some!) properties and we will not accidentally
|
||||
* disrupt their UI.
|
||||
*
|
||||
* Race condition if not synchronized. Only one update should be applied at
|
||||
* a time.
|
||||
*
|
||||
* @param newProps New file property instances to be updated in the current
|
||||
* sheet.
|
||||
*/
|
||||
private synchronized void updateSheet(NodeProperty<?>... newProps) {
|
||||
//Refresh ONLY those properties in the sheet currently. Subclasses may have
|
||||
//only added a subset of our properties or their own props.s
|
||||
Sheet visibleSheet = this.getSheet();
|
||||
Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES);
|
||||
Property<?>[] visibleProps = visibleSheetSet.getProperties();
|
||||
for (NodeProperty<?> newProp : newProps) {
|
||||
for (int i = 0; i < visibleProps.length; i++) {
|
||||
if (visibleProps[i].getName().equals(newProp.getName())) {
|
||||
visibleProps[i] = newProp;
|
||||
}
|
||||
}
|
||||
}
|
||||
visibleSheetSet.put(visibleProps);
|
||||
visibleSheet.put(visibleSheetSet);
|
||||
//setSheet() will notify Netbeans to update this node in the UI.
|
||||
this.setSheet(visibleSheet);
|
||||
}
|
||||
|
||||
/*
|
||||
* This is called when the node is first initialized. Any new updates or
|
||||
* changes happen by directly manipulating the sheet. That means we can fire
|
||||
@ -368,18 +325,18 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
properties.add(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, ""));
|
||||
}
|
||||
|
||||
//SCO column prereq info..
|
||||
List<ContentTag> tags = getContentTagsFromDatabase();
|
||||
CorrelationAttributeInstance attribute = getCorrelationAttributeInstance();
|
||||
|
||||
Pair<DataResultViewerTable.Score, String> scoreAndDescription = getScorePropertyAndDescription(tags);
|
||||
properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoreAndDescription.getRight(), scoreAndDescription.getLeft()));
|
||||
DataResultViewerTable.HasCommentStatus comment = getCommentProperty(tags, attribute);
|
||||
properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, comment));
|
||||
if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) {
|
||||
Pair<Long, String> countAndDescription = getCountPropertyAndDescription(attribute);
|
||||
properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), countAndDescription.getRight(), countAndDescription.getLeft()));
|
||||
// Create place holders for S C O
|
||||
properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), VALUE_LOADING, ""));
|
||||
properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), VALUE_LOADING, ""));
|
||||
if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) {
|
||||
properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), VALUE_LOADING, ""));
|
||||
}
|
||||
|
||||
|
||||
// Get the SCO columns data in a background task
|
||||
backgroundTasksPool.submit(new GetSCOTask(
|
||||
new WeakReference<>(this), weakPcl));
|
||||
|
||||
properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content)));
|
||||
properties.add(new NodeProperty<>(MOD_TIME.toString(), MOD_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getMtime(), content)));
|
||||
properties.add(new NodeProperty<>(CHANGED_TIME.toString(), CHANGED_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getCtime(), content)));
|
||||
@ -441,7 +398,8 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
"AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated",
|
||||
"# {0} - occuranceCount",
|
||||
"AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"})
|
||||
Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute) {
|
||||
@Override
|
||||
protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute) {
|
||||
Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting
|
||||
String description = Bundle.AbstractAbstractFileNode_createSheet_count_noCentralRepo_description();
|
||||
try {
|
||||
@ -468,7 +426,8 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
"AbstractAbstractFileNode.createSheet.taggedFile.description=File has been tagged.",
|
||||
"AbstractAbstractFileNode.createSheet.notableTaggedFile.description=File tagged with notable tag.",
|
||||
"AbstractAbstractFileNode.createSheet.noScore.description=No score"})
|
||||
Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<ContentTag> tags) {
|
||||
@Override
|
||||
protected Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<Tag> tags) {
|
||||
DataResultViewerTable.Score score = DataResultViewerTable.Score.NO_SCORE;
|
||||
String description = Bundle.AbstractAbstractFileNode_createSheet_noScore_description();
|
||||
if (content.getKnown() == TskData.FileKnown.BAD) {
|
||||
@ -486,7 +445,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
if (!tags.isEmpty() && (score == DataResultViewerTable.Score.NO_SCORE || score == DataResultViewerTable.Score.INTERESTING_SCORE)) {
|
||||
score = DataResultViewerTable.Score.INTERESTING_SCORE;
|
||||
description = Bundle.AbstractAbstractFileNode_createSheet_taggedFile_description();
|
||||
for (ContentTag tag : tags) {
|
||||
for (Tag tag : tags) {
|
||||
if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) {
|
||||
score = DataResultViewerTable.Score.NOTABLE_SCORE;
|
||||
description = Bundle.AbstractAbstractFileNode_createSheet_notableTaggedFile_description();
|
||||
@ -499,11 +458,12 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
|
||||
@NbBundle.Messages({
|
||||
"AbstractAbstractFileNode.createSheet.comment.displayName=C"})
|
||||
HasCommentStatus getCommentProperty(List<ContentTag> tags, CorrelationAttributeInstance attribute) {
|
||||
@Override
|
||||
protected HasCommentStatus getCommentProperty(List<Tag> tags, CorrelationAttributeInstance attribute) {
|
||||
|
||||
DataResultViewerTable.HasCommentStatus status = !tags.isEmpty() ? DataResultViewerTable.HasCommentStatus.TAG_NO_COMMENT : DataResultViewerTable.HasCommentStatus.NO_COMMENT;
|
||||
|
||||
for (ContentTag tag : tags) {
|
||||
for (Tag tag : tags) {
|
||||
if (!StringUtils.isBlank(tag.getComment())) {
|
||||
//if the tag is null or empty or contains just white space it will indicate there is not a comment
|
||||
status = DataResultViewerTable.HasCommentStatus.TAG_COMMENT;
|
||||
@ -571,7 +531,13 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
|
||||
return tags;
|
||||
}
|
||||
|
||||
CorrelationAttributeInstance getCorrelationAttributeInstance() {
|
||||
@Override
|
||||
protected List<Tag> getAllTagsFromDatabase() {
|
||||
return new ArrayList<>(getContentTagsFromDatabase());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CorrelationAttributeInstance getCorrelationAttributeInstance() {
|
||||
CorrelationAttributeInstance attribute = null;
|
||||
if (EamDb.isEnabled() && !UserPreferences.hideCentralRepoCommentsAndOccurrences()) {
|
||||
attribute = EamArtifactUtil.getInstanceFromContent(content);
|
||||
|
@ -18,20 +18,29 @@
|
||||
*/
|
||||
package org.sleuthkit.autopsy.datamodel;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.logging.Level;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openide.nodes.Children;
|
||||
import org.openide.nodes.Sheet;
|
||||
|
||||
import org.openide.util.lookup.Lookups;
|
||||
import org.openide.util.Lookup;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
|
||||
import org.sleuthkit.datamodel.Content;
|
||||
import org.sleuthkit.datamodel.SleuthkitCase;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.TskData;
|
||||
import org.sleuthkit.datamodel.TskException;
|
||||
@ -50,6 +59,38 @@ public abstract class AbstractContentNode<T extends Content> extends ContentNode
|
||||
T content;
|
||||
private static final Logger logger = Logger.getLogger(AbstractContentNode.class.getName());
|
||||
|
||||
/**
|
||||
* A pool of background tasks to run any long computation needed to
|
||||
* populate this node.
|
||||
*/
|
||||
static final ExecutorService backgroundTasksPool;
|
||||
private static final Integer MAX_POOL_SIZE = 10;
|
||||
|
||||
/**
|
||||
* Default no description string
|
||||
*/
|
||||
@NbBundle.Messages({"AbstractContentNode.nodescription=no description",
|
||||
"AbstractContentNode.valueLoading=value loading"})
|
||||
protected static final String NO_DESCR = Bundle.AbstractContentNode_nodescription();
|
||||
protected static final String VALUE_LOADING = Bundle.AbstractContentNode_valueLoading();
|
||||
|
||||
/**
|
||||
* Event signals to indicate the background tasks have completed processing.
|
||||
* Currently, we have one property task in the background:
|
||||
*
|
||||
* 1) Retrieving the translation of the file name
|
||||
*/
|
||||
enum NodeSpecificEvents {
|
||||
TRANSLATION_AVAILABLE,
|
||||
SCO_AVAILABLE
|
||||
}
|
||||
|
||||
static {
|
||||
//Initialize this pool only once! This will be used by every instance of AAFN
|
||||
//to do their heavy duty SCO column and translation updates.
|
||||
backgroundTasksPool = Executors.newFixedThreadPool(MAX_POOL_SIZE,
|
||||
new ThreadFactoryBuilder().setNameFormat("content-node-background-task-%d").build());
|
||||
}
|
||||
/**
|
||||
* Handles aspects that depend on the Content object
|
||||
*
|
||||
@ -240,4 +281,79 @@ public abstract class AbstractContentNode<T extends Content> extends ContentNode
|
||||
public int read(byte[] buf, long offset, long len) throws TskException {
|
||||
return content.read(buf, offset, len);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the values of the properties in the current property sheet with
|
||||
* the new properties being passed in. Only if that property exists in the
|
||||
* current sheet will it be applied. That way, we allow for subclasses to
|
||||
* add their own (or omit some!) properties and we will not accidentally
|
||||
* disrupt their UI.
|
||||
*
|
||||
* Race condition if not synchronized. Only one update should be applied at
|
||||
* a time.
|
||||
*
|
||||
* @param newProps New file property instances to be updated in the current
|
||||
* sheet.
|
||||
*/
|
||||
protected synchronized void updateSheet(NodeProperty<?>... newProps) {
|
||||
//Refresh ONLY those properties in the sheet currently. Subclasses may have
|
||||
//only added a subset of our properties or their own props.s
|
||||
Sheet visibleSheet = this.getSheet();
|
||||
Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES);
|
||||
Property<?>[] visibleProps = visibleSheetSet.getProperties();
|
||||
for (NodeProperty<?> newProp : newProps) {
|
||||
for (int i = 0; i < visibleProps.length; i++) {
|
||||
if (visibleProps[i].getName().equals(newProp.getName())) {
|
||||
visibleProps[i] = newProp;
|
||||
}
|
||||
}
|
||||
}
|
||||
visibleSheetSet.put(visibleProps);
|
||||
visibleSheet.put(visibleSheetSet);
|
||||
//setSheet() will notify Netbeans to update this node in the UI.
|
||||
this.setSheet(visibleSheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns a list of all tags associated with this content node.
|
||||
*
|
||||
* @return list of tags associated with the node.
|
||||
*/
|
||||
abstract protected List<Tag> getAllTagsFromDatabase();
|
||||
|
||||
/**
|
||||
* Returns correlation attribute instance for the underlying content of the node.
|
||||
*
|
||||
* @return correlation attribute instance for the underlying content of the node.
|
||||
*/
|
||||
abstract protected CorrelationAttributeInstance getCorrelationAttributeInstance();
|
||||
|
||||
/**
|
||||
* Returns Score property for the node.
|
||||
*
|
||||
* @param tags list of tags.
|
||||
*
|
||||
* @return Score property for the underlying content of the node.
|
||||
*/
|
||||
abstract protected Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<Tag> tags);
|
||||
|
||||
/**
|
||||
* Returns comment property for the node.
|
||||
*
|
||||
* @param tags list of tags
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return Comment property for the underlying content of the node.
|
||||
*/
|
||||
abstract protected DataResultViewerTable.HasCommentStatus getCommentProperty(List<Tag> tags, CorrelationAttributeInstance attribute);
|
||||
|
||||
/**
|
||||
* Returns occurrences/count property for the node.
|
||||
*
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return count property for the underlying content of the node.
|
||||
*/
|
||||
abstract protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute);
|
||||
}
|
||||
|
@ -286,7 +286,12 @@ public abstract class BaseChildFactory<T extends Content> extends ChildFactory.D
|
||||
* If pageSize is set split keys into pages, otherwise create a
|
||||
* single page containing all keys.
|
||||
*/
|
||||
pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size());
|
||||
if (keys.isEmpty()) {
|
||||
pages.clear();
|
||||
} else {
|
||||
pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size());
|
||||
}
|
||||
|
||||
if (pages.size() != oldPageCount) {
|
||||
try {
|
||||
// Number of pages has changed so we need to send out a notification.
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2011-2018 Basis Technology Corp.
|
||||
* Copyright 2011-2019 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -22,6 +22,7 @@ import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -37,6 +38,7 @@ import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.swing.Action;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openide.nodes.Sheet;
|
||||
import org.openide.util.Lookup;
|
||||
import org.openide.util.NbBundle;
|
||||
@ -55,11 +57,13 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException;
|
||||
import org.sleuthkit.autopsy.core.UserPreferences;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
|
||||
import static org.sleuthkit.autopsy.datamodel.DisplayableItemNode.findLinked;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus;
|
||||
import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.backgroundTasksPool;
|
||||
import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager;
|
||||
import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction;
|
||||
import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction;
|
||||
@ -95,10 +99,7 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
private Content associated = null;
|
||||
|
||||
private List<NodeProperty<? extends Object>> customProperties;
|
||||
|
||||
private final static String NO_DESCR = NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.noDesc.text");
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Artifact types which should have the full unique path of the associated
|
||||
* content as a property.
|
||||
@ -151,6 +152,19 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
contentCache.invalidateAll();
|
||||
}
|
||||
}
|
||||
else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString())) {
|
||||
SCOData scoData = (SCOData)evt.getNewValue();
|
||||
if (scoData.getScoreAndDescription() != null) {
|
||||
updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft()));
|
||||
}
|
||||
if (scoData.getComment() != null) {
|
||||
updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, scoData.getComment()));
|
||||
}
|
||||
if (scoData.getCountAndDescription() != null &&
|
||||
!UserPreferences.hideCentralRepoCommentsAndOccurrences()) {
|
||||
updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft()));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -335,8 +349,6 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
@Override
|
||||
protected Sheet createSheet() {
|
||||
Sheet sheet = super.createSheet();
|
||||
List<Tag> tags = getAllTagsFromDatabase();
|
||||
|
||||
Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES);
|
||||
if (sheetSet == null) {
|
||||
sheetSet = Sheet.createPropertiesSet();
|
||||
@ -351,17 +363,17 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
NO_DESCR,
|
||||
this.getSourceName()));
|
||||
|
||||
addScoreProperty(sheetSet, tags);
|
||||
|
||||
CorrelationAttributeInstance correlationAttribute = null;
|
||||
// Create place holders for S C O
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), VALUE_LOADING, ""));
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), VALUE_LOADING, ""));
|
||||
if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) {
|
||||
correlationAttribute = getCorrelationAttributeInstance();
|
||||
}
|
||||
addCommentProperty(sheetSet, tags, correlationAttribute);
|
||||
|
||||
if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) {
|
||||
addCountProperty(sheetSet, correlationAttribute);
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), VALUE_LOADING, ""));
|
||||
}
|
||||
|
||||
// Get the SCO columns data in a background task
|
||||
backgroundTasksPool.submit(new GetSCOTask(
|
||||
new WeakReference<>(this), weakPcl));
|
||||
|
||||
if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) {
|
||||
try {
|
||||
BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT));
|
||||
@ -520,6 +532,7 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
* @return a list of tags which on the artifact or the file it is associated
|
||||
* with
|
||||
*/
|
||||
@Override
|
||||
protected final List<Tag> getAllTagsFromDatabase() {
|
||||
List<Tag> tags = new ArrayList<>();
|
||||
try {
|
||||
@ -569,6 +582,7 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", "))));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final CorrelationAttributeInstance getCorrelationAttributeInstance() {
|
||||
CorrelationAttributeInstance correlationAttribute = null;
|
||||
if (EamDb.isEnabled()) {
|
||||
@ -581,16 +595,36 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
* Used by (subclasses of) BlackboardArtifactNode to add the comment
|
||||
* property to their sheets.
|
||||
*
|
||||
* @param sheetSet the modifiable Sheet.Set returned by
|
||||
* Sheet.get(Sheet.PROPERTIES)
|
||||
* @param sheetSet the modifiable Sheet.Set to add the property to
|
||||
* @param tags the list of tags associated with the file
|
||||
* @param attribute the correlation attribute associated with this
|
||||
* artifact's associated file, null if central repo is not
|
||||
* enabled
|
||||
*
|
||||
* @deprecated Use the GetSCOTask to get this data on a background thread...,
|
||||
* and then update the property sheet asynchronously
|
||||
*/
|
||||
@NbBundle.Messages({"BlackboardArtifactNode.createSheet.comment.name=C",
|
||||
"BlackboardArtifactNode.createSheet.comment.displayName=C"})
|
||||
@Deprecated
|
||||
protected final void addCommentProperty(Sheet.Set sheetSet, List<Tag> tags, CorrelationAttributeInstance attribute) {
|
||||
HasCommentStatus status = getCommentProperty(tags, attribute );
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR,
|
||||
status));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the comment property for the node
|
||||
*
|
||||
* @param tags the list of tags associated with the file
|
||||
* @param attribute the correlation attribute associated with this
|
||||
* artifact's associated file, null if central repo is not
|
||||
* enabled
|
||||
* @return comment property
|
||||
*/
|
||||
@Override
|
||||
protected DataResultViewerTable.HasCommentStatus getCommentProperty(List<Tag> tags, CorrelationAttributeInstance attribute) {
|
||||
|
||||
HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT;
|
||||
for (Tag tag : tags) {
|
||||
if (!StringUtils.isBlank(tag.getComment())) {
|
||||
@ -609,17 +643,17 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
status = HasCommentStatus.CR_COMMENT;
|
||||
}
|
||||
}
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR,
|
||||
status));
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by (subclasses of) BlackboardArtifactNode to add the Score property
|
||||
* to their sheets.
|
||||
*
|
||||
* @param sheetSet the modifiable Sheet.Set returned by
|
||||
* Sheet.get(Sheet.PROPERTIES)
|
||||
* @param sheetSet the modifiable Sheet.Set to add the property to
|
||||
* @param tags the list of tags associated with the file
|
||||
*
|
||||
* @deprecated Use the GetSCOTask to get this data on a background thread...,
|
||||
* and then update the property sheet asynchronously
|
||||
*/
|
||||
@NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S",
|
||||
"BlackboardArtifactNode.createSheet.score.displayName=S",
|
||||
@ -628,8 +662,22 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
"BlackboardArtifactNode.createSheet.taggedItem.description=Result or associated file has been tagged.",
|
||||
"BlackboardArtifactNode.createSheet.notableTaggedItem.description=Result or associated file tagged with notable tag.",
|
||||
"BlackboardArtifactNode.createSheet.noScore.description=No score"})
|
||||
protected final void addScoreProperty(Sheet.Set sheetSet, List<Tag> tags) {
|
||||
Score score = Score.NO_SCORE;
|
||||
@Deprecated
|
||||
protected final void addScorePropertyAndDescription(Sheet.Set sheetSet, List<Tag> tags) {
|
||||
Pair<DataResultViewerTable.Score, String> scoreAndDescription = getScorePropertyAndDescription(tags);
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoreAndDescription.getRight(), scoreAndDescription.getLeft()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the score property for the node.
|
||||
*
|
||||
* @param tags the list of tags associated with the file
|
||||
*
|
||||
* @return score property and description
|
||||
*/
|
||||
@Override
|
||||
protected Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<Tag> tags) {
|
||||
Score score = Score.NO_SCORE;
|
||||
String description = Bundle.BlackboardArtifactNode_createSheet_noScore_description();
|
||||
if (associated instanceof AbstractFile) {
|
||||
if (((AbstractFile) associated).getKnown() == TskData.FileKnown.BAD) {
|
||||
@ -673,18 +721,43 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
}
|
||||
}
|
||||
}
|
||||
sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), description, score));
|
||||
|
||||
return Pair.of(score, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by (subclasses of) BlackboardArtifactNode to add the Occurrences property
|
||||
* to their sheets.
|
||||
*
|
||||
* @param sheetSet the modifiable Sheet.Set to add the property to
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @deprecated Use the GetSCOTask to get this data on a background thread...,
|
||||
* and then update the property sheet asynchronously
|
||||
*/
|
||||
@NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O",
|
||||
"BlackboardArtifactNode.createSheet.count.displayName=O",
|
||||
"BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated",
|
||||
"BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated",
|
||||
"# {0} - occuranceCount",
|
||||
"BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"})
|
||||
|
||||
@Deprecated
|
||||
protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) {
|
||||
Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting
|
||||
Pair<Long, String> countAndDescription = getCountPropertyAndDescription(attribute);
|
||||
sheetSet.put(
|
||||
new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), countAndDescription.getRight(), countAndDescription.getLeft()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Occurrences property for the node.
|
||||
*
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return count and description
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute) {
|
||||
Long count = -1L;
|
||||
String description = Bundle.BlackboardArtifactNode_createSheet_count_noCentralRepo_description();
|
||||
try {
|
||||
//don't perform the query if there is no correlation value
|
||||
@ -699,14 +772,13 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa
|
||||
} catch (CorrelationAttributeNormalizationException ex) {
|
||||
logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex);
|
||||
}
|
||||
sheetSet.put(
|
||||
new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), description, count));
|
||||
}
|
||||
|
||||
return Pair.of(count, description);
|
||||
}
|
||||
|
||||
private void updateSheet() {
|
||||
this.setSheet(createSheet());
|
||||
}
|
||||
|
||||
|
||||
private String getRootParentName() {
|
||||
String parentName = associated.getName();
|
||||
Content parent = associated;
|
||||
|
@ -1,5 +1,4 @@
|
||||
AbstractAbstractFileNode.accessTimeColLbl=Access Time
|
||||
AbstractAbstractFileNode.addFileProperty.desc=no description
|
||||
AbstractAbstractFileNode.attrAddrColLbl=Attr. Addr.
|
||||
AbstractAbstractFileNode.changeTimeColLbl=Change Time
|
||||
AbstractAbstractFileNode.createdTimeColLbl=Created Time
|
||||
@ -37,6 +36,8 @@ AbstractAbstractFileNode.tagsProperty.displayName=Tags
|
||||
AbstractAbstractFileNode.typeDirColLbl=Type(Dir)
|
||||
AbstractAbstractFileNode.typeMetaColLbl=Type(Meta)
|
||||
AbstractAbstractFileNode.useridColLbl=UserID
|
||||
AbstractContentNode.nodescription=no description
|
||||
AbstractContentNode.valueLoading=value loading
|
||||
AbstractFsContentNode.noDesc.text=no description
|
||||
ArtifactStringContent.attrsTableHeader.sources=Source(s)
|
||||
ArtifactStringContent.attrsTableHeader.type=Type
|
||||
|
@ -36,6 +36,7 @@ import org.sleuthkit.autopsy.actions.ReplaceBlackboardArtifactTagAction;
|
||||
import org.sleuthkit.autopsy.actions.ReplaceContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.datamodel.Reports.ReportNode;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
@ -92,6 +93,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -119,6 +121,7 @@ public class DataModelActionsFactory {
|
||||
actionsList.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, slackFileNode));
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -155,6 +158,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());//
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -189,6 +193,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -223,6 +228,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -257,6 +263,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -291,6 +298,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -325,6 +333,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -379,6 +388,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
@ -415,6 +425,7 @@ public class DataModelActionsFactory {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
if (isArtifactSource) {
|
||||
|
@ -28,6 +28,7 @@ import org.openide.util.Utilities;
|
||||
import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ViewContextAction;
|
||||
@ -89,6 +90,7 @@ public class DirectoryNode extends AbstractFsContentNode<AbstractFile> {
|
||||
actionsList.add(ViewFileInTimelineAction.createViewFileAction(content));
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(new RunIngestModulesAction(content));
|
||||
actionsList.add(null); // creates a menu separator
|
||||
|
@ -32,6 +32,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
@ -173,6 +174,7 @@ public class FileNode extends AbstractFsContentNode<AbstractFile> {
|
||||
actionsList.add(null); // Creates an item separator
|
||||
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // Creates an item separator
|
||||
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
73
Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.datamodel;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
|
||||
import org.sleuthkit.autopsy.core.UserPreferences;
|
||||
import org.sleuthkit.autopsy.events.AutopsyEvent;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
|
||||
/**
|
||||
* Background task to get Score, Comment and Occurrences values for an
|
||||
* Abstract content node.
|
||||
*
|
||||
*/
|
||||
class GetSCOTask implements Runnable {
|
||||
|
||||
private final WeakReference<AbstractContentNode<?>> weakNodeRef;
|
||||
private final PropertyChangeListener listener;
|
||||
|
||||
GetSCOTask(WeakReference<AbstractContentNode<?>> weakContentRef, PropertyChangeListener listener) {
|
||||
this.weakNodeRef = weakContentRef;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
AbstractContentNode<?> contentNode = weakNodeRef.get();
|
||||
|
||||
//Check for stale reference
|
||||
if (contentNode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the SCO column values
|
||||
List<Tag> tags = contentNode.getAllTagsFromDatabase();
|
||||
CorrelationAttributeInstance attribute = contentNode.getCorrelationAttributeInstance();
|
||||
|
||||
SCOData scoData = new SCOData();
|
||||
scoData.setScoreAndDescription(contentNode.getScorePropertyAndDescription(tags));
|
||||
scoData.setComment(contentNode.getCommentProperty(tags, attribute));
|
||||
if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) {
|
||||
scoData.setCountAndDescription(contentNode.getCountPropertyAndDescription(attribute));
|
||||
}
|
||||
|
||||
// signal SCO data is available.
|
||||
if (listener != null) {
|
||||
listener.propertyChange(new PropertyChangeEvent(
|
||||
AutopsyEvent.SourceType.LOCAL.toString(),
|
||||
AbstractAbstractFileNode.NodeSpecificEvents.SCO_AVAILABLE.toString(),
|
||||
null, scoData));
|
||||
}
|
||||
}
|
||||
}
|
@ -28,12 +28,15 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import javax.swing.Action;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openide.nodes.Sheet;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor;
|
||||
import org.sleuthkit.autopsy.directorytree.FileSearchAction;
|
||||
@ -47,6 +50,7 @@ import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery;
|
||||
import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.VirtualDirectory;
|
||||
import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
|
||||
/**
|
||||
* This class is used to represent the "Node" for the image. The children of
|
||||
@ -250,4 +254,67 @@ public class ImageNode extends AbstractContentNode<Image> {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads and returns a list of all tags associated with this content node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @return list of tags associated with the node.
|
||||
*/
|
||||
@Override
|
||||
protected List<Tag> getAllTagsFromDatabase() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
/**
|
||||
* Returns correlation attribute instance for the underlying content of the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @return correlation attribute instance for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected CorrelationAttributeInstance getCorrelationAttributeInstance() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Score property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param tags list of tags.
|
||||
*
|
||||
* @return Score property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<Tag> tags) {
|
||||
return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR);
|
||||
}
|
||||
/**
|
||||
* Returns comment property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param tags list of tags
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return Comment property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected DataResultViewerTable.HasCommentStatus getCommentProperty(List<Tag> tags, CorrelationAttributeInstance attribute) {
|
||||
return DataResultViewerTable.HasCommentStatus.NO_COMMENT;
|
||||
}
|
||||
/**
|
||||
* Returns occurrences/count property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return count property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute) {
|
||||
return Pair.of(-1L, NO_DESCR);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import org.openide.util.Utilities;
|
||||
import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
@ -105,6 +106,7 @@ public class LayoutFileNode extends AbstractAbstractFileNode<LayoutFile> {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
|
@ -31,6 +31,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
@ -82,6 +83,7 @@ public class LocalFileNode extends AbstractAbstractFileNode<AbstractFile> {
|
||||
actionsList.add(null); // creates a menu separator
|
||||
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
|
56
Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.datamodel;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
|
||||
|
||||
/**
|
||||
* Container to bag the S C & O data for an abstract file node.
|
||||
*
|
||||
*/
|
||||
class SCOData {
|
||||
|
||||
private Pair<DataResultViewerTable.Score, String> scoreAndDescription = null;
|
||||
private DataResultViewerTable.HasCommentStatus comment = null;
|
||||
private Pair<Long, String> countAndDescription = null;
|
||||
|
||||
Pair<DataResultViewerTable.Score, String> getScoreAndDescription() {
|
||||
return scoreAndDescription;
|
||||
}
|
||||
|
||||
DataResultViewerTable.HasCommentStatus getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
Pair<Long, String> getCountAndDescription() {
|
||||
return countAndDescription;
|
||||
}
|
||||
|
||||
void setScoreAndDescription(Pair<DataResultViewerTable.Score, String> scoreAndDescription) {
|
||||
this.scoreAndDescription = scoreAndDescription;
|
||||
}
|
||||
void setComment(DataResultViewerTable.HasCommentStatus comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
void setCountAndDescription(Pair<Long, String> countAndDescription) {
|
||||
this.countAndDescription = countAndDescription;
|
||||
}
|
||||
|
||||
}
|
@ -28,6 +28,7 @@ import org.openide.util.Utilities;
|
||||
import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ViewContextAction;
|
||||
@ -85,6 +86,7 @@ public class SlackFileNode extends AbstractFsContentNode<AbstractFile> {
|
||||
NbBundle.getMessage(this.getClass(), "SlackFileNode.getActions.viewInNewWin.text"), this));
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
|
@ -25,6 +25,7 @@ import javax.swing.Action;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
import org.sleuthkit.autopsy.directorytree.FileSearchAction;
|
||||
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
|
||||
@ -61,6 +62,7 @@ public abstract class SpecialDirectoryNode extends AbstractAbstractFileNode<Spec
|
||||
Bundle.SpecialDirectoryNode_getActions_viewInNewWin_text(), this));
|
||||
actions.add(null); // creates a menu separator
|
||||
actions.add(ExtractAction.getInstance());
|
||||
actions.add(ExportCSVAction.getInstance());
|
||||
actions.add(null); // creates a menu separator
|
||||
actions.add(new FileSearchAction(Bundle.ImageNode_getActions_openFileSearchByAttr_text()));
|
||||
if (content.isDataSource()) {
|
||||
|
@ -25,10 +25,14 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import javax.swing.Action;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.openide.nodes.Sheet;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
|
||||
import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.NO_DESCR;
|
||||
import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException;
|
||||
import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor;
|
||||
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
|
||||
@ -39,6 +43,7 @@ import org.sleuthkit.datamodel.TskCoreException;
|
||||
import org.sleuthkit.datamodel.VirtualDirectory;
|
||||
import org.sleuthkit.datamodel.Volume;
|
||||
import org.sleuthkit.autopsy.directorytree.FileSystemDetailsAction;
|
||||
import org.sleuthkit.datamodel.Tag;
|
||||
|
||||
/**
|
||||
* This class is used to represent the "Node" for the volume. Its child is the
|
||||
@ -212,4 +217,67 @@ public class VolumeNode extends AbstractContentNode<Volume> {
|
||||
public String getItemType() {
|
||||
return DisplayableItemNode.FILE_PARENT_NODE_KEY;
|
||||
}
|
||||
/**
|
||||
* Reads and returns a list of all tags associated with this content node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @return list of tags associated with the node.
|
||||
*/
|
||||
@Override
|
||||
protected List<Tag> getAllTagsFromDatabase() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
/**
|
||||
* Returns correlation attribute instance for the underlying content of the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @return correlation attribute instance for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected CorrelationAttributeInstance getCorrelationAttributeInstance() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Score property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param tags list of tags.
|
||||
*
|
||||
* @return Score property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected Pair<DataResultViewerTable.Score, String> getScorePropertyAndDescription(List<Tag> tags) {
|
||||
return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR);
|
||||
}
|
||||
/**
|
||||
* Returns comment property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param tags list of tags
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return Comment property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected DataResultViewerTable.HasCommentStatus getCommentProperty(List<Tag> tags, CorrelationAttributeInstance attribute) {
|
||||
return DataResultViewerTable.HasCommentStatus.NO_COMMENT;
|
||||
}
|
||||
/**
|
||||
* Returns occurrences/count property for the node.
|
||||
*
|
||||
* Null implementation of an abstract method.
|
||||
*
|
||||
* @param attribute correlation attribute instance
|
||||
*
|
||||
* @return count property for the underlying content of the node.
|
||||
*/
|
||||
@Override
|
||||
protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attribute) {
|
||||
return Pair.of(-1L, NO_DESCR);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,8 @@
|
||||
CSVWriter.done.notifyMsg.error=Error exporting to CSV file
|
||||
# {0} - Output file
|
||||
CSVWriter.done.notifyMsg.success=Wrote to {0}
|
||||
CSVWriter.progress.cancelling=Cancelling
|
||||
CSVWriter.progress.extracting=Exporting to CSV file
|
||||
CTL_DirectoryTreeTopComponent=Directory Tree
|
||||
DataResultFilterNode.viewSourceArtifact.text=View Source Result
|
||||
# {0} - dataSourceCount
|
||||
@ -5,6 +10,11 @@ DirectoryTreeTopComponent.componentOpened.groupDataSources.text=This case contai
|
||||
DirectoryTreeTopComponent.componentOpened.groupDataSources.title=Group by data source?
|
||||
DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module.
|
||||
DirectoryTreeTopComponent.resultsView.title=Listing
|
||||
ExportCSV.saveNodesToCSV.empty=No data to export
|
||||
# {0} - Output file
|
||||
ExportCSV.saveNodesToCSV.fileExists=File {0} already exists
|
||||
ExportCSV.saveNodesToCSV.noCurrentCase=No open case available
|
||||
ExportCSV.title.text=Export selected rows to CSV
|
||||
ExternalViewerAction.actionPerformed.failure.exe.message=The file is an executable and will not be opened.
|
||||
ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch.
|
||||
ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists.
|
||||
|
@ -372,6 +372,7 @@ public class DataResultFilterNode extends FilterNode {
|
||||
}
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
actionsList.add(AddBlackboardArtifactTagAction.getInstance());
|
||||
|
@ -125,6 +125,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default<List<? ext
|
||||
}
|
||||
}
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.addAll(ContextMenuExtensionPoint.getActions());
|
||||
return actionsList;
|
||||
}
|
||||
@ -142,6 +143,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default<List<? ext
|
||||
}
|
||||
}
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.addAll(ContextMenuExtensionPoint.getActions());
|
||||
return actionsList;
|
||||
}
|
||||
@ -150,6 +152,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default<List<? ext
|
||||
public List<? extends Action> visit(final DerivedFile d) {
|
||||
List<Action> actionsList = new ArrayList<>();
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
final Collection<AbstractFile> selectedFilesList =
|
||||
@ -166,6 +169,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default<List<? ext
|
||||
public List<? extends Action> visit(final LocalFile d) {
|
||||
List<Action> actionsList = new ArrayList<>();
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
final Collection<AbstractFile> selectedFilesList =
|
||||
@ -182,6 +186,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default<List<? ext
|
||||
public List<? extends Action> visit(final org.sleuthkit.datamodel.File d) {
|
||||
List<Action> actionsList = new ArrayList<>();
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
final Collection<AbstractFile> selectedFilesList =
|
||||
|
@ -0,0 +1,360 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this content 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.directorytree;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.File;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.logging.Level;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.SwingWorker;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import org.netbeans.api.progress.ProgressHandle;
|
||||
import org.openide.util.Cancellable;
|
||||
import org.openide.util.NbBundle;
|
||||
import org.openide.util.Utilities;
|
||||
import org.sleuthkit.autopsy.casemodule.Case;
|
||||
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
|
||||
import org.sleuthkit.autopsy.coreutils.Logger;
|
||||
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
|
||||
import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType;
|
||||
import org.openide.nodes.Node;
|
||||
import org.openide.nodes.Node.PropertySet;
|
||||
import org.openide.nodes.Node.Property;
|
||||
|
||||
/**
|
||||
* Exports CSV version of result nodes to a location selected by the user.
|
||||
*/
|
||||
public final class ExportCSVAction extends AbstractAction {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ExportCSVAction.class.getName());
|
||||
private final static String DEFAULT_FILENAME = "Results";
|
||||
private final static List<String> columnsToSkip = Arrays.asList(AbstractFilePropertyType.SCORE.toString(),
|
||||
AbstractFilePropertyType.COMMENT.toString(), AbstractFilePropertyType.OCCURRENCES.toString());
|
||||
|
||||
private static String userDefinedExportPath;
|
||||
|
||||
// This class is a singleton to support multi-selection of nodes, since
|
||||
// org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every
|
||||
// node in the array returns a reference to the same action object from Node.getActions(boolean).
|
||||
private static ExportCSVAction instance;
|
||||
|
||||
/**
|
||||
* Get an instance of the Action. See above for why
|
||||
* the class is a singleton.
|
||||
*
|
||||
* @return the instance
|
||||
*/
|
||||
public static synchronized ExportCSVAction getInstance() {
|
||||
if (null == instance) {
|
||||
instance = new ExportCSVAction();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the action.
|
||||
*/
|
||||
@NbBundle.Messages({"ExportCSV.title.text=Export selected rows to CSV"})
|
||||
private ExportCSVAction() {
|
||||
super(Bundle.ExportCSV_title_text());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks user to choose destination, then extracts content to destination
|
||||
* (recursing on directories).
|
||||
*
|
||||
* @param e The action event.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Collection<? extends Node> selectedNodes = Utilities.actionsGlobalContext().lookupAll(Node.class);
|
||||
saveNodesToCSV(selectedNodes, (Component)e.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the selected nodes to a CSV file
|
||||
*
|
||||
* @param nodesToExport the nodes to save
|
||||
* @param component
|
||||
*/
|
||||
@NbBundle.Messages({
|
||||
"# {0} - Output file",
|
||||
"ExportCSV.saveNodesToCSV.fileExists=File {0} already exists",
|
||||
"ExportCSV.saveNodesToCSV.noCurrentCase=No open case available",
|
||||
"ExportCSV.saveNodesToCSV.empty=No data to export"})
|
||||
public static void saveNodesToCSV(Collection<? extends Node> nodesToExport, Component component) {
|
||||
|
||||
if (nodesToExport.isEmpty()) {
|
||||
MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_empty());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Set up the file chooser with a default name and either the Export
|
||||
// folder or the last used folder.
|
||||
String fileName = getDefaultOutputFileName(nodesToExport.iterator().next().getParentNode());
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setCurrentDirectory(new File(getExportDirectory(Case.getCurrentCaseThrows())));
|
||||
fileChooser.setSelectedFile(new File(fileName));
|
||||
fileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv"));
|
||||
|
||||
int returnVal = fileChooser.showSaveDialog(component);
|
||||
if (returnVal == JFileChooser.APPROVE_OPTION) {
|
||||
|
||||
// Get the file name, appending .csv if necessary
|
||||
File selectedFile = fileChooser.getSelectedFile();
|
||||
if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS
|
||||
selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS
|
||||
}
|
||||
|
||||
// Save the directory used for next time
|
||||
updateExportDirectory(selectedFile.getParent(), Case.getCurrentCaseThrows());
|
||||
|
||||
if (selectedFile.exists()) {
|
||||
logger.log(Level.SEVERE, "File {0} already exists", selectedFile.getAbsolutePath()); //NON-NLS
|
||||
MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_fileExists(selectedFile));
|
||||
return;
|
||||
}
|
||||
|
||||
CSVWriter writer = new CSVWriter(nodesToExport, selectedFile);
|
||||
writer.execute();
|
||||
}
|
||||
} catch (NoCurrentCaseException ex) {
|
||||
JOptionPane.showMessageDialog(component, Bundle.ExportCSV_saveNodesToCSV_noCurrentCase());
|
||||
logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default name for the CSV output.
|
||||
*
|
||||
* @param parent The parent node for the selected nodes
|
||||
*
|
||||
* @return the default name
|
||||
*/
|
||||
private static String getDefaultOutputFileName(Node parent) {
|
||||
String dateStr = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS", Calendar.getInstance());
|
||||
|
||||
if (parent != null) {
|
||||
// The first value in the property set is generally a reasonable name
|
||||
for (PropertySet set : parent.getPropertySets()) {
|
||||
for (Property<?> prop : set.getProperties()) {
|
||||
try {
|
||||
String parentName = prop.getValue().toString();
|
||||
|
||||
// Strip off the count (if present)
|
||||
parentName = parentName.replaceAll("\\([0-9]+\\)$", "");
|
||||
|
||||
// Strip out any invalid characters
|
||||
parentName = parentName.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
|
||||
return parentName + " " + dateStr;
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
logger.log(Level.WARNING, "Failed to get property set value as string", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return DEFAULT_FILENAME + " " + dateStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the export directory path.
|
||||
*
|
||||
* @param openCase The current case.
|
||||
*
|
||||
* @return The export directory path.
|
||||
*/
|
||||
private static String getExportDirectory(Case openCase) {
|
||||
String caseExportPath = openCase.getExportDirectory();
|
||||
|
||||
if (userDefinedExportPath == null) {
|
||||
return caseExportPath;
|
||||
}
|
||||
|
||||
File file = new File(userDefinedExportPath);
|
||||
if (file.exists() == false || file.isDirectory() == false) {
|
||||
return caseExportPath;
|
||||
}
|
||||
|
||||
return userDefinedExportPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the default export directory. If the directory path matches the
|
||||
* case export directory, then the directory used will always match the
|
||||
* export directory of any given case. Otherwise, the path last used will be
|
||||
* saved.
|
||||
*
|
||||
* @param exportPath The export path.
|
||||
* @param openCase The current case.
|
||||
*/
|
||||
private static void updateExportDirectory(String exportPath, Case openCase) {
|
||||
if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) {
|
||||
userDefinedExportPath = null;
|
||||
} else {
|
||||
userDefinedExportPath = exportPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Thread that does the actual extraction work
|
||||
*/
|
||||
private static class CSVWriter extends SwingWorker<Object, Void> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(CSVWriter.class.getName());
|
||||
private ProgressHandle progress;
|
||||
|
||||
private final Collection<? extends Node> nodesToExport;
|
||||
private final File outputFile;
|
||||
|
||||
/**
|
||||
* Create an instance of the CSVWriter.
|
||||
*
|
||||
* @param extractionTasks List of file extraction tasks.
|
||||
*/
|
||||
CSVWriter(Collection<? extends Node> nodesToExport, File outputFile) {
|
||||
this.nodesToExport = nodesToExport;
|
||||
this.outputFile = outputFile;
|
||||
}
|
||||
|
||||
@NbBundle.Messages({"CSVWriter.progress.extracting=Exporting to CSV file",
|
||||
"CSVWriter.progress.cancelling=Cancelling"})
|
||||
@Override
|
||||
protected Object doInBackground() throws Exception {
|
||||
if (nodesToExport.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set up progress bar.
|
||||
final String displayName = Bundle.CSVWriter_progress_extracting();
|
||||
progress = ProgressHandle.createHandle(displayName, new Cancellable() {
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
if (progress != null) {
|
||||
progress.setDisplayName(Bundle.CSVWriter_progress_cancelling());
|
||||
}
|
||||
return ExportCSVAction.CSVWriter.this.cancel(true);
|
||||
}
|
||||
});
|
||||
progress.start();
|
||||
progress.switchToIndeterminate();
|
||||
|
||||
try (BufferedWriter br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) {
|
||||
// Write BOM
|
||||
br.write('\ufeff');
|
||||
|
||||
// Write the header
|
||||
List<String> headers = new ArrayList<>();
|
||||
PropertySet[] sets = nodesToExport.iterator().next().getPropertySets();
|
||||
for(PropertySet set : sets) {
|
||||
for (Property<?> prop : set.getProperties()) {
|
||||
if ( ! columnsToSkip.contains(prop.getDisplayName())) {
|
||||
headers.add(prop.getDisplayName());
|
||||
}
|
||||
}
|
||||
}
|
||||
br.write(listToCSV(headers));
|
||||
|
||||
// Write each line
|
||||
Iterator<?> nodeIterator = nodesToExport.iterator();
|
||||
while (nodeIterator.hasNext()) {
|
||||
if (this.isCancelled()) {
|
||||
break;
|
||||
}
|
||||
|
||||
Node node = (Node)nodeIterator.next();
|
||||
List<String> values = new ArrayList<>();
|
||||
sets = node.getPropertySets();
|
||||
for(PropertySet set : sets) {
|
||||
for (Property<?> prop : set.getProperties()) {
|
||||
if ( ! columnsToSkip.contains(prop.getDisplayName())) {
|
||||
values.add(escapeQuotes(prop.getValue().toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
br.write(listToCSV(values));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape any quotes in the string
|
||||
*
|
||||
* @param original
|
||||
*
|
||||
* @return the string with quotes escaped
|
||||
*/
|
||||
private String escapeQuotes(String original) {
|
||||
return original.replaceAll("\"", "\\\\\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert list of values to a comma separated string.
|
||||
*
|
||||
* @param values Values to convert
|
||||
*
|
||||
* @return values as CSV
|
||||
*/
|
||||
private String listToCSV(List<String> values) {
|
||||
return "\"" + String.join("\",\"", values) + "\"\n";
|
||||
}
|
||||
|
||||
@NbBundle.Messages({"CSVWriter.done.notifyMsg.error=Error exporting to CSV file",
|
||||
"# {0} - Output file",
|
||||
"CSVWriter.done.notifyMsg.success=Wrote to {0}"})
|
||||
@Override
|
||||
protected void done() {
|
||||
boolean msgDisplayed = false;
|
||||
try {
|
||||
super.get();
|
||||
} catch (InterruptedException | ExecutionException ex) {
|
||||
logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS
|
||||
MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_error());
|
||||
msgDisplayed = true;
|
||||
} catch (java.util.concurrent.CancellationException ex) {
|
||||
// catch and ignore if we were cancelled
|
||||
} finally {
|
||||
progress.finish();
|
||||
if (!this.isCancelled() && !msgDisplayed) {
|
||||
MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_success(outputFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -278,6 +278,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp
|
||||
//enable the new button
|
||||
FilesSetDefsPanel.this.newSetButton.setEnabled(canBeEnabled);
|
||||
FilesSetDefsPanel.this.importSetButton.setEnabled(canBeEnabled);
|
||||
|
||||
// Get the selected interesting files set and populate the set
|
||||
// components.
|
||||
FilesSet selectedSet = FilesSetDefsPanel.this.setsList.getSelectedValue();
|
||||
@ -302,6 +303,12 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp
|
||||
if (!FilesSetDefsPanel.this.rulesListModel.isEmpty()) {
|
||||
FilesSetDefsPanel.this.rulesList.setSelectedIndex(0);
|
||||
}
|
||||
} else {
|
||||
// Disable the edit, delete, copy, and export buttons
|
||||
FilesSetDefsPanel.this.editSetButton.setEnabled(false);
|
||||
FilesSetDefsPanel.this.deleteSetButton.setEnabled(false);
|
||||
FilesSetDefsPanel.this.copySetButton.setEnabled(false);
|
||||
FilesSetDefsPanel.this.exportSetButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@ -63,9 +64,12 @@ class FileReportText implements FileReportModule {
|
||||
public void startReport(String baseReportDir) {
|
||||
this.reportPath = baseReportDir + FILE_NAME;
|
||||
try {
|
||||
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath)));
|
||||
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath), StandardCharsets.UTF_8));
|
||||
out.write('\ufeff');
|
||||
} catch (FileNotFoundException ex) {
|
||||
logger.log(Level.WARNING, "Failed to create report text file", ex); //NON-NLS
|
||||
} catch (IOException ex) {
|
||||
logger.log(Level.WARNING, "Failed to write BOM to report text file", ex); //NON-NLS
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,4 +120,13 @@ public final class TextTranslationService {
|
||||
public boolean hasProvider() {
|
||||
return selectedTranslator.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hard limit for translation request sizes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getMaxPayloadSize() {
|
||||
return selectedTranslator.get().getMaxPayloadSize();
|
||||
}
|
||||
}
|
||||
|
@ -56,4 +56,11 @@ public interface TextTranslator {
|
||||
* Save the settings as they have been modified in the component.
|
||||
*/
|
||||
void saveSettings();
|
||||
|
||||
/**
|
||||
* Returns the hard limit for translation request sizes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int getMaxPayloadSize();
|
||||
}
|
||||
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.texttranslation.translators;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
import java.awt.Component;
|
||||
import java.io.IOException;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.openide.util.lookup.ServiceProvider;
|
||||
import org.sleuthkit.autopsy.texttranslation.TextTranslator;
|
||||
import org.sleuthkit.autopsy.texttranslation.TranslationException;
|
||||
|
||||
/**
|
||||
* Translates text by making HTTP requests to Bing Translator. This requires a
|
||||
* valid subscription key for a Microsoft Azure account.
|
||||
*/
|
||||
@ServiceProvider(service = TextTranslator.class)
|
||||
public class BingTranslator implements TextTranslator {
|
||||
|
||||
//The target language follows the to= in the string below. You can include multiple target
|
||||
//languages separated by commas. A full list of supported languages is here:
|
||||
//https://docs.microsoft.com/en-us/azure/cognitive-services/translator/language-support
|
||||
private static final String BASE_URL = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=";
|
||||
private static final int MAX_STRING_LENGTH = 5000;
|
||||
private final BingTranslatorSettingsPanel settingsPanel;
|
||||
private final BingTranslatorSettings settings = new BingTranslatorSettings();
|
||||
// This sends messages to Microsoft.
|
||||
private final OkHttpClient CLIENT = new OkHttpClient();
|
||||
|
||||
/**
|
||||
* Create a Bing Translator
|
||||
*/
|
||||
public BingTranslator() {
|
||||
settingsPanel = new BingTranslatorSettingsPanel(settings.getAuthenticationKey(), settings.getTargetLanguageCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tranlationurl for the specified language code
|
||||
*
|
||||
*
|
||||
*
|
||||
* @param languageCode language code for language to translate to
|
||||
*
|
||||
* @return a string representation of the url to request translation from
|
||||
*/
|
||||
static String getTranlatorUrl(String languageCode) {
|
||||
return BASE_URL + languageCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an input text to the JSON format required by Bing Translator,
|
||||
* posts it to Microsoft, and returns the JSON text response.
|
||||
*
|
||||
* @param string The input text to be translated.
|
||||
*
|
||||
* @return The translation response as a JSON string
|
||||
*
|
||||
* @throws IOException if the request could not be executed due to
|
||||
* cancellation, a connectivity problem or timeout.
|
||||
*/
|
||||
public String postTranslationRequest(String string) throws IOException {
|
||||
MediaType mediaType = MediaType.parse("application/json");
|
||||
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("Text", string);
|
||||
jsonArray.add(jsonObject);
|
||||
String bodyString = jsonArray.toString();
|
||||
|
||||
RequestBody body = RequestBody.create(mediaType,
|
||||
bodyString);
|
||||
Request request = new Request.Builder()
|
||||
.url(getTranlatorUrl(settings.getTargetLanguageCode())).post(body)
|
||||
.addHeader("Ocp-Apim-Subscription-Key", settings.getAuthenticationKey())
|
||||
.addHeader("Content-type", "application/json").build();
|
||||
Response response = CLIENT.newCall(request).execute();
|
||||
return response.body().string();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translate(String string) throws TranslationException {
|
||||
if (settings.getAuthenticationKey() == null || settings.getAuthenticationKey().isEmpty()) {
|
||||
throw new TranslationException("Bing Translator has not been configured, authentication key needs to be specified");
|
||||
}
|
||||
String toTranslate = string.trim();
|
||||
//Translates some text into English, without specifying the source langauge.
|
||||
|
||||
//Google Translate required us to replace (\r\n|\n) with <br />
|
||||
//but Bing Translator doesn not have that requirement.
|
||||
//The free account has a maximum file size. If you have a paid account,
|
||||
//you probably still want to limit file size to prevent accidentally
|
||||
//translating very large documents.
|
||||
if (toTranslate.length() > MAX_STRING_LENGTH) {
|
||||
toTranslate = toTranslate.substring(0, MAX_STRING_LENGTH);
|
||||
}
|
||||
|
||||
try {
|
||||
String response = postTranslationRequest(toTranslate);
|
||||
return parseJSONResponse(response);
|
||||
} catch (IOException | TranslationException ex) {
|
||||
throw new TranslationException("Exception while attempting to translate using BingTranslator", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Messages({"BingTranslator.name.text=Bing Translator"})
|
||||
@Override
|
||||
public String getName() {
|
||||
return Bundle.BingTranslator_name_text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getComponent() {
|
||||
return settingsPanel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSettings() {
|
||||
settings.setAuthenticationKey(settingsPanel.getAuthenticationKey());
|
||||
settings.setTargetLanguageCode(settingsPanel.getTargetLanguageCode());
|
||||
settings.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response to get the translated text
|
||||
*
|
||||
* @param json_text the json which was received as a response to a
|
||||
* translation request
|
||||
*
|
||||
* @return the translated text
|
||||
*
|
||||
* @throws TranslationException
|
||||
*/
|
||||
private String parseJSONResponse(String json_text) throws TranslationException {
|
||||
/*
|
||||
* Here is an example of the text we get from Bing when input is "gato",
|
||||
* the Spanish word for cat: [ { "detectedLanguage": { "language": "es",
|
||||
* "score": 1.0 }, "translations": [ { "text": "cat", "to": "en" } ] } ]
|
||||
*/
|
||||
JsonParser parser = new JsonParser();
|
||||
try {
|
||||
JsonArray responses = parser.parse(json_text).getAsJsonArray();
|
||||
//As far as I know, there's always exactly one item in the array.
|
||||
JsonObject response0 = responses.get(0).getAsJsonObject();
|
||||
JsonArray translations = response0.getAsJsonArray("translations");
|
||||
JsonObject translation0 = translations.get(0).getAsJsonObject();
|
||||
return translation0.get("text").getAsString();
|
||||
} catch (IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException e) {
|
||||
throw new TranslationException("JSON text does not match Bing Translator scheme: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPayloadSize() {
|
||||
return MAX_STRING_LENGTH;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Autopsy
|
||||
*
|
||||
* Copyright 2019 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.texttranslation.translators;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
|
||||
|
||||
/**
|
||||
* Class to handle the settings associated with the BingTranslator
|
||||
*/
|
||||
public final class BingTranslatorSettings {
|
||||
|
||||
private static final String AUTHENTICATION_KEY = "Credentials";
|
||||
private static final String BING_TRANSLATE_NAME = "BingTranslate";
|
||||
private static final String DEFAULT_AUTHENTICATION = "";
|
||||
private static final String DEFAULT_TARGET_LANGUAGE = "en";
|
||||
private static final String TARGET_LANGUAGE_CODE_KEY = "TargetLanguageCode";
|
||||
private String authenticationKey;
|
||||
private String targetLanguageCode;
|
||||
|
||||
/**
|
||||
* Construct a new BingTranslatorSettings object
|
||||
*/
|
||||
BingTranslatorSettings() {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Authentication key to be used for the Microsoft translation
|
||||
* service
|
||||
*
|
||||
* @return the Authentication key for the service
|
||||
*/
|
||||
String getAuthenticationKey() {
|
||||
return authenticationKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Authentication key to be used for the Microsoft translation
|
||||
* service
|
||||
*
|
||||
* @param authKey the Authentication key for the service
|
||||
*/
|
||||
void setAuthenticationKey(String authKey) {
|
||||
authenticationKey = authKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the settings into memory from their on disk storage
|
||||
*/
|
||||
void loadSettings() {
|
||||
if (!ModuleSettings.configExists(BING_TRANSLATE_NAME)) {
|
||||
ModuleSettings.makeConfigFile(BING_TRANSLATE_NAME);
|
||||
}
|
||||
if (ModuleSettings.settingExists(BING_TRANSLATE_NAME, AUTHENTICATION_KEY)) {
|
||||
authenticationKey = ModuleSettings.getConfigSetting(BING_TRANSLATE_NAME, AUTHENTICATION_KEY);
|
||||
}
|
||||
if (authenticationKey == null || StringUtils.isBlank(authenticationKey)) {
|
||||
authenticationKey = DEFAULT_AUTHENTICATION;
|
||||
}
|
||||
if (ModuleSettings.settingExists(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY)) {
|
||||
targetLanguageCode = ModuleSettings.getConfigSetting(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY);
|
||||
}
|
||||
if (targetLanguageCode == null || StringUtils.isBlank(targetLanguageCode)) {
|
||||
targetLanguageCode = DEFAULT_TARGET_LANGUAGE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target language code
|
||||
*
|
||||
* @return the code used to identify the target language
|
||||
*/
|
||||
String getTargetLanguageCode() {
|
||||
return targetLanguageCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target language code. If a blank code is specified it sets the
|
||||
* default code instead.
|
||||
*
|
||||
* @param code the target language code to set
|
||||
*/
|
||||
void setTargetLanguageCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
targetLanguageCode = DEFAULT_TARGET_LANGUAGE;
|
||||
} else {
|
||||
targetLanguageCode = code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the setting from memory to their location on disk
|
||||
*/
|
||||
void saveSettings() {
|
||||
ModuleSettings.setConfigSetting(BING_TRANSLATE_NAME, AUTHENTICATION_KEY, authenticationKey);
|
||||
ModuleSettings.setConfigSetting(BING_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY, targetLanguageCode);
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
|
||||
<AuxValues>
|
||||
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
|
||||
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
|
||||
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
|
||||
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
|
||||
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
|
||||
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout>
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="instructionsScrollPane" max="32767" attributes="0"/>
|
||||
<Group type="102" attributes="0">
|
||||
<Group type="103" groupAlignment="0" max="-2" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="authenticationKeyLabel" min="-2" pref="100" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="authenticationKeyField" min="-2" pref="486" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="warningLabel" min="-2" pref="551" max="-2" attributes="0"/>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="targetLanguageLabel" alignment="0" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="testButton" alignment="0" min="-2" pref="79" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace type="separate" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="untranslatedLabel" min="-2" pref="66" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="testUntranslatedTextField" min="-2" pref="140" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="resultLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="testResultValueLabel" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<Component id="targetLanguageComboBox" max="32767" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="276" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="instructionsScrollPane" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="authenticationKeyField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="authenticationKeyLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="targetLanguageLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="targetLanguageComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="testButton" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="testUntranslatedTextField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="untranslatedLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="resultLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="testResultValueLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="warningLabel" min="-2" pref="18" max="-2" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
</Layout>
|
||||
<SubComponents>
|
||||
<Component class="javax.swing.JTextField" name="authenticationKeyField">
|
||||
<Properties>
|
||||
<Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.authenticationKeyField.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="warningLabel">
|
||||
<Properties>
|
||||
<Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
|
||||
<Color blue="0" green="0" red="ff" type="rgb"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.warningLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="testButton">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.testButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="testButtonActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="targetLanguageLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.targetLanguageLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JComboBox" name="targetLanguageComboBox">
|
||||
<Properties>
|
||||
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
|
||||
<StringArray count="0"/>
|
||||
</Property>
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="itemStateChanged" listener="java.awt.event.ItemListener" parameters="java.awt.event.ItemEvent" handler="targetLanguageComboBoxSelected"/>
|
||||
</Events>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<LanguageWrapper>"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Component class="javax.swing.JTextField" name="testUntranslatedTextField">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
|
||||
<Connection code="DEFUALT_TEST_STRING" type="code"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="untranslatedLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.untranslatedLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="resultLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.resultLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="testResultValueLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.testResultValueLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="authenticationKeyLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.authenticationKeyLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Container class="javax.swing.JScrollPane" name="instructionsScrollPane">
|
||||
<Properties>
|
||||
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
|
||||
<Border info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo">
|
||||
<EtchetBorder/>
|
||||
</Border>
|
||||
</Property>
|
||||
<Property name="horizontalScrollBarPolicy" type="int" value="31"/>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
|
||||
<SubComponents>
|
||||
<Component class="javax.swing.JTextArea" name="instructionsTextArea">
|
||||
<Properties>
|
||||
<Property name="editable" type="boolean" value="false"/>
|
||||
<Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
|
||||
<Color blue="f0" green="f0" red="f0" type="rgb"/>
|
||||
</Property>
|
||||
<Property name="columns" type="int" value="20"/>
|
||||
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">
|
||||
<Font name="Tahoma" size="11" style="0"/>
|
||||
</Property>
|
||||
<Property name="lineWrap" type="boolean" value="true"/>
|
||||
<Property name="rows" type="int" value="2"/>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="BingTranslatorSettingsPanel.instructionsTextArea.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="wrapStyleWord" type="boolean" value="true"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Container>
|
||||
</SubComponents>
|
||||
</Form>
|
@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Autopsy
|
||||
*
|
||||
* Copyright 2019 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.texttranslation.translators;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.squareup.okhttp.MediaType;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
|
||||
/**
|
||||
* Settings panel for the BingTranslator
|
||||
*/
|
||||
public class BingTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(BingTranslatorSettingsPanel.class.getName());
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String GET_TARGET_LANGUAGES_URL = "https://api.cognitive.microsofttranslator.com/languages?api-version=3.0&scope=translation";
|
||||
private static final String DEFUALT_TEST_STRING = "traducción exitoso"; //spanish which should translate to something along the lines of "successful translation"
|
||||
private String targetLanguageCode = "";
|
||||
|
||||
/**
|
||||
* Creates new form BingTranslatorSettingsPanel
|
||||
*/
|
||||
public BingTranslatorSettingsPanel(String authenticationKey, String code) {
|
||||
initComponents();
|
||||
authenticationKeyField.setText(authenticationKey);
|
||||
authenticationKeyField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
firePropertyChange("SettingChanged", true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
firePropertyChange("SettingChanged", true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
firePropertyChange("SettingChanged", true, false);
|
||||
}
|
||||
|
||||
});
|
||||
populateComboBox();
|
||||
selectLanguageByCode(code);
|
||||
targetLanguageCode = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the target language combo box with available target languages
|
||||
*/
|
||||
@Messages({"BingTranslatorSettingsPanel.warning.targetLanguageFailure=Unable to get list of target languages or parse the result that was received"})
|
||||
private void populateComboBox() {
|
||||
Request get_request = new Request.Builder()
|
||||
.url(GET_TARGET_LANGUAGES_URL).build();
|
||||
try {
|
||||
Response response = new OkHttpClient().newCall(get_request).execute();
|
||||
JsonParser parser = new JsonParser();
|
||||
String responseBody = response.body().string();
|
||||
JsonElement elementBody = parser.parse(responseBody);
|
||||
JsonObject asObject = elementBody.getAsJsonObject();
|
||||
JsonElement translationElement = asObject.get("translation");
|
||||
JsonObject responses = translationElement.getAsJsonObject();
|
||||
responses.entrySet().forEach((entry) -> {
|
||||
targetLanguageComboBox.addItem(new LanguageWrapper(entry.getKey(), entry.getValue().getAsJsonObject().get("name").getAsString()));
|
||||
});
|
||||
targetLanguageComboBox.setEnabled(true);
|
||||
} catch (IOException | IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException ex) {
|
||||
logger.log(Level.SEVERE, Bundle.BingTranslatorSettingsPanel_warning_targetLanguageFailure(), ex);
|
||||
warningLabel.setText(Bundle.BingTranslatorSettingsPanel_warning_targetLanguageFailure());
|
||||
targetLanguageComboBox.setEnabled(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a language code select the corresponding language in the combo box
|
||||
* if it is present
|
||||
*
|
||||
* @param code language code such as "en" for English
|
||||
*/
|
||||
private void selectLanguageByCode(String code) {
|
||||
for (int i = 0; i < targetLanguageComboBox.getModel().getSize(); i++) {
|
||||
if (targetLanguageComboBox.getModel().getElementAt(i).getLanguageCode().equals(code)) {
|
||||
targetLanguageComboBox.setSelectedIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from within the constructor to initialize the form.
|
||||
* WARNING: Do NOT modify this code. The content of this method is always
|
||||
* regenerated by the Form Editor.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
|
||||
private void initComponents() {
|
||||
|
||||
authenticationKeyField = new javax.swing.JTextField();
|
||||
warningLabel = new javax.swing.JLabel();
|
||||
testButton = new javax.swing.JButton();
|
||||
targetLanguageLabel = new javax.swing.JLabel();
|
||||
targetLanguageComboBox = new javax.swing.JComboBox<>();
|
||||
testUntranslatedTextField = new javax.swing.JTextField();
|
||||
untranslatedLabel = new javax.swing.JLabel();
|
||||
resultLabel = new javax.swing.JLabel();
|
||||
testResultValueLabel = new javax.swing.JLabel();
|
||||
authenticationKeyLabel = new javax.swing.JLabel();
|
||||
instructionsScrollPane = new javax.swing.JScrollPane();
|
||||
instructionsTextArea = new javax.swing.JTextArea();
|
||||
|
||||
authenticationKeyField.setToolTipText(org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.authenticationKeyField.toolTipText")); // NOI18N
|
||||
|
||||
warningLabel.setForeground(new java.awt.Color(255, 0, 0));
|
||||
org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.warningLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(testButton, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.testButton.text")); // NOI18N
|
||||
testButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
testButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(targetLanguageLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.targetLanguageLabel.text")); // NOI18N
|
||||
|
||||
targetLanguageComboBox.setEnabled(false);
|
||||
targetLanguageComboBox.addItemListener(new java.awt.event.ItemListener() {
|
||||
public void itemStateChanged(java.awt.event.ItemEvent evt) {
|
||||
targetLanguageComboBoxSelected(evt);
|
||||
}
|
||||
});
|
||||
|
||||
testUntranslatedTextField.setText(DEFUALT_TEST_STRING);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(untranslatedLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.untranslatedLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(resultLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.resultLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(testResultValueLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.testResultValueLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(authenticationKeyLabel, org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.authenticationKeyLabel.text")); // NOI18N
|
||||
|
||||
instructionsScrollPane.setBorder(javax.swing.BorderFactory.createEtchedBorder());
|
||||
instructionsScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
instructionsTextArea.setEditable(false);
|
||||
instructionsTextArea.setBackground(new java.awt.Color(240, 240, 240));
|
||||
instructionsTextArea.setColumns(20);
|
||||
instructionsTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N
|
||||
instructionsTextArea.setLineWrap(true);
|
||||
instructionsTextArea.setRows(2);
|
||||
instructionsTextArea.setText(org.openide.util.NbBundle.getMessage(BingTranslatorSettingsPanel.class, "BingTranslatorSettingsPanel.instructionsTextArea.text")); // NOI18N
|
||||
instructionsTextArea.setWrapStyleWord(true);
|
||||
instructionsScrollPane.setViewportView(instructionsTextArea);
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(instructionsScrollPane)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(authenticationKeyLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(authenticationKeyField, javax.swing.GroupLayout.PREFERRED_SIZE, 486, javax.swing.GroupLayout.PREFERRED_SIZE))
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(targetLanguageLabel)
|
||||
.addComponent(testButton, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE))
|
||||
.addGap(18, 18, 18)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(untranslatedLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 66, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(resultLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(testResultValueLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(targetLanguageComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addGap(276, 276, 276)))))
|
||||
.addGap(0, 0, Short.MAX_VALUE)))
|
||||
.addContainerGap())
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(instructionsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(authenticationKeyField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(authenticationKeyLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(targetLanguageLabel)
|
||||
.addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(testButton)
|
||||
.addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(untranslatedLabel)
|
||||
.addComponent(resultLabel)
|
||||
.addComponent(testResultValueLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
);
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
|
||||
@Messages({"BingTranslatorSettingsPanel.warning.invalidKey=Invalid translation authentication key"})
|
||||
private void testButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_testButtonActionPerformed
|
||||
if (testTranslationSetup()) {
|
||||
warningLabel.setText("");
|
||||
} else {
|
||||
warningLabel.setText(Bundle.BingTranslatorSettingsPanel_warning_invalidKey());
|
||||
}
|
||||
}//GEN-LAST:event_testButtonActionPerformed
|
||||
|
||||
private void targetLanguageComboBoxSelected(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_targetLanguageComboBoxSelected
|
||||
String selectedCode = ((LanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguageCode();
|
||||
if (!StringUtils.isBlank(selectedCode) && !selectedCode.equals(targetLanguageCode)) {
|
||||
targetLanguageCode = selectedCode;
|
||||
firePropertyChange("SettingChanged", true, false);
|
||||
}
|
||||
}//GEN-LAST:event_targetLanguageComboBoxSelected
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JTextField authenticationKeyField;
|
||||
private javax.swing.JLabel authenticationKeyLabel;
|
||||
private javax.swing.JScrollPane instructionsScrollPane;
|
||||
private javax.swing.JTextArea instructionsTextArea;
|
||||
private javax.swing.JLabel resultLabel;
|
||||
private javax.swing.JComboBox<LanguageWrapper> targetLanguageComboBox;
|
||||
private javax.swing.JLabel targetLanguageLabel;
|
||||
private javax.swing.JButton testButton;
|
||||
private javax.swing.JLabel testResultValueLabel;
|
||||
private javax.swing.JTextField testUntranslatedTextField;
|
||||
private javax.swing.JLabel untranslatedLabel;
|
||||
private javax.swing.JLabel warningLabel;
|
||||
// End of variables declaration//GEN-END:variables
|
||||
|
||||
/**
|
||||
* Attempts to translate the text specified in the Untranslated field using
|
||||
* the settings currently specified but not necessarily saved
|
||||
*
|
||||
* @return true if the translation was able to be performed, false otherwise
|
||||
*
|
||||
*/
|
||||
private boolean testTranslationSetup() {
|
||||
testResultValueLabel.setText("");
|
||||
MediaType mediaType = MediaType.parse("application/json");
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("Text", testUntranslatedTextField.getText());
|
||||
jsonArray.add(jsonObject);
|
||||
String bodyString = jsonArray.toString();
|
||||
|
||||
RequestBody body = RequestBody.create(mediaType,
|
||||
bodyString);
|
||||
Request request = new Request.Builder()
|
||||
.url(BingTranslator.getTranlatorUrl(targetLanguageCode)).post(body)
|
||||
.addHeader("Ocp-Apim-Subscription-Key", authenticationKeyField.getText())
|
||||
.addHeader("Content-type", "application/json").build();
|
||||
try {
|
||||
Response response = new OkHttpClient().newCall(request).execute();
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonArray responses = parser.parse(response.body().string()).getAsJsonArray();
|
||||
//As far as I know, there's always exactly one item in the array.
|
||||
JsonObject response0 = responses.get(0).getAsJsonObject();
|
||||
JsonArray translations = response0.getAsJsonArray("translations");
|
||||
JsonObject translation0 = translations.get(0).getAsJsonObject();
|
||||
testResultValueLabel.setText(translation0.get("text").getAsString());
|
||||
return true;
|
||||
} catch (IOException | IllegalStateException | ClassCastException | NullPointerException | IndexOutOfBoundsException e) {
|
||||
logger.log(Level.WARNING, "Test of Bing Translator failed due to exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently set authentication key to be used for the Microsoft
|
||||
* translation service
|
||||
*
|
||||
* @return the authentication key specified in the textarea
|
||||
*/
|
||||
String getAuthenticationKey() {
|
||||
return authenticationKeyField.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected target language code
|
||||
*
|
||||
* @return the target language code of the language selected in the combobox
|
||||
*/
|
||||
String getTargetLanguageCode() {
|
||||
return targetLanguageCode;
|
||||
}
|
||||
}
|
@ -1,4 +1,17 @@
|
||||
GoogleTranslatorSettingsPanel.browseButton.text=Browse
|
||||
GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials:
|
||||
GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials Path:
|
||||
GoogleTranslatorSettingsPanel.warningLabel.text=
|
||||
GoogleTranslatorSettingsPanel.targetLanguageLabel.text=Target Language:
|
||||
BingTranslatorSettingsPanel.testButton.text=Test
|
||||
BingTranslatorSettingsPanel.testResultValueLabel.text=
|
||||
BingTranslatorSettingsPanel.resultLabel.text=Result:
|
||||
BingTranslatorSettingsPanel.untranslatedLabel.text=Untranslated:
|
||||
BingTranslatorSettingsPanel.targetLanguageLabel.text=Target Language:
|
||||
BingTranslatorSettingsPanel.authenticationKeyField.toolTipText=Enter the hash for the
|
||||
GoogleTranslatorSettingsPanel.testButton.text=Test
|
||||
GoogleTranslatorSettingsPanel.untranslatedLabel.text=Untranslated:
|
||||
GoogleTranslatorSettingsPanel.resultLabel.text=Result:
|
||||
GoogleTranslatorSettingsPanel.testResultValueLabel.text=
|
||||
BingTranslatorSettingsPanel.authenticationKeyLabel.text=Authentication Key:
|
||||
BingTranslatorSettingsPanel.instructionsTextArea.text=You will need to provide a Microsoft Translator authentication key for your Microsoft Translator account. Instructions on how to get one are available here: https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-text-how-to-signup
|
||||
GoogleTranslatorSettingsPanel.instructionsTextArea.text=You will need a JSON credentials file which contains your service account key for your Google Translate account. Information on how to create a service account key is available here: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
|
||||
|
@ -1,8 +1,12 @@
|
||||
BingTranslator.name.text=Bing Translator
|
||||
BingTranslatorSettingsPanel.warning.invalidKey=Invalid translation authentication key
|
||||
BingTranslatorSettingsPanel.warning.targetLanguageFailure=Unable to get list of target languages or parse the result that was received
|
||||
GoogleTranslator.name.text=Google Translate
|
||||
GoogleTranslatorSettingsPanel.browseButton.text=Browse
|
||||
GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials:
|
||||
GoogleTranslatorSettingsPanel.credentialsLabel.text=Credentials Path:
|
||||
GoogleTranslatorSettingsPanel.errorMessage.fileNotFound=Credentials file not found, please set the location to be a valid JSON credentials file.
|
||||
GoogleTranslatorSettingsPanel.errorMessage.noFileSelected=A JSON file must be selected to provide your credentials for Google Translate.
|
||||
GoogleTranslatorSettingsPanel.errorMessage.translationFailure=Translation failure with specified credentials
|
||||
GoogleTranslatorSettingsPanel.errorMessage.unableToMakeCredentials=Unable to construct credentials object from credentials file, please set the location to be a valid JSON credentials file.
|
||||
GoogleTranslatorSettingsPanel.errorMessage.unableToReadCredentials=Unable to read credentials from credentials file, please set the location to be a valid JSON credentials file.
|
||||
GoogleTranslatorSettingsPanel.errorMessage.unknownFailureGetting=Failure getting list of supported languages with current credentials file.
|
||||
@ -11,3 +15,16 @@ GoogleTranslatorSettingsPanel.fileChooser.confirmButton=Select
|
||||
GoogleTranslatorSettingsPanel.json.description=JSON Files
|
||||
GoogleTranslatorSettingsPanel.warningLabel.text=
|
||||
GoogleTranslatorSettingsPanel.targetLanguageLabel.text=Target Language:
|
||||
BingTranslatorSettingsPanel.testButton.text=Test
|
||||
BingTranslatorSettingsPanel.testResultValueLabel.text=
|
||||
BingTranslatorSettingsPanel.resultLabel.text=Result:
|
||||
BingTranslatorSettingsPanel.untranslatedLabel.text=Untranslated:
|
||||
BingTranslatorSettingsPanel.targetLanguageLabel.text=Target Language:
|
||||
BingTranslatorSettingsPanel.authenticationKeyField.toolTipText=Enter the hash for the
|
||||
GoogleTranslatorSettingsPanel.testButton.text=Test
|
||||
GoogleTranslatorSettingsPanel.untranslatedLabel.text=Untranslated:
|
||||
GoogleTranslatorSettingsPanel.resultLabel.text=Result:
|
||||
GoogleTranslatorSettingsPanel.testResultValueLabel.text=
|
||||
BingTranslatorSettingsPanel.authenticationKeyLabel.text=Authentication Key:
|
||||
BingTranslatorSettingsPanel.instructionsTextArea.text=You will need to provide a Microsoft Translator authentication key for your Microsoft Translator account. Instructions on how to get one are available here: https://docs.microsoft.com/en-us/azure/cognitive-services/translator/translator-text-how-to-signup
|
||||
GoogleTranslatorSettingsPanel.instructionsTextArea.text=You will need a JSON credentials file which contains your service account key for your Google Translate account. Information on how to create a service account key is available here: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
|
||||
|
@ -28,10 +28,14 @@ import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openide.util.NbBundle.Messages;
|
||||
import org.openide.util.lookup.ServiceProvider;
|
||||
import org.sleuthkit.autopsy.coreutils.EscapeUtil;
|
||||
import org.sleuthkit.autopsy.texttranslation.TextTranslator;
|
||||
import org.sleuthkit.autopsy.texttranslation.TranslationException;
|
||||
|
||||
@ -43,7 +47,8 @@ import org.sleuthkit.autopsy.texttranslation.TranslationException;
|
||||
public final class GoogleTranslator implements TextTranslator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GoogleTranslator.class.getName());
|
||||
private static final int MAX_STRING_LENGTH = 15000;
|
||||
//See translate method for justification of this limit.
|
||||
private static final int MAX_PAYLOAD_SIZE = 5000;
|
||||
private final GoogleTranslatorSettingsPanel settingsPanel;
|
||||
private final GoogleTranslatorSettings settings = new GoogleTranslatorSettings();
|
||||
private Translate googleTranslate;
|
||||
@ -57,26 +62,49 @@ public final class GoogleTranslator implements TextTranslator {
|
||||
loadTranslator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if google is able to be reached
|
||||
*
|
||||
* @return true if it can be, false otherwise
|
||||
*/
|
||||
private static boolean googleIsReachable() {
|
||||
String host = "www.google.com";
|
||||
InetAddress address;
|
||||
try {
|
||||
address = InetAddress.getByName(host);
|
||||
return address.isReachable(1500);
|
||||
} catch (UnknownHostException ex) {
|
||||
logger.log(Level.WARNING, "Unable to reach google.com due to unknown host", ex);
|
||||
return false;
|
||||
} catch (IOException ex) {
|
||||
logger.log(Level.WARNING, "Unable to reach google.com due IOException", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String translate(String string) throws TranslationException {
|
||||
if (!googleIsReachable()) {
|
||||
throw new TranslationException("Failure translating using GoogleTranslator: Cannot connect to Google");
|
||||
}
|
||||
|
||||
if (googleTranslate != null) {
|
||||
try {
|
||||
// Translates some text into English, without specifying the source language.
|
||||
|
||||
// HTML files were producing lots of white space at the end
|
||||
String substring = string.trim();
|
||||
|
||||
// WE can't currently set parameters, so we are using the default behavior of
|
||||
// asuming the input is HTML. We need to replace newlines with <br> for Google to preserve them
|
||||
// We can't currently set parameters, so we are using the default behavior of
|
||||
// assuming the input is HTML. We need to replace newlines with <br> for Google to preserve them
|
||||
substring = substring.replaceAll("(\r\n|\n)", "<br />");
|
||||
|
||||
// The API complains if the "Payload" is over 204800 bytes. I'm assuming that
|
||||
// deals with the full request. At some point, we get different errors about too
|
||||
// much text. Officially, Google says they will googleTranslate only 5k chars,
|
||||
// but we have seen more than that working.
|
||||
// there could be a value betwen 15k and 25k that works. I (BC) didn't test further
|
||||
if (substring.length() > MAX_STRING_LENGTH) {
|
||||
substring = substring.substring(0, MAX_STRING_LENGTH);
|
||||
// The API complains if the "Payload" is over 204800 bytes. Google references that
|
||||
//their service is optimized for 2K code points and recommends keeping the requests that size.
|
||||
//There is a hard limit of 30K code points per request. There is also a time-based quota that
|
||||
//we are not enforcing, which may lead to 403 errors. We are currently configured for a max of 5K
|
||||
//in each request, for two reasons. 1) To be more in line with Google's recommendation. 2) To
|
||||
//minimize accidental exceedence of time based quotas.
|
||||
if (substring.length() > MAX_PAYLOAD_SIZE) {
|
||||
substring = substring.substring(0, MAX_PAYLOAD_SIZE);
|
||||
}
|
||||
Translation translation
|
||||
= googleTranslate.translate(substring);
|
||||
@ -84,8 +112,12 @@ public final class GoogleTranslator implements TextTranslator {
|
||||
|
||||
// put back the newlines
|
||||
translatedString = translatedString.replaceAll("<br />", "\n");
|
||||
|
||||
// With our current settings, Google Translate outputs HTML
|
||||
// so we need to undo the escape characters.
|
||||
translatedString = EscapeUtil.unEscapeHtml(translatedString);
|
||||
return translatedString;
|
||||
} catch (Throwable ex) {
|
||||
} catch (Throwable ex) {
|
||||
//Catching throwables because some of this Google Translate code throws throwables
|
||||
throw new TranslationException("Failure translating using GoogleTranslator", ex);
|
||||
}
|
||||
@ -106,16 +138,20 @@ public final class GoogleTranslator implements TextTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Google Cloud Translation service give the currently saved
|
||||
* Load the Google Cloud Translation service given the currently saved
|
||||
* settings
|
||||
*/
|
||||
private void loadTranslator() {
|
||||
InputStream credentialStream = null;
|
||||
Credentials creds = null;
|
||||
try {
|
||||
credentialStream = new FileInputStream(settings.getCredentialPath());
|
||||
} catch (FileNotFoundException ex) {
|
||||
logger.log(Level.WARNING, "JSON file for GoogleTranslator credentials not found", ex);
|
||||
if (StringUtils.isBlank(settings.getCredentialPath())) {
|
||||
logger.log(Level.INFO, "No credentials file has been provided for Google Translator");
|
||||
} else {
|
||||
try {
|
||||
credentialStream = new FileInputStream(settings.getCredentialPath());
|
||||
} catch (FileNotFoundException ex) {
|
||||
logger.log(Level.WARNING, "JSON file for GoogleTranslator credentials not found", ex);
|
||||
}
|
||||
}
|
||||
if (credentialStream != null) {
|
||||
try {
|
||||
@ -142,4 +178,9 @@ public final class GoogleTranslator implements TextTranslator {
|
||||
settings.saveSettings();
|
||||
loadTranslator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPayloadSize() {
|
||||
return MAX_PAYLOAD_SIZE;
|
||||
}
|
||||
}
|
||||
|
@ -92,12 +92,14 @@ public final class GoogleTranslatorSettings {
|
||||
}
|
||||
if (ModuleSettings.settingExists(GOOGLE_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY)) {
|
||||
targetLanguageCode = ModuleSettings.getConfigSetting(GOOGLE_TRANSLATE_NAME, TARGET_LANGUAGE_CODE_KEY);
|
||||
} else {
|
||||
}
|
||||
if (targetLanguageCode == null || StringUtils.isBlank(targetLanguageCode)) {
|
||||
targetLanguageCode = DEFAULT_TARGET_LANGUAGE;
|
||||
}
|
||||
if (ModuleSettings.settingExists(GOOGLE_TRANSLATE_NAME, CREDENTIAL_PATH_KEY)) {
|
||||
credentialPath = ModuleSettings.getConfigSetting(GOOGLE_TRANSLATE_NAME, CREDENTIAL_PATH_KEY);
|
||||
} else {
|
||||
}
|
||||
if (credentialPath == null) {
|
||||
credentialPath = DEFAULT_CREDENTIAL_PATH;
|
||||
}
|
||||
}
|
||||
|
@ -17,28 +17,47 @@
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="warningLabel" min="-2" pref="551" max="-2" attributes="0"/>
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
<Component id="instructionsScrollPane" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" attributes="0">
|
||||
<Group type="103" groupAlignment="0" max="-2" attributes="0">
|
||||
<Component id="credentialsLabel" max="32767" attributes="0"/>
|
||||
<Component id="targetLanguageLabel" max="32767" attributes="0"/>
|
||||
<Component id="warningLabel" min="-2" pref="551" max="-2" attributes="0"/>
|
||||
<EmptySpace min="77" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<Group type="103" groupAlignment="1" max="-2" attributes="0">
|
||||
<Component id="testButton" max="32767" attributes="0"/>
|
||||
<Component id="credentialsLabel" alignment="0" max="32767" attributes="0"/>
|
||||
<Component id="targetLanguageLabel" alignment="0" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="credentialsPathField" pref="443" max="32767" attributes="0"/>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="browseButton" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="14" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="credentialsPathField" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="browseButton" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="14" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="targetLanguageComboBox" min="-2" pref="317" max="-2" attributes="0"/>
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="targetLanguageComboBox" min="-2" pref="317" max="-2" attributes="0"/>
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="7" max="-2" attributes="0"/>
|
||||
<Component id="untranslatedLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="testUntranslatedTextField" min="-2" pref="140" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="resultLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="testResultValueLabel" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
@ -50,19 +69,29 @@
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="instructionsScrollPane" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="credentialsLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="credentialsPathField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="browseButton" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="targetLanguageLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="targetLanguageComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="testButton" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="testUntranslatedTextField" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="untranslatedLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="resultLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="testResultValueLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Component id="warningLabel" min="-2" pref="18" max="-2" attributes="0"/>
|
||||
<EmptySpace pref="23" max="32767" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
@ -79,6 +108,9 @@
|
||||
<Properties>
|
||||
<Property name="editable" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="credentialsPathFieldActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="browseButton">
|
||||
<Properties>
|
||||
@ -98,7 +130,7 @@
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<org.sleuthkit.autopsy.texttranslation.translators.GoogleLanguageWrapper>"/>
|
||||
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<org.sleuthkit.autopsy.texttranslation.translators.LanguageWrapper>"/>
|
||||
</AuxValues>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="targetLanguageLabel">
|
||||
@ -118,5 +150,82 @@
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="testResultValueLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.testResultValueLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="resultLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.resultLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="untranslatedLabel">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.untranslatedLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JTextField" name="testUntranslatedTextField">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
|
||||
<Connection code="DEFUALT_TEST_STRING" type="code"/>
|
||||
</Property>
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JButton" name="testButton">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.testButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
<Events>
|
||||
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="testButtonActionPerformed"/>
|
||||
</Events>
|
||||
</Component>
|
||||
<Container class="javax.swing.JScrollPane" name="instructionsScrollPane">
|
||||
<Properties>
|
||||
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
|
||||
<Border info="org.netbeans.modules.form.compat2.border.EtchedBorderInfo">
|
||||
<EtchetBorder/>
|
||||
</Border>
|
||||
</Property>
|
||||
<Property name="horizontalScrollBarPolicy" type="int" value="31"/>
|
||||
</Properties>
|
||||
<AuxValues>
|
||||
<AuxValue name="autoScrollPane" type="java.lang.Boolean" value="true"/>
|
||||
</AuxValues>
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
|
||||
<SubComponents>
|
||||
<Component class="javax.swing.JTextArea" name="instructionsTextArea">
|
||||
<Properties>
|
||||
<Property name="editable" type="boolean" value="false"/>
|
||||
<Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
|
||||
<Color blue="f0" green="f0" red="f0" type="rgb"/>
|
||||
</Property>
|
||||
<Property name="columns" type="int" value="20"/>
|
||||
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">
|
||||
<Font name="Tahoma" size="11" style="0"/>
|
||||
</Property>
|
||||
<Property name="lineWrap" type="boolean" value="true"/>
|
||||
<Property name="rows" type="int" value="2"/>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/translators/Bundle.properties" key="GoogleTranslatorSettingsPanel.instructionsTextArea.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
<Property name="wrapStyleWord" type="boolean" value="true"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Container>
|
||||
</SubComponents>
|
||||
</Form>
|
||||
|
@ -21,7 +21,9 @@ package org.sleuthkit.autopsy.texttranslation.translators;
|
||||
import com.google.auth.Credentials;
|
||||
import com.google.auth.oauth2.ServiceAccountCredentials;
|
||||
import com.google.cloud.translate.Language;
|
||||
import com.google.cloud.translate.Translate;
|
||||
import com.google.cloud.translate.TranslateOptions;
|
||||
import com.google.cloud.translate.Translation;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@ -44,6 +46,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GoogleTranslatorSettingsPanel.class.getName());
|
||||
private static final String JSON_EXTENSION = "json";
|
||||
private static final String DEFUALT_TEST_STRING = "traducción exitoso"; //spanish which should translate to something along the lines of "successful translation"
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final ItemListener listener = new ComboBoxSelectionListener();
|
||||
private String targetLanguageCode = "";
|
||||
@ -60,16 +63,15 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
|
||||
/**
|
||||
* Private method to make a temporary translation service given the current
|
||||
* settings and use it to retrieve the available target languages for
|
||||
* population of combobox with target language with unsaved settings.
|
||||
* settings with unsaved settings.
|
||||
*
|
||||
* @return A list of Languages
|
||||
* @return A Translate object which is the translation service
|
||||
*/
|
||||
@Messages({"GoogleTranslatorSettingsPanel.errorMessage.fileNotFound=Credentials file not found, please set the location to be a valid JSON credentials file.",
|
||||
"GoogleTranslatorSettingsPanel.errorMessage.unableToReadCredentials=Unable to read credentials from credentials file, please set the location to be a valid JSON credentials file.",
|
||||
"GoogleTranslatorSettingsPanel.errorMessage.unableToMakeCredentials=Unable to construct credentials object from credentials file, please set the location to be a valid JSON credentials file.",
|
||||
"GoogleTranslatorSettingsPanel.errorMessage.unknownFailureGetting=Failure getting list of supported languages with current credentials file.",})
|
||||
private List<Language> getListOfTargetLanguages() {
|
||||
private Translate getTemporaryTranslationService() {
|
||||
//This method also has the side effect of more or less validating the JSON file which was selected as it is necessary to get the list of target languages
|
||||
try {
|
||||
InputStream credentialStream;
|
||||
@ -77,31 +79,31 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
credentialStream = new FileInputStream(credentialsPathField.getText());
|
||||
} catch (FileNotFoundException ignored) {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_fileNotFound());
|
||||
return new ArrayList<>();
|
||||
return null;
|
||||
}
|
||||
Credentials creds;
|
||||
try {
|
||||
creds = ServiceAccountCredentials.fromStream(credentialStream);
|
||||
} catch (IOException ignored) {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unableToMakeCredentials());
|
||||
return new ArrayList<>();
|
||||
return null;
|
||||
}
|
||||
if (creds == null) {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unableToReadCredentials());
|
||||
logger.log(Level.WARNING, "Credentials were not successfully made, no translations will be available from the GoogleTranslator");
|
||||
return new ArrayList<>();
|
||||
return null;
|
||||
} else {
|
||||
TranslateOptions.Builder builder = TranslateOptions.newBuilder();
|
||||
builder.setCredentials(creds);
|
||||
builder.setTargetLanguage(targetLanguageCode); //localize the list to the currently selected target language
|
||||
warningLabel.setText(""); //clear any previous warning text
|
||||
return builder.build().getService().listSupportedLanguages();
|
||||
return builder.build().getService();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
//Catching throwables because some of this Google Translate code throws throwables
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unknownFailureGetting());
|
||||
logger.log(Level.WARNING, "Throwable caught while getting list of supported languages", throwable);
|
||||
return new ArrayList<>();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,29 +116,51 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
targetLanguageComboBox.removeItemListener(listener);
|
||||
try {
|
||||
if (!StringUtils.isBlank(credentialsPathField.getText())) {
|
||||
List<Language> listSupportedLanguages = getListOfTargetLanguages();
|
||||
List<Language> listSupportedLanguages;
|
||||
Translate tempService = getTemporaryTranslationService();
|
||||
if (tempService != null) {
|
||||
listSupportedLanguages = tempService.listSupportedLanguages();
|
||||
} else {
|
||||
listSupportedLanguages = new ArrayList<>();
|
||||
}
|
||||
targetLanguageComboBox.removeAllItems();
|
||||
if (!listSupportedLanguages.isEmpty()) {
|
||||
listSupportedLanguages.forEach((lang) -> {
|
||||
targetLanguageComboBox.addItem(new GoogleLanguageWrapper(lang));
|
||||
targetLanguageComboBox.addItem(new LanguageWrapper(lang));
|
||||
});
|
||||
selectLanguageByCode(targetLanguageCode);
|
||||
targetLanguageComboBox.addItemListener(listener);
|
||||
targetLanguageComboBox.setEnabled(true);
|
||||
enableControls(true);
|
||||
|
||||
} else {
|
||||
targetLanguageComboBox.setEnabled(false);
|
||||
enableControls(false);
|
||||
}
|
||||
} else {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_noFileSelected());
|
||||
targetLanguageComboBox.setEnabled(false);
|
||||
enableControls(false);
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_unknownFailurePopulating());
|
||||
logger.log(Level.WARNING, "Throwable caught while populating list of supported languages", throwable);
|
||||
targetLanguageComboBox.setEnabled(false);
|
||||
enableControls(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to enable/disable all controls which are dependent on valid
|
||||
* credentials having been provided
|
||||
*
|
||||
* @param enabled true to enable the controls, false to disable them
|
||||
*/
|
||||
private void enableControls(boolean enabled) {
|
||||
targetLanguageComboBox.setEnabled(enabled);
|
||||
testButton.setEnabled(enabled);
|
||||
testResultValueLabel.setEnabled(enabled);
|
||||
testUntranslatedTextField.setEnabled(enabled);
|
||||
untranslatedLabel.setEnabled(enabled);
|
||||
resultLabel.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a language code select the corresponding language in the combo box
|
||||
* if it is present
|
||||
@ -145,7 +169,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
*/
|
||||
private void selectLanguageByCode(String code) {
|
||||
for (int i = 0; i < targetLanguageComboBox.getModel().getSize(); i++) {
|
||||
if (targetLanguageComboBox.getItemAt(i).getLanguage().getCode().equals(code)) {
|
||||
if (targetLanguageComboBox.getItemAt(i).getLanguageCode().equals(code)) {
|
||||
targetLanguageComboBox.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
@ -167,10 +191,22 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
targetLanguageComboBox = new javax.swing.JComboBox<>();
|
||||
targetLanguageLabel = new javax.swing.JLabel();
|
||||
warningLabel = new javax.swing.JLabel();
|
||||
testResultValueLabel = new javax.swing.JLabel();
|
||||
resultLabel = new javax.swing.JLabel();
|
||||
untranslatedLabel = new javax.swing.JLabel();
|
||||
testUntranslatedTextField = new javax.swing.JTextField();
|
||||
testButton = new javax.swing.JButton();
|
||||
instructionsScrollPane = new javax.swing.JScrollPane();
|
||||
instructionsTextArea = new javax.swing.JTextArea();
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(credentialsLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.credentialsLabel.text")); // NOI18N
|
||||
|
||||
credentialsPathField.setEditable(false);
|
||||
credentialsPathField.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
credentialsPathFieldActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.browseButton.text")); // NOI18N
|
||||
browseButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
@ -186,6 +222,38 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
warningLabel.setForeground(new java.awt.Color(255, 0, 0));
|
||||
org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.warningLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(testResultValueLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.testResultValueLabel.text")); // NOI18N
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(resultLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.resultLabel.text")); // NOI18N
|
||||
resultLabel.setEnabled(false);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(untranslatedLabel, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.untranslatedLabel.text")); // NOI18N
|
||||
untranslatedLabel.setEnabled(false);
|
||||
|
||||
testUntranslatedTextField.setText(DEFUALT_TEST_STRING);
|
||||
testUntranslatedTextField.setEnabled(false);
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(testButton, org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.testButton.text")); // NOI18N
|
||||
testButton.setEnabled(false);
|
||||
testButton.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
testButtonActionPerformed(evt);
|
||||
}
|
||||
});
|
||||
|
||||
instructionsScrollPane.setBorder(javax.swing.BorderFactory.createEtchedBorder());
|
||||
instructionsScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
instructionsTextArea.setEditable(false);
|
||||
instructionsTextArea.setBackground(new java.awt.Color(240, 240, 240));
|
||||
instructionsTextArea.setColumns(20);
|
||||
instructionsTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N
|
||||
instructionsTextArea.setLineWrap(true);
|
||||
instructionsTextArea.setRows(2);
|
||||
instructionsTextArea.setText(org.openide.util.NbBundle.getMessage(GoogleTranslatorSettingsPanel.class, "GoogleTranslatorSettingsPanel.instructionsTextArea.text")); // NOI18N
|
||||
instructionsTextArea.setWrapStyleWord(true);
|
||||
instructionsScrollPane.setViewportView(instructionsTextArea);
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
@ -194,27 +262,44 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addGap(0, 0, Short.MAX_VALUE))
|
||||
.addComponent(instructionsScrollPane)
|
||||
.addContainerGap())
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
|
||||
.addComponent(credentialsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(targetLanguageLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 551, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
|
||||
.addComponent(testButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(credentialsLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(targetLanguageLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(credentialsPathField, javax.swing.GroupLayout.DEFAULT_SIZE, 443, Short.MAX_VALUE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(browseButton)
|
||||
.addGap(14, 14, 14))
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(credentialsPathField)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(browseButton)
|
||||
.addGap(14, 14, 14))
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 317, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addGap(0, 0, Short.MAX_VALUE))))
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 317, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addGap(0, 0, Short.MAX_VALUE))))))
|
||||
.addGap(7, 7, 7)
|
||||
.addComponent(untranslatedLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(resultLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(testResultValueLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))))))
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(instructionsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(credentialsLabel)
|
||||
.addComponent(credentialsPathField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
@ -223,9 +308,16 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(targetLanguageLabel)
|
||||
.addComponent(targetLanguageComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(testButton)
|
||||
.addComponent(testUntranslatedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(untranslatedLabel)
|
||||
.addComponent(resultLabel)
|
||||
.addComponent(testResultValueLabel))
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addContainerGap(23, Short.MAX_VALUE))
|
||||
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
);
|
||||
}// </editor-fold>//GEN-END:initComponents
|
||||
|
||||
@ -249,12 +341,39 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
}
|
||||
}//GEN-LAST:event_browseButtonActionPerformed
|
||||
|
||||
@Messages({"GoogleTranslatorSettingsPanel.errorMessage.translationFailure=Translation failure with specified credentials"})
|
||||
private void testButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_testButtonActionPerformed
|
||||
testResultValueLabel.setText("");
|
||||
Translate tempTranslate = getTemporaryTranslationService();
|
||||
if (tempTranslate != null) {
|
||||
try {
|
||||
Translation translation = tempTranslate.translate(testUntranslatedTextField.getText());
|
||||
testResultValueLabel.setText(translation.getTranslatedText());
|
||||
warningLabel.setText("");
|
||||
} catch (Exception ex) {
|
||||
warningLabel.setText(Bundle.GoogleTranslatorSettingsPanel_errorMessage_translationFailure());
|
||||
logger.log(Level.WARNING, Bundle.GoogleTranslatorSettingsPanel_errorMessage_translationFailure(), ex);
|
||||
}
|
||||
}
|
||||
}//GEN-LAST:event_testButtonActionPerformed
|
||||
|
||||
private void credentialsPathFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_credentialsPathFieldActionPerformed
|
||||
// TODO add your handling code here:
|
||||
}//GEN-LAST:event_credentialsPathFieldActionPerformed
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JButton browseButton;
|
||||
private javax.swing.JLabel credentialsLabel;
|
||||
private javax.swing.JTextField credentialsPathField;
|
||||
private javax.swing.JComboBox<org.sleuthkit.autopsy.texttranslation.translators.GoogleLanguageWrapper> targetLanguageComboBox;
|
||||
private javax.swing.JScrollPane instructionsScrollPane;
|
||||
private javax.swing.JTextArea instructionsTextArea;
|
||||
private javax.swing.JLabel resultLabel;
|
||||
private javax.swing.JComboBox<org.sleuthkit.autopsy.texttranslation.translators.LanguageWrapper> targetLanguageComboBox;
|
||||
private javax.swing.JLabel targetLanguageLabel;
|
||||
private javax.swing.JButton testButton;
|
||||
private javax.swing.JLabel testResultValueLabel;
|
||||
private javax.swing.JTextField testUntranslatedTextField;
|
||||
private javax.swing.JLabel untranslatedLabel;
|
||||
private javax.swing.JLabel warningLabel;
|
||||
// End of variables declaration//GEN-END:variables
|
||||
|
||||
@ -284,7 +403,7 @@ public class GoogleTranslatorSettingsPanel extends javax.swing.JPanel {
|
||||
|
||||
@Override
|
||||
public void itemStateChanged(java.awt.event.ItemEvent evt) {
|
||||
String selectedCode = ((GoogleLanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguage().getCode();
|
||||
String selectedCode = ((LanguageWrapper) targetLanguageComboBox.getSelectedItem()).getLanguageCode();
|
||||
if (!StringUtils.isBlank(selectedCode) && !selectedCode.equals(targetLanguageCode)) {
|
||||
targetLanguageCode = selectedCode;
|
||||
populateTargetLanguageComboBox();
|
||||
|
@ -21,19 +21,33 @@ package org.sleuthkit.autopsy.texttranslation.translators;
|
||||
import com.google.cloud.translate.Language;
|
||||
|
||||
/**
|
||||
* Wrapper for the Language class
|
||||
* Wrapper for Language definitions used by translators
|
||||
*/
|
||||
class GoogleLanguageWrapper {
|
||||
class LanguageWrapper {
|
||||
|
||||
private final Language language;
|
||||
private final String languageCode;
|
||||
private final String languageDisplayName;
|
||||
|
||||
/**
|
||||
* Create a new GoogleLanguageWrapper
|
||||
* Create a new LanguageWrapper to wrap the google language object
|
||||
*
|
||||
* @param lang the Language object to wrap
|
||||
*/
|
||||
GoogleLanguageWrapper(Language lang) {
|
||||
language = lang;
|
||||
LanguageWrapper(Language language) {
|
||||
languageCode = language.getCode();
|
||||
languageDisplayName = language.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new LanguageWrapper to wrap json elements that identify a
|
||||
* language for microsofts translation service
|
||||
*
|
||||
* @param code the code which uniquely identifies a language
|
||||
* @param name the name of the language
|
||||
*/
|
||||
LanguageWrapper(String code, String name) {
|
||||
languageCode = code;
|
||||
languageDisplayName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,14 +55,14 @@ class GoogleLanguageWrapper {
|
||||
*
|
||||
* @return the wrapped Language
|
||||
*/
|
||||
Language getLanguage() {
|
||||
return language;
|
||||
String getLanguageCode() {
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//toString overridden so that the jComboBox in the GoogleTranslatorSettingsPanel will display the name of the language
|
||||
return language.getName();
|
||||
//toString overridden so that the jComboBox in the TranslatorSettingsPanels will display the name of the language
|
||||
return languageDisplayName;
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
OptionsCategory_Name_Machine_Translation=Machine Translation
|
||||
OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings
|
||||
TranslationContentPanel.ShowLabel.text=Show:
|
||||
TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed
|
||||
TranslationContentPanel.ocrLabel.text=OCR:
|
||||
TranslationOptionsPanel.translationServiceLabel.text=Text translator:
|
||||
TranslationOptionsPanelController.moduleErr=Module Error
|
||||
TranslationOptionsPanelController.moduleErr.msg=A module caused an error listening to TranslationSettingsPanelController updates. See log to determine which module. Some data could be incomplete.
|
||||
TranslationOptionsPanel.translationOptionsDescription.text=Configure a 3rd party text translation service to enable text and file name translation.
|
||||
|
@ -1,6 +1,6 @@
|
||||
OptionsCategory_Name_Machine_Translation=Machine Translation
|
||||
OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings
|
||||
TranslatedContentPanel.comboBoxOption.originalText=Original Text
|
||||
TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB)
|
||||
TranslatedContentPanel.comboBoxOption.translatedText=Translated Text
|
||||
TranslatedContentViewer.emptyTranslation=The resulting translation was empty.
|
||||
TranslatedContentViewer.errorExtractingText=Could not extract text from file.
|
||||
@ -12,11 +12,11 @@ TranslatedContentViewer.noServiceProvider=Machine Translation software was not f
|
||||
TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer.
|
||||
TranslatedContentViewer.translatingText=Translating text, please wait...
|
||||
TranslatedContentViewer.translationException=Error encountered while attempting translation.
|
||||
TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated
|
||||
TranslatedTextViewer.title=Translation
|
||||
TranslatedTextViewer.toolTip=Displays translated file text.
|
||||
TranslationContentPanel.autoDetectOCR=Autodetect language
|
||||
TranslationContentPanel.ShowLabel.text=Show:
|
||||
TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed
|
||||
TranslationContentPanel.ocrLabel.text=OCR:
|
||||
TranslationOptionsPanel.noTextTranslators.text=No text translators exist, translation is disabled.
|
||||
TranslationOptionsPanel.noTextTranslatorSelected.text=No text translator selected, translation is disabled.
|
||||
@ -25,3 +25,4 @@ TranslationOptionsPanel.translationDisabled.text=Translation disabled
|
||||
TranslationOptionsPanel.translationServiceLabel.text=Text translator:
|
||||
TranslationOptionsPanelController.moduleErr=Module Error
|
||||
TranslationOptionsPanelController.moduleErr.msg=A module caused an error listening to TranslationSettingsPanelController updates. See log to determine which module. Some data could be incomplete.
|
||||
TranslationOptionsPanel.translationOptionsDescription.text=Configure a 3rd party text translation service to enable text and file name translation.
|
||||
|
@ -67,7 +67,7 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
|
||||
private static final boolean OCR_ENABLED = true;
|
||||
private static final boolean OCR_DISABLED = false;
|
||||
private static final int MAX_SIZE_1MB = 1024000;
|
||||
private static final int MAX_EXTRACT_SIZE_BYTES = 25600;
|
||||
private static final List<String> INSTALLED_LANGUAGE_PACKS = PlatformUtil.getOcrLanguagePacks();
|
||||
private final TranslationContentPanel panel = new TranslationContentPanel();
|
||||
|
||||
@ -77,6 +77,9 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
= new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build();
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory);
|
||||
|
||||
@NbBundle.Messages({
|
||||
"TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated"
|
||||
})
|
||||
@Override
|
||||
public void setNode(final Node node) {
|
||||
this.node = node;
|
||||
@ -92,6 +95,9 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS);
|
||||
}
|
||||
}
|
||||
|
||||
int payloadMaxInKB = TextTranslationService.getInstance().getMaxPayloadSize() / 1000;
|
||||
panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB));
|
||||
|
||||
//Force a background task.
|
||||
displayDropDownListener.actionPerformed(null);
|
||||
@ -296,8 +302,9 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
|
||||
//Correct for UTF-8
|
||||
byte[] resultInUTF8Bytes = result.getBytes("UTF8");
|
||||
byte[] trimTo1MB = Arrays.copyOfRange(resultInUTF8Bytes, 0, MAX_SIZE_1MB );
|
||||
return new String(trimTo1MB, "UTF-8");
|
||||
byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0,
|
||||
Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES) );
|
||||
return new String(trimToArraySize, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -331,7 +338,7 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
|
||||
//Short-circuit the read if its greater than our max
|
||||
//translatable size
|
||||
int bytesLeft = MAX_SIZE_1MB - bytesRead;
|
||||
int bytesLeft = MAX_EXTRACT_SIZE_BYTES - bytesRead;
|
||||
|
||||
if (bytesLeft < read) {
|
||||
textBuilder.append(cbuf, 0, bytesLeft);
|
||||
@ -341,9 +348,8 @@ public final class TranslatedTextViewer implements TextViewer {
|
||||
textBuilder.append(cbuf, 0, read);
|
||||
bytesRead += read;
|
||||
}
|
||||
|
||||
// The trim is on here because HTML files were observed with nearly 1MB of white space at the end
|
||||
return textBuilder.toString().trim();
|
||||
|
||||
return textBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,7 +16,7 @@
|
||||
<Layout>
|
||||
<DimensionLayout dim="0">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Component id="jPanel1" alignment="1" pref="628" max="32767" attributes="0"/>
|
||||
<Component id="jPanel1" alignment="1" pref="872" max="32767" attributes="0"/>
|
||||
<Component id="jScrollPane1" alignment="0" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
@ -48,12 +48,12 @@
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="warningLabel2MB" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="32767" attributes="0"/>
|
||||
<Component id="warningLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace pref="422" max="32767" attributes="0"/>
|
||||
<Component id="ShowLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="displayTextComboBox" min="-2" pref="128" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Component id="displayTextComboBox" min="-2" pref="150" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<Component id="ocrLabel" min="-2" pref="26" max="-2" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Component id="ocrDropdown" min="-2" pref="180" max="-2" attributes="0"/>
|
||||
@ -72,18 +72,25 @@
|
||||
</DimensionLayout>
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="1" attributes="0">
|
||||
<EmptySpace min="9" pref="9" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="1" attributes="0">
|
||||
<Group type="103" alignment="1" groupAlignment="3" attributes="0">
|
||||
<Component id="displayTextComboBox" alignment="3" pref="26" max="32767" attributes="0"/>
|
||||
<Component id="ShowLabel" alignment="3" max="32767" attributes="0"/>
|
||||
<Component id="ocrLabel" alignment="3" max="32767" attributes="0"/>
|
||||
<Component id="warningLabel2MB" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace min="-2" pref="9" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="warningLabel" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="102" attributes="0">
|
||||
<Group type="103" groupAlignment="1" attributes="0">
|
||||
<Group type="103" alignment="1" groupAlignment="3" attributes="0">
|
||||
<Component id="displayTextComboBox" alignment="3" pref="26" max="32767" attributes="0"/>
|
||||
<Component id="ShowLabel" alignment="3" max="32767" attributes="0"/>
|
||||
<Component id="ocrLabel" alignment="3" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="ocrDropdown" alignment="1" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace min="-2" pref="7" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="ocrDropdown" alignment="1" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace min="-2" pref="7" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<Group type="103" rootIndex="1" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
@ -153,14 +160,11 @@
|
||||
<Property name="enabled" type="boolean" value="false"/>
|
||||
</Properties>
|
||||
</Component>
|
||||
<Component class="javax.swing.JLabel" name="warningLabel2MB">
|
||||
<Component class="javax.swing.JLabel" name="warningLabel">
|
||||
<Properties>
|
||||
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
|
||||
<Image iconType="3" name="/org/sleuthkit/autopsy/images/warning16.png"/>
|
||||
</Property>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties" key="TranslationContentPanel.warningLabel2MB.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
|
@ -87,6 +87,10 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
dropDown.removeActionListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
void setWarningLabelMsg(String msg) {
|
||||
warningLabel.setText(msg);
|
||||
}
|
||||
|
||||
@NbBundle.Messages({"TranslationContentPanel.autoDetectOCR=Autodetect language"})
|
||||
final void reset() {
|
||||
@ -246,7 +250,7 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
/**
|
||||
* Selection choices to be displayed in the combobox dropdown.
|
||||
*/
|
||||
@Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text",
|
||||
@Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB)",
|
||||
"TranslatedContentPanel.comboBoxOption.translatedText=Translated Text"})
|
||||
static enum DisplayDropdownOptions {
|
||||
ORIGINAL_TEXT(Bundle.TranslatedContentPanel_comboBoxOption_originalText()),
|
||||
@ -280,7 +284,7 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
displayTextComboBox = new javax.swing.JComboBox<>();
|
||||
ocrDropdown = new javax.swing.JComboBox<>();
|
||||
ocrLabel = new javax.swing.JLabel();
|
||||
warningLabel2MB = new javax.swing.JLabel();
|
||||
warningLabel = new javax.swing.JLabel();
|
||||
jScrollPane1 = new javax.swing.JScrollPane();
|
||||
displayTextArea = new javax.swing.JTextArea();
|
||||
|
||||
@ -302,8 +306,7 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
org.openide.awt.Mnemonics.setLocalizedText(ocrLabel, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.ocrLabel.text")); // NOI18N
|
||||
ocrLabel.setEnabled(false);
|
||||
|
||||
warningLabel2MB.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N
|
||||
org.openide.awt.Mnemonics.setLocalizedText(warningLabel2MB, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.warningLabel2MB.text")); // NOI18N
|
||||
warningLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N
|
||||
|
||||
javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
|
||||
jPanel1.setLayout(jPanel1Layout);
|
||||
@ -311,12 +314,12 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(warningLabel2MB)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(warningLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 422, Short.MAX_VALUE)
|
||||
.addComponent(ShowLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 128, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addComponent(ocrLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
|
||||
.addComponent(ocrDropdown, javax.swing.GroupLayout.PREFERRED_SIZE, 180, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
@ -331,16 +334,20 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
);
|
||||
jPanel1Layout.setVerticalGroup(
|
||||
jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup()
|
||||
.addGroup(jPanel1Layout.createSequentialGroup()
|
||||
.addGap(9, 9, 9)
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE)
|
||||
.addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(warningLabel2MB))
|
||||
.addComponent(ocrDropdown))
|
||||
.addGap(7, 7, 7))
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(jPanel1Layout.createSequentialGroup()
|
||||
.addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addContainerGap())
|
||||
.addGroup(jPanel1Layout.createSequentialGroup()
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE)
|
||||
.addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addComponent(ocrDropdown))
|
||||
.addGap(7, 7, 7))))
|
||||
.addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(jPanel1Layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
@ -362,7 +369,7 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE)
|
||||
.addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 872, Short.MAX_VALUE)
|
||||
.addComponent(jScrollPane1)
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
@ -384,6 +391,6 @@ public class TranslationContentPanel extends javax.swing.JPanel {
|
||||
private javax.swing.JSeparator jSeparator2;
|
||||
private javax.swing.JComboBox<String> ocrDropdown;
|
||||
private javax.swing.JLabel ocrLabel;
|
||||
private javax.swing.JLabel warningLabel2MB;
|
||||
private javax.swing.JLabel warningLabel;
|
||||
// End of variables declaration//GEN-END:variables
|
||||
}
|
||||
|
@ -19,13 +19,14 @@
|
||||
<Group type="102" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<Component id="translationServicePanel" max="32767" attributes="0"/>
|
||||
<Group type="102" attributes="0">
|
||||
<Component id="translationServiceLabel" min="-2" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" pref="10" max="-2" attributes="0"/>
|
||||
<Component id="translatorComboBox" min="-2" pref="214" max="-2" attributes="0"/>
|
||||
<EmptySpace min="0" pref="162" max="32767" attributes="0"/>
|
||||
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<Component id="translationServicePanel" alignment="1" max="32767" attributes="0"/>
|
||||
<Component id="translationOptionsDescription" alignment="0" pref="462" max="32767" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
</Group>
|
||||
@ -34,14 +35,16 @@
|
||||
<DimensionLayout dim="1">
|
||||
<Group type="103" groupAlignment="0" attributes="0">
|
||||
<Group type="102" alignment="0" attributes="0">
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
<Component id="translationOptionsDescription" min="-2" pref="15" max="-2" attributes="0"/>
|
||||
<EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/>
|
||||
<Group type="103" groupAlignment="3" attributes="0">
|
||||
<Component id="translatorComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
<Component id="translationServiceLabel" alignment="3" min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
<Component id="translationServicePanel" max="32767" attributes="0"/>
|
||||
<EmptySpace max="-2" attributes="0"/>
|
||||
<EmptySpace min="-2" max="-2" attributes="0"/>
|
||||
</Group>
|
||||
</Group>
|
||||
</DimensionLayout>
|
||||
@ -66,5 +69,12 @@
|
||||
|
||||
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
|
||||
</Container>
|
||||
<Component class="javax.swing.JLabel" name="translationOptionsDescription">
|
||||
<Properties>
|
||||
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
|
||||
<ResourceString bundle="org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties" key="TranslationOptionsPanel.translationOptionsDescription.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/>
|
||||
</Property>
|
||||
</Properties>
|
||||
</Component>
|
||||
</SubComponents>
|
||||
</Form>
|
||||
|
@ -145,6 +145,7 @@ public class TranslationOptionsPanel extends javax.swing.JPanel {
|
||||
translatorComboBox = new javax.swing.JComboBox<>();
|
||||
translationServiceLabel = new javax.swing.JLabel();
|
||||
translationServicePanel = new javax.swing.JPanel();
|
||||
translationOptionsDescription = new javax.swing.JLabel();
|
||||
|
||||
translatorComboBox.addActionListener(new java.awt.event.ActionListener() {
|
||||
public void actionPerformed(java.awt.event.ActionEvent evt) {
|
||||
@ -156,6 +157,8 @@ public class TranslationOptionsPanel extends javax.swing.JPanel {
|
||||
|
||||
translationServicePanel.setLayout(new java.awt.BorderLayout());
|
||||
|
||||
org.openide.awt.Mnemonics.setLocalizedText(translationOptionsDescription, org.openide.util.NbBundle.getMessage(TranslationOptionsPanel.class, "TranslationOptionsPanel.translationOptionsDescription.text")); // NOI18N
|
||||
|
||||
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
|
||||
this.setLayout(layout);
|
||||
layout.setHorizontalGroup(
|
||||
@ -163,18 +166,21 @@ public class TranslationOptionsPanel extends javax.swing.JPanel {
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addComponent(translationServicePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(translationServiceLabel)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGap(10, 10, 10)
|
||||
.addComponent(translatorComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 214, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addGap(0, 162, Short.MAX_VALUE))
|
||||
.addComponent(translationServicePanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
|
||||
.addGap(0, 0, Short.MAX_VALUE))
|
||||
.addComponent(translationOptionsDescription, javax.swing.GroupLayout.DEFAULT_SIZE, 462, Short.MAX_VALUE))
|
||||
.addContainerGap())
|
||||
);
|
||||
layout.setVerticalGroup(
|
||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addContainerGap()
|
||||
.addComponent(translationOptionsDescription, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
|
||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(translatorComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
|
||||
.addComponent(translationServiceLabel))
|
||||
@ -190,6 +196,7 @@ public class TranslationOptionsPanel extends javax.swing.JPanel {
|
||||
|
||||
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
private javax.swing.JLabel translationOptionsDescription;
|
||||
private javax.swing.JLabel translationServiceLabel;
|
||||
private javax.swing.JPanel translationServicePanel;
|
||||
private javax.swing.JComboBox<String> translatorComboBox;
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2019 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.texttranslation.translators;
|
||||
|
||||
//import java.io.IOException;
|
||||
//import org.junit.After;
|
||||
//import org.junit.AfterClass;
|
||||
//import org.junit.Before;
|
||||
//import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
//import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests for the BingTranslator translation service, these tests have been
|
||||
* commented out because they require credentials to perform
|
||||
*/
|
||||
public class BingTranslatorTest {
|
||||
|
||||
@Test
|
||||
public void testTranslate() {
|
||||
// BingTranslator translator = new BingTranslator();
|
||||
// String input = "gato";
|
||||
// String expectedTranslation = "cat";
|
||||
// runTest(translator, input, expectedTranslation);
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testQuickStartSentence() throws Exception {
|
||||
// BingTranslator translator = new BingTranslator();
|
||||
// String input = "Willkommen bei Microsoft Translator. Raten Sie mal, wie viele Sprachen ich spreche.";
|
||||
// String expectedTranslation = "Welcome to Microsoft Translator. Guess how many languages I speak.";
|
||||
// runTest(translator, input, expectedTranslation);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// public void testCharacterEscapes() throws Exception {
|
||||
// BingTranslator translator = new BingTranslator();
|
||||
// String input = "\"gato\"";;
|
||||
// String expectedTranslation = "Cat";
|
||||
// runTest(translator, input, expectedTranslation);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// public void testLineBreaks() throws Exception {
|
||||
// BingTranslator translator = new BingTranslator();
|
||||
// String input = "gato\nperro";;
|
||||
// String expectedTranslation = "cat\nDog";
|
||||
// runTest(translator, input, expectedTranslation);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Test whether translator throws an error. This should not be part of our
|
||||
// * regular testing, because we are limited to only 2MB of free translations
|
||||
// * ever.
|
||||
// * @param translator A BingTranslator
|
||||
// * @param input Text to translate
|
||||
// * @param expectedTranslation Not used unless you uncomment those lines.
|
||||
// */
|
||||
// public void runTest(BingTranslator translator, String input, String expectedTranslation) {
|
||||
// String translation;
|
||||
// try {
|
||||
// translation = translator.translate(input);
|
||||
// }
|
||||
// catch (Throwable e) {
|
||||
// fail("Bing translation produced an exception: " + e.getMessage());
|
||||
// return;
|
||||
// };
|
||||
//
|
||||
// /*
|
||||
// //It's unrealistic to expect the same answer every time, but sometimes
|
||||
// //it's helpful to have this in your debug process.
|
||||
// System.out.println(translation);
|
||||
// assertEquals(expectedTranslation, translation);
|
||||
// */
|
||||
// }
|
||||
}
|
@ -698,7 +698,7 @@ public final class ImageGalleryController {
|
||||
//grab files with supported mime-types
|
||||
+ MIMETYPE_CLAUSE //NON-NLS
|
||||
//grab files with image or video mime-types even if we don't officially support them
|
||||
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )" //NON-NLS
|
||||
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )" //NON-NLS
|
||||
+ " ORDER BY parent_path ";
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Autopsy Forensic Browser
|
||||
*
|
||||
* Copyright 2013-2018 Basis Technology Corp.
|
||||
* Copyright 2013-2019 Basis Technology Corp.
|
||||
* Contact: carrier <at> sleuthkit <dot> org
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -32,6 +32,7 @@ import org.openide.util.NbBundle;
|
||||
import org.openide.util.Utilities;
|
||||
import org.openide.util.lookup.ProxyLookup;
|
||||
import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint;
|
||||
import org.sleuthkit.autopsy.directorytree.ExportCSVAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExternalViewerAction;
|
||||
import org.sleuthkit.autopsy.directorytree.ExtractAction;
|
||||
import org.sleuthkit.autopsy.actions.AddContentTagAction;
|
||||
@ -163,6 +164,7 @@ class AdHocSearchFilterNode extends FilterNode {
|
||||
actionsList.add(new ExternalViewerAction(NbBundle.getMessage(this.getClass(), "KeywordSearchFilterNode.getFileActions.openExternViewActLbl"), getOriginal()));
|
||||
actionsList.add(null);
|
||||
actionsList.add(ExtractAction.getInstance());
|
||||
actionsList.add(ExportCSVAction.getInstance());
|
||||
actionsList.add(null); // creates a menu separator
|
||||
actionsList.add(AddContentTagAction.getInstance());
|
||||
|
||||
|
BIN
docs/doxygen-user/images/InterestingFiles/bomb_png.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/download_archive.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/ingest.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/main.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/new_large_files.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/new_rule.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/new_rule_set.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/private_folder.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/doxygen-user/images/InterestingFiles/results.png
Normal file
After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 20 KiB |
@ -1,81 +1,114 @@
|
||||
/*! \page interesting_files_identifier_page Interesting Files Identifier Module
|
||||
|
||||
What Does It Do
|
||||
========
|
||||
\section interesting_files_overview Overview
|
||||
|
||||
The Interesting Files module allows you to search for files or directories in a data source and generate alerts when they are found. You configure rules for the files that you want to find.
|
||||
The Interesting Files module allows you to automatically flag files and directories that match a set of rules. This can be useful if you always need to check whether files with a given name or path are in the data source, or if you are always interested in files with a certain type.
|
||||
|
||||
Use this to be notified when certain things are found. There are examples below that generate alerts when VMWare images are found or when iPhone backup files are found. This module is useful for file types that will frequently have a consistent name and that may not be part of the standard checklist that you look for, or if you simply want to automate your checklist.
|
||||
This module allows you to make sets of rules that will be run against each file as it is processed. If a file matches any of the rules, you will see an entry for it in the \ref tree_viewer_page. You can share your rules with other users, and import sets made by others into your copy of Autopsy.
|
||||
|
||||
Configuration
|
||||
=======
|
||||
\section interesting_files_terminology Terminology
|
||||
|
||||
Add rules using "Tools", "Options", "Interesting Files".
|
||||
<ul>
|
||||
<li>A <b>rule</b> is a set of conditions that must be true about a file for it to match the rule. All conditions in the rule must be true. For example, if a rule has conditions "file size > 1 MB" and "file extension = .txt", only files that match both conditions will be considered a match.
|
||||
<li>A <b>rule set</b> is a collection of rules. If a file matches any rule in the rule set it will be flagged as a match for this rule set. Rule sets can be enabled and disabled at ingest time.
|
||||
</ul>
|
||||
|
||||
All rules need to be part of a set. Select "New set" on the left side panel to create a new set. Sets need to have the following defined:
|
||||
\section interesting_files_config Configuration
|
||||
|
||||
- Set Name (required)
|
||||
- Set Description (optional)
|
||||
To create and edit your rule sets, go to "Tools", "Options" and then select the "Interesting Files" tab. The area on the left side will show you a list of all the rule sets that are currently available. Selecting a rule set will display its description and information about each of its rules on the right side of the panel.
|
||||
|
||||
Sets can be renamed, edited, copied, and imported and exported from the left side panel.
|
||||
\image html InterestingFiles/main.png
|
||||
|
||||
Rules specify what to look for in a data source. Each rule specifies:
|
||||
- Type: If the rule should be applied to only files, only directories, or both files and directories.
|
||||
- Name Pattern: String to match the file name against. Note that you can enter multiple extensions in a comma-separated list.
|
||||
- Name Pattern Type: Should the pattern be matched against the full file type or just the extension.
|
||||
- Path Pattern: A substring of the parent path that must be matched. This allows you to restrict generic names to a specific structure (such as an application name). A substring match is performed.
|
||||
- Rule Name: Additional details that are displayed in the UI when that rule is matched. This allows you to determine which rule in the set matched.
|
||||
The buttons on the bottom of the left side of the panel control the rule sets.
|
||||
|
||||
\image html interesting_files_configuration.PNG
|
||||
<ul>
|
||||
<li><b>New Set</b> - Allows you to create a new rule set (rules will be added later). You will see a new window asking for the name of the new rule set, an optional description, and whether known files should be ignored (i.e., if a file is in the NSRL, then it won't show up on the list of matches even if it satisfies the conditions of one of the rules in the set).
|
||||
\image html InterestingFiles/new_rule_set.png
|
||||
<li><b>Edit Set</b> - Brings up the same window as "New Set" and allows you to change any of the fields.
|
||||
<li><b>Delete Set</b> - Removes the selected rule set
|
||||
<li><b>Copy Set</b> - Makes a copy of the selected rule set. It will bring up the same window as "New Set". You must change the rule set name in order to save the copy.
|
||||
<li><b>Import Set</b> - Imports a previously exported rule set. Once imported, you will not need the original copy.
|
||||
<li><b>Export Set</b> - Exports the selected rule set in a format that can be shared with other Autopsy users.
|
||||
</ul>
|
||||
|
||||
Selecting a rule set will display its description, whether it ignores known files, and the rules contained in the set. Selecting a rule will display the conditions for that rule in the "Rule Details" section.
|
||||
|
||||
VMWare Example
|
||||
--------
|
||||
This set of rules is to detect VMWare Player or vmdk files. This would help to make sure you look into the virtual machines for additional evidence.
|
||||
The buttons under the list of rules allow you to create new rules and edit or delete existing rules. Selecting "New Rule" will bring up a new window to create the rule.
|
||||
|
||||
NOTE: This is not extensive and is simply a minimal example:
|
||||
\image html InterestingFiles/new_rule.png
|
||||
|
||||
The top line allows you to choose whether you want to match only files, only directories, or both. If you select directories or both, some of the condition types will be unavailable since they only apply to files.
|
||||
|
||||
- Set Name: VMWare
|
||||
- Rule 1:
|
||||
- Type: Files
|
||||
- Full Name: vmplayer.exe
|
||||
- Name: Program EXE
|
||||
- Rule 2:
|
||||
- Type: Files
|
||||
- Extension: vmdk
|
||||
- Name: VMDK File
|
||||
Each rule must have at least one condition. To create conditions, check the box to the left of the condition you want to enable. The following is a description of each condition, with some full examples after.
|
||||
|
||||
iPhone Backups Example
|
||||
-------------
|
||||
This set of rules is to detect a folder for iPhone Backups. These are typically in a folder such as "%AppData%\Roaming\Apple Computer\MobileSync\Backup" on Windows. Here is a rule that you could use for that.
|
||||
<ul>
|
||||
<li><b>Name</b> - Enter either the full file name or one or more extensions, and select whether this is an exact match or a substring/regex match. If substring/regex match is enabled, it will automatically add wildcards to the beginning and end of the text. If you're only matching directories, this will match the directory name. If you're using a comma-separated list of extensions, make sure the regex checkbox is disabled - the two features do not work together. The following table shows some examples of what the different combinations can be used for.
|
||||
|
||||
- Set Name: iPhone Backups
|
||||
- Rule 1:
|
||||
- Type: Directory
|
||||
- Name: Backup
|
||||
- Path: Apple Computer/MobileSync
|
||||
<table>
|
||||
<tr><th>Type</th><th>Substring/Regex</th><th>Text</th><th>Description</th><th>Sample match</th></tr>
|
||||
<tr><td>Full Name</td><td>false</td><td>\verbatim test.txt \endverbatim</td><td>Will match files named "test.txt"</td><td>text.txt</tr>
|
||||
<tr><td>Full Name</td><td>true</td><td>\verbatim bomb \endverbatim</td><td>Will match files with "bomb" anywhere their name</td><td>Pipe bomb.png</td></tr>
|
||||
<tr><td>Full Name</td><td>true</td><td>\verbatim virus.*\.exe \endverbatim</td><td>Will match files with "virus" followed by ".exe" anywhere their name</td><td>bad_virus.exe</td></tr>
|
||||
<tr><td>Extension Only</td><td>false</td><td>\verbatim zip \endverbatim</td><td>Will match .zip files</td><td>myArchive.zip</td></tr>
|
||||
<tr><td>Extension Only</td><td>false</td><td>\verbatim zip,rar,7z \endverbatim</td><td>Will match .zip, .rar, and .7z files</td><td>anotherArchive.rar</td></tr>
|
||||
<tr><td>Extension Only</td><td>true</td><td>\verbatim jp \endverbatim</td><td>Will match .jpg, .jpeg files, and any others with "jp" in the extension</td><td>myImage.jpg</td></tr>
|
||||
</table>
|
||||
|
||||
<li><b>Path Substring</b> - Enter a folder name that must be part of file's path for it to be a match. If you only want to specify that a word appears somewhere in the path, use the regex option.
|
||||
<table>
|
||||
<tr><th>Regex</th><th>Text</th><th>Description</th><th>Sample match</th></tr>
|
||||
<tr><td>false</td><td>\verbatim Documents \endverbatim</td><td>Match any file that has a folder named "Documents" in its path</td><td>/folder1/Documents/fileA.doc</td></tr>
|
||||
<tr><td>true</td><td>\verbatim bomb \endverbatim</td><td>Match any file with "bomb" in the path</td><td>/folder1/bomb making/file2.doc</td></tr>
|
||||
<tr><td>true</td><td>\verbatim Users/.*/Downloads \endverbatim</td><td>Match any file with "Users" and "Downloads" in the path</td><td>C:/Users/user1/Downloads/myFile.txt</td></tr>
|
||||
</table>
|
||||
|
||||
Using the Module
|
||||
======
|
||||
<li><b>MIME Type</b> - Use the pull-down list to select a MIME type. Only a single MIME type can be selected.
|
||||
|
||||
When you enable the Interesting Files module, you can choose what rule sets to enable. To add rules, use the "Advanced" button from the ingest module panel.
|
||||
<li><b>File Size</b> - Select whether you want to match files equal to, smaller than, or larger than a given size.
|
||||
|
||||
When files are found, they will be in the Interesting Files area of the tree. You should see the set and rule names with the match.
|
||||
<li><b>Modified Within</b> - Select how recently a file must have been modified to match the rule.
|
||||
</ul>
|
||||
|
||||
Finally you can optionally enter a name for the rule. This will be displayed in the UI for each match.
|
||||
|
||||
Ingest Settings
|
||||
------
|
||||
\subsection interesting_files_examples Examples
|
||||
Here are a few examples of rules being created.
|
||||
|
||||
When running the ingest modules, the user can choose which interesting file rules to enable .
|
||||
<br>
|
||||
\image html interesting_files_ingest_settings.PNG
|
||||
This is a rule that matches any file with "bomb" in the name that also has an "image/png" MIME type.
|
||||
|
||||
Seeing Results
|
||||
------
|
||||
The results show up in the tree under "Results", "Interesting Items".
|
||||
\image html InterestingFiles/bomb_png.png
|
||||
|
||||
\image html interesting_files_results.PNG
|
||||
This is a rule that matches folders named "Private".
|
||||
|
||||
\image html InterestingFiles/private_folder.png
|
||||
|
||||
This rule is looking for archives in the user download directory. It requires "Users" and "Downloads" in the file's path, and an extension of .zip, .rar, or .7z.
|
||||
|
||||
\image html InterestingFiles/download_archive.png
|
||||
|
||||
This is a rule that matches files with size at least 50MB that have been modified in the last week.
|
||||
|
||||
\image html InterestingFiles/new_large_files.png
|
||||
|
||||
\section interesting_files_running Running the Module
|
||||
|
||||
At runtime, you can select which rule sets you would like to run on your data source.
|
||||
|
||||
\image html InterestingFiles/ingest.png
|
||||
|
||||
\section interesting_files_results Viewing Results
|
||||
|
||||
Files that match any of the rules in the enabled rule sets will be shown in the Results section of the \ref tree_viewer_page under "Interesting Items" and then the name of the rule set that matched. Note that other modules besides Interesting Files put results in this section of the tree, so there may be more than just what matched your rule sets. Selecting the "Interesting Files" node under one of your rule sets will display all matching files in the \ref result_viewer_page.
|
||||
|
||||
\image html InterestingFiles/results.png
|
||||
|
||||
You can see which rule matched in the "Category" column. You can export some or all of the files for further analysis. To do this, first use the standard Windows file
|
||||
selection methods to highlight the files you want to export in the \ref result_viewer_page :
|
||||
<ul>
|
||||
<li>Hold down Ctrl and click on each file you want to export
|
||||
<li>Hold down Shift to select a range of files
|
||||
<li>Click on any file in the Result Viewer and then hit Ctrl+A to select all the files
|
||||
</ul>
|
||||
Once you have your desired files selected, right click and select “Extract Files” to save copies of them.
|
||||
|
||||
*/
|
||||
|
@ -143,8 +143,9 @@ final class VcardParser {
|
||||
* @throws NoCurrentCaseException If there is no open case.
|
||||
*/
|
||||
void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException {
|
||||
VCard vcard = Ezvcard.parse(vcardFile).first();
|
||||
addContactArtifact(vcard, abstractFile);
|
||||
for (VCard vcard: Ezvcard.parse(vcardFile).all()) {
|
||||
addContactArtifact(vcard, abstractFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|