Remove DSP

This commit is contained in:
Ann Priestman 2016-12-09 11:09:41 -05:00
parent 657c32ada0
commit ce1df65f45
7 changed files with 0 additions and 1278 deletions

View File

@ -1,241 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.cellex.datasourceprocessors;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.services.FileManager;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.LocalFilesDataSource;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.SleuthkitJNI;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskDataException;
/*
* A runnable that adds the image files from a Cellebrite UFED output folder to
* a case database. If SleuthKit fails to find a filesystem in any of input
* image files, the file is added to the case as a local/logical file instead.
*/
class AddCellebritePhysicalReportTask implements Runnable {
private static final Logger logger = Logger.getLogger(AddCellebritePhysicalReportTask.class.getName());
public static final String MODULE_NAME = "Cellebrite UFED Output Data Source Processor";
public static final String TSK_FS_TYPE_UNKNOWN_ERR_MSG = "Cannot determine file system type";
private final String deviceId;
private final List<String> imageFilePaths;
private final String timeZone;
private final DataSourceProcessorProgressMonitor progressMonitor;
private final DataSourceProcessorCallback callback;
private final Case currentCase;
private boolean criticalErrorOccurred;
private volatile boolean cancelled;
/**
* Constructs a runnable that adds the image files from a Cellebrite UFED
* output folder to a case database. If SleuthKit fails to find a filesystem
* in any of input image files, the file is added to the case as a
* local/logical file instead.
*
* @param deviceId An ASCII-printable identifier for the device
* associated with the data source that is intended
* to be unique across multiple cases (e.g., a UUID).
* @param imageFilePaths The paths of the Cellebrite output files.
* @param timeZone The time zone to use when processing dates and
* times for the image, obtained from
* java.util.TimeZone.getID.
* @param progressMonitor Progress monitor for reporting progress during
* processing.
* @param callback Callback to call when processing is done.
*/
AddCellebritePhysicalReportTask(String deviceId, List<String> imageFilePaths, String timeZone, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
this.deviceId = deviceId;
this.imageFilePaths = imageFilePaths;
this.timeZone = timeZone;
this.callback = callback;
this.progressMonitor = progressMonitor;
currentCase = Case.getCurrentCase();
}
@Override
public void run() {
/*
* Try to add the input image files as images.
*/
List<Content> newDataSources = new ArrayList<>();
List<String> localFileDataSourcePaths = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
currentCase.getSleuthkitCase().acquireExclusiveLock();
try {
progressMonitor.setIndeterminate(true);
for (String imageFilePath : imageFilePaths) {
if (!cancelled) {
addImageToCase(imageFilePath, newDataSources, localFileDataSourcePaths, errorMessages);
}
}
} finally {
currentCase.getSleuthkitCase().releaseExclusiveLock();
}
/*
* Try to add any input image files that did not have file systems as a
* single local/logical files set with the device id as the root virtual
* directory name.
*/
if (!cancelled && localFileDataSourcePaths.size() > 0) {
FileManager fileManager = currentCase.getServices().getFileManager();
FileManager.FileAddProgressUpdater progressUpdater = (final AbstractFile newFile) -> {
progressMonitor.setProgressText(String.format("Adding: %s as logical file", Paths.get(newFile.getParentPath(), newFile.getName())));
};
try {
LocalFilesDataSource localFilesDataSource = fileManager.addLocalFilesDataSource(deviceId, deviceId, timeZone, localFileDataSourcePaths, progressUpdater);
newDataSources.add(localFilesDataSource.getRootDirectory());
} catch (TskCoreException | TskDataException ex) {
errorMessages.add(String.format("Error adding images without file systems for device %s: %s", deviceId, ex.getLocalizedMessage()));
criticalErrorOccurred = true;
}
}
/*
* This appears to be the best that can be done to indicate completion
* with the DataSourceProcessorProgressMonitor in its current form.
*/
progressMonitor.setProgress(0);
progressMonitor.setProgress(100);
/*
* Pass the results back via the callback.
*/
DataSourceProcessorResult result;
if (criticalErrorOccurred) {
result = DataSourceProcessorResult.CRITICAL_ERRORS;
} else if (!errorMessages.isEmpty()) {
result = DataSourceProcessorResult.NONCRITICAL_ERRORS;
} else {
result = DataSourceProcessorResult.NO_ERRORS;
}
callback.done(result, errorMessages, newDataSources);
criticalErrorOccurred = false;
}
/**
* Attempts to cancel the processing of the input image files. May result in
* partial processing of the input.
*/
public void cancelTask() {
logger.log(Level.WARNING, "AddCellebriteAndroidImageTask cancelled, processing may be incomplete");
cancelled = true;
}
/**
* Attempts to add an input image to the case.
*
* @param imageFilePath The image file path.
* @param newDataSources If the image is added, a data source is
* added to this list for eventual return to
* the caller via the callback.
* @param localFileDataSourcePaths If the image cannot be added because
* SleuthKit cannot detect a filesystem, the
* image file path is added to this list for
* later addition as a part of a
* local/logical files data source.
* @param errorMessages If there are any error messages, the
* error messages are added to this list for
* eventual return to the caller via the
* callback.
*/
private void addImageToCase(String imageFilePath, List<Content> newDataSources, List<String> localFileDataSourcePaths, List<String> errorMessages) {
/*
* Try to add the image to the case database as a data source.
*/
progressMonitor.setProgressText(String.format("Adding: %s", imageFilePath));
SleuthkitCase caseDatabase = currentCase.getSleuthkitCase();
SleuthkitJNI.CaseDbHandle.AddImageProcess addImageProcess = caseDatabase.makeAddImageProcess(timeZone, false, false);
Thread progressReporterThread = new Thread(new AddImageProgressReportingTask(progressMonitor, addImageProcess));
try {
progressReporterThread.start();
addImageProcess.run(deviceId, new String[]{imageFilePath});
} catch (TskCoreException ex) {
if (ex.getMessage().contains(TSK_FS_TYPE_UNKNOWN_ERR_MSG)) {
/*
* If SleuthKit failed to add the image because it did not find
* a file system, save the image path so it can be added to the
* case as part of a local/logical files data source. All other
* errors are critical.
*/
localFileDataSourcePaths.add(imageFilePath);
} else {
errorMessages.add(String.format("Critical error adding %s for %s:", imageFilePath, deviceId, ex.getLocalizedMessage()));
criticalErrorOccurred = true;
}
/*
* Either way, the add image process needs to be reverted.
*/
try {
addImageProcess.revert();
} catch (TskCoreException e) {
errorMessages.add(String.format("Critical error reverting add image process for %s for %s: %s", imageFilePath, deviceId, e.getLocalizedMessage()));
criticalErrorOccurred = true;
}
return;
} catch (TskDataException ex) {
errorMessages.add(String.format("Non-critical error adding %s for %s: %s", imageFilePath, deviceId, ex.getLocalizedMessage()));
} finally {
progressReporterThread.interrupt();
}
/*
* Try to commit the results of the add image process, retrieve the new
* image from the case database, and add it to the list of new data
* sources to be returned via the callback.
*/
try {
long imageId = addImageProcess.commit();
Image dataSource = caseDatabase.getImageById(imageId);
newDataSources.add(dataSource);
/*
* Verify the size of the new image. Note that it may not be what is
* expected, but at least part of it was added to the case.
*/
String verificationError = dataSource.verifyImageSize();
if (!verificationError.isEmpty()) {
errorMessages.add(String.format("Non-critical error adding %s for device %s: %s", imageFilePath, deviceId, verificationError));
}
} catch (TskCoreException ex) {
/*
* The add image process commit failed or querying the case database
* for the newly added image failed. Either way, this is a critical
* error.
*/
errorMessages.add(String.format("Critical error adding %s for device %s: %s", imageFilePath, deviceId, ex.getLocalizedMessage()));
criticalErrorOccurred = true;
}
}
}

View File

@ -1,74 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.cellex.datasourceprocessors;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor;
import org.sleuthkit.datamodel.SleuthkitJNI;
/*
* A Runnable that updates a data source processor progress monitor with the
* name of the directory currently being processed by a SleuthKit add image
* process.
*
* TODO (JIRA-1578): The sleep code in the run method should be removed. Clients
* should use a java.util.concurrent.ScheduledThreadPoolExecutor instead to be
* able to control update frequency and cancellation.
*/
class AddImageProgressReportingTask implements Runnable {
DataSourceProcessorProgressMonitor progressMonitor;
SleuthkitJNI.CaseDbHandle.AddImageProcess addImageProcess;
/**
* Constructs a Runnable that updates a data source processor progress
* monitor with the name of the directory currently being processed by a
* SleuthKit add image process.
*
* @param progressMonitor The progress monitor.
* @param addImageProcess An Sleuth add image process.
*/
AddImageProgressReportingTask(DataSourceProcessorProgressMonitor progressMonitor, SleuthkitJNI.CaseDbHandle.AddImageProcess addImageProcess) {
this.progressMonitor = progressMonitor;
this.addImageProcess = addImageProcess;
}
/**
* Every two seconds, updates the progress monitor with the name of the
* directory currently being processed by the add image process.
*/
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
String currDir = addImageProcess.currentDirectory();
if (null != currDir && !currDir.isEmpty()) {
progressMonitor.setProgressText("Adding: " + currDir);
}
/*
* TODO (JIRA-1578): The sleep should be removed here. Clients
* should use a java.util.concurrent.ScheduledThreadPoolExecutor
* instead to be able to control update frequency and
* cancellation,
*/
Thread.sleep(2 * 1000);
}
} catch (InterruptedException expected) {
}
}
}

View File

@ -1,28 +0,0 @@
OpenIDE-Module-Display-Category=Ingest Module
OpenIDE-Module-Long-Description=\
Extracts application information, lists potentially suspicious databases and SD card folders, and parses known SQLite databases.
OpenIDE-Module-Name=Android Triage
MPFModuleSimplePanel.jSizeLabel.text=Min Size (MB):
MPFModuleSimplePanel.jImgExtCheckBox.text=.img
MPFModuleSimplePanel.jNoExtCheckBox.text=\ No extension
MPFModuleSimplePanel.jBinExtCheckBox.text=.bin
MPFModuleSimplePanel.jMinSizeSpinner.toolTipText=Minimum size of file to be processed by MPF.
MPFModuleSimplePanel.jAllFilesRadioButton.text=Yes (will take longer)
MPFModuleSimplePanel.jFilterFilesRadioButton.text=No
MPFModuleSimplePanel.jLabel2.text=File Extensions:
MPFModuleSimplePanel.jLabel3.text=Process files that meet the following criteria:
MPFModuleSimplePanel.jLabel4.text=Run on all files in set of logical files?
OpenIDE-Module-Short-Description=Does triage of Android data.
CellebritePhysicalReportInputPanel.errorLabel.text=Error Label
CellebritePhysicalReportInputPanel.timeZoneLabel.text=Please select the input timezone:
CellebritePhysicalReportInputPanel.browseButton.text=Browse
CellebritePhysicalReportInputPanel.pathTextField.text=
CellebritePhysicalReportInputPanel.pathLabel.text=Browse for a Android phone image folder:
CellebriteLogicalReportPanel.pathLabel.text=Browse for a Cellebrite XML file:
CellebriteLogicalReportPanel.errorLabel.text=Error Label
CellebriteLogicalReportPanel.jSIMRadioButton.text=SIM
CellebriteLogicalReportPanel.jLabel1.text=Input type:
CellebriteLogicalReportPanel.jHandsetRadioButton.text=Handset
CellebriteLogicalReportPanel.browseButton.text=Browse
CellebriteLogicalReportPanel.pathTextField.text=
CellebritePhysicalReportProcessor.process.exception.text=Exception while trying to extract archive

View File

@ -1,115 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.5" maxVersion="1.8" 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" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="pathLabel" alignment="0" min="-2" max="-2" attributes="0"/>
<Group type="102" alignment="0" attributes="0">
<Component id="pathTextField" min="-2" pref="286" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="browseButton" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="timeZoneLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="timeZoneComboBox" min="-2" pref="215" max="-2" attributes="0"/>
</Group>
<Component id="errorLabel" alignment="0" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="pathLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="pathTextField" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="browseButton" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace min="-2" pref="8" max="-2" attributes="0"/>
<Component id="errorLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="timeZoneLabel" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="timeZoneComboBox" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace pref="198" max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JLabel" name="pathLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/cellex/datasourceprocessors/Bundle.properties" key="CellebritePhysicalReportInputPanel.pathLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JTextField" name="pathTextField">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/cellex/datasourceprocessors/Bundle.properties" key="CellebritePhysicalReportInputPanel.pathTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="browseButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/cellex/datasourceprocessors/Bundle.properties" key="CellebritePhysicalReportInputPanel.browseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="browseButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="timeZoneLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/cellex/datasourceprocessors/Bundle.properties" key="CellebritePhysicalReportInputPanel.timeZoneLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JComboBox" name="timeZoneComboBox">
<Properties>
<Property name="maximumRowCount" type="int" value="30"/>
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
<StringArray count="0"/>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="&lt;String&gt;"/>
</AuxValues>
</Component>
<Component class="javax.swing.JLabel" name="errorLabel">
<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/experimental/cellex/datasourceprocessors/Bundle.properties" key="CellebritePhysicalReportInputPanel.errorLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
</SubComponents>
</Form>

View File

@ -1,348 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.cellex.datasourceprocessors;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.Calendar;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import javax.swing.JFileChooser;
import javax.swing.JPanel;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor;
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
import org.sleuthkit.autopsy.coreutils.PathValidator;
public class CellebritePhysicalReportInputPanel extends JPanel implements DocumentListener {
private static final long serialVersionUID = 1L;
private final String PROP_LASTIMAGE_PATH = "LBL_LastImage_PATH";
private PropertyChangeSupport pcs = null;
private JFileChooser fc = new JFileChooser();
// Externally supplied name is used to store settings
private String contextName;
/**
* Creates new form CellebriteAndroidInputPanel
*/
public CellebritePhysicalReportInputPanel(String context) {
initComponents();
errorLabel.setVisible(false);
fc.setDragEnabled(false);
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
fc.setMultiSelectionEnabled(false);
this.contextName = context;
pcs = new PropertyChangeSupport(this);
createTimeZoneList();
}
/**
* Creates and returns an instance of a ImageFilePanel.
*/
public static synchronized CellebritePhysicalReportInputPanel createInstance(String context) {
CellebritePhysicalReportInputPanel instance = new CellebritePhysicalReportInputPanel(context);
instance.postInit();
return instance;
}
//post-constructor initialization to properly initialize listener support
//without leaking references of uninitialized objects
private void postInit() {
pathTextField.getDocument().addDocumentListener(this);
}
/**
* Creates the drop down list for the time zones and then makes the local
* machine time zone to be selected.
*/
public void createTimeZoneList() {
// load and add all timezone
String[] ids = SimpleTimeZone.getAvailableIDs();
for (String id : ids) {
TimeZone zone = TimeZone.getTimeZone(id);
int offset = zone.getRawOffset() / 1000;
int hour = offset / 3600;
int minutes = (offset % 3600) / 60;
String item = String.format("(GMT%+d:%02d) %s", hour, minutes, id);
/*
* DateFormat dfm = new SimpleDateFormat("z");
* dfm.setTimeZone(zone); boolean hasDaylight =
* zone.useDaylightTime(); String first = dfm.format(new Date(2010,
* 1, 1)); String second = dfm.format(new Date(2011, 6, 6)); int mid
* = hour * -1; String result = first + Integer.toString(mid);
* if(hasDaylight){ result = result + second; }
* timeZoneComboBox.addItem(item + " (" + result + ")");
*/
timeZoneComboBox.addItem(item);
}
// get the current timezone
TimeZone thisTimeZone = Calendar.getInstance().getTimeZone();
int thisOffset = thisTimeZone.getRawOffset() / 1000;
int thisHour = thisOffset / 3600;
int thisMinutes = (thisOffset % 3600) / 60;
String formatted = String.format("(GMT%+d:%02d) %s", thisHour, thisMinutes, thisTimeZone.getID());
// set the selected timezone
timeZoneComboBox.setSelectedItem(formatted);
}
/**
* Get the path of the user selected folder.
*
* @return the image path
*/
public String getContentPaths() {
return pathTextField.getText();
}
/**
* Set the path of the images folder.
*/
public void setContentPath(String s) {
pathTextField.setText(s);
}
public String getTimeZone() {
String tz = timeZoneComboBox.getSelectedItem().toString();
return tz.substring(tz.indexOf(")") + 2).trim();
}
/**
* 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() {
pathLabel = new javax.swing.JLabel();
pathTextField = new javax.swing.JTextField();
browseButton = new javax.swing.JButton();
timeZoneLabel = new javax.swing.JLabel();
timeZoneComboBox = new javax.swing.JComboBox<>();
errorLabel = new javax.swing.JLabel();
org.openide.awt.Mnemonics.setLocalizedText(pathLabel, org.openide.util.NbBundle.getMessage(CellebritePhysicalReportInputPanel.class, "CellebritePhysicalReportInputPanel.pathLabel.text")); // NOI18N
pathTextField.setText(org.openide.util.NbBundle.getMessage(CellebritePhysicalReportInputPanel.class, "CellebritePhysicalReportInputPanel.pathTextField.text")); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(CellebritePhysicalReportInputPanel.class, "CellebritePhysicalReportInputPanel.browseButton.text")); // NOI18N
browseButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
browseButtonActionPerformed(evt);
}
});
org.openide.awt.Mnemonics.setLocalizedText(timeZoneLabel, org.openide.util.NbBundle.getMessage(CellebritePhysicalReportInputPanel.class, "CellebritePhysicalReportInputPanel.timeZoneLabel.text")); // NOI18N
timeZoneComboBox.setMaximumRowCount(30);
errorLabel.setForeground(new java.awt.Color(255, 0, 0));
org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(CellebritePhysicalReportInputPanel.class, "CellebritePhysicalReportInputPanel.errorLabel.text")); // NOI18N
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(pathLabel)
.addGroup(layout.createSequentialGroup()
.addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 286, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(browseButton))
.addGroup(layout.createSequentialGroup()
.addComponent(timeZoneLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE))
.addComponent(errorLabel))
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(pathLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(browseButton))
.addGap(8, 8, 8)
.addComponent(errorLabel)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(timeZoneLabel)
.addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addContainerGap(198, Short.MAX_VALUE))
);
}// </editor-fold>//GEN-END:initComponents
@SuppressWarnings("deprecation")
private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
String oldText = pathTextField.getText();
// set the current directory of the FileChooser if the ImagePath Field is valid
File currentDir = new File(oldText);
if (currentDir.exists()) {
fc.setCurrentDirectory(currentDir);
}
int retval = fc.showOpenDialog(this);
if (retval == JFileChooser.APPROVE_OPTION) {
String path = fc.getSelectedFile().getPath();
pathTextField.setText(path);
}
pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.FOCUS_NEXT.toString(), false, true);
}//GEN-LAST:event_browseButtonActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton browseButton;
private javax.swing.JLabel errorLabel;
private javax.swing.JLabel pathLabel;
private javax.swing.JTextField pathTextField;
private javax.swing.JComboBox<String> timeZoneComboBox;
private javax.swing.JLabel timeZoneLabel;
// End of variables declaration//GEN-END:variables
/**
* Update functions are called by the pathTextField which has this set as
* it's DocumentEventListener. Each update function fires a property change
* to be caught by the parent panel.
*
* @param e the event, which is ignored
*/
@Override
public void insertUpdate(DocumentEvent e) {
pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
}
@Override
public void removeUpdate(DocumentEvent e) {
pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
}
@Override
public void changedUpdate(DocumentEvent e) {
pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
}
/**
* Set the focus to the pathTextField.
*/
public void select() {
pathTextField.requestFocusInWindow();
}
@Override
public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) {
super.addPropertyChangeListener(pcl);
if (pcs == null) {
pcs = new PropertyChangeSupport(this);
}
pcs.addPropertyChangeListener(pcl);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener pcl) {
super.removePropertyChangeListener(pcl);
pcs.removePropertyChangeListener(pcl);
}
/**
* Should we enable the next button of the wizard?
*
* @return true if a proper image has been selected, false otherwise
*/
public boolean validatePanel() {
errorLabel.setVisible(false);
String path = getContentPaths();
if (path == null || path.isEmpty()) {
return false;
}
// display warning if there is one (but don't disable "next" button)
warnIfPathIsInvalid(path);
// check if a folder exists by this name.
File f = new File(path);
boolean isValid = (f.exists() && f.isDirectory());
return isValid;
}
/**
* Validates path to selected data source and displays warning if it is
* invalid.
*
* @param path Absolute path to the selected data source
*/
private void warnIfPathIsInvalid(String path) {
if (!PathValidator.isValid(path, Case.getCurrentCase().getCaseType())) {
errorLabel.setVisible(true);
errorLabel.setText("Path to multi-user data source is on \"C:\" drive");
}
}
public void reset() {
//reset the UI elements to default
pathTextField.setText(null);
}
/*
* Store the last used settings
*/
public void storeSettings() {
String imagePathName = getContentPaths();
if (null != imagePathName) {
String imagePath = imagePathName.substring(0, imagePathName.lastIndexOf(File.separator) + 1);
ModuleSettings.setConfigSetting(contextName, PROP_LASTIMAGE_PATH, imagePath);
}
}
/*
* Read and load the last used settings
*/
public void readSettings() {
String lastImagePath = ModuleSettings.getConfigSetting(contextName, PROP_LASTIMAGE_PATH);
if (null != lastImagePath) {
if (!lastImagePath.isEmpty()) {
pathTextField.setText(lastImagePath);
}
}
}
}

View File

@ -1,340 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.cellex.datasourceprocessors;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor;
import java.io.File;
import java.io.FileOutputStream;
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.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.swing.JPanel;
import javax.swing.filechooser.FileFilter;
import org.apache.commons.io.FilenameUtils;
import org.openide.util.NbBundle;
import org.openide.util.lookup.ServiceProvider;
import org.openide.util.lookup.ServiceProviders;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.GeneralFilter;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor;
import org.sleuthkit.autopsy.coreutils.TimeStampUtils;
/**
* An Cellebrite UFED output folder data source processor that implements the
* DataSourceProcessor service provider interface to allow integration with the
* add data source wizard. It also provides a run method overload to allow it to
* be used independently of the wizard.
*/
@ServiceProviders(value={
@ServiceProvider(service=DataSourceProcessor.class),
@ServiceProvider(service=AutomatedIngestDataSourceProcessor.class)}
)
public class CellebritePhysicalReportProcessor implements AutomatedIngestDataSourceProcessor {
private static final String DATA_SOURCE_TYPE = "Cellebrite Physical Report";
private final CellebritePhysicalReportInputPanel configPanel;
private AddCellebritePhysicalReportTask addImagesTask;
private static final List<String> CELLEBRITE_EXTS = Arrays.asList(new String[]{".bin"});
private static final String CELLEBRITE_DESC = "Cellebrite Physical Files (*.bin)";
private static final GeneralFilter binFileFilter = new GeneralFilter(CELLEBRITE_EXTS, CELLEBRITE_DESC);
private static final List<FileFilter> filtersList = new ArrayList<>();
static {
filtersList.add(binFileFilter);
}
private static final GeneralFilter zipFilter = new GeneralFilter(Arrays.asList(new String[]{".zip"}), "");
private static final List<FileFilter> archiveFilters = new ArrayList<>();
static {
archiveFilters.add(zipFilter);
}
private static final String AUTO_INGEST_MODULE_OUTPUT_DIR = "AutoIngest";
/**
* Constructs a Cellebrite UFED output folder data source processor that
* implements the DataSourceProcessor service provider interface to allow
* integration with the add data source wizard. It also provides a run
* method overload to allow it to be used independently of the wizard.
*/
public CellebritePhysicalReportProcessor() {
configPanel = CellebritePhysicalReportInputPanel.createInstance(CellebritePhysicalReportProcessor.class.getName());
}
/**
* Gets the file extensions supported by this data source processor as a
* list of file filters.
*
* @return List<FileFilter> List of FileFilter objects
*/
public static final List<FileFilter> getFileFilterList() {
return filtersList;
}
/**
* Gets a string that describes the type of data sources this processor is
* able to add to the case database. The string is suitable for display in a
* type selection UI component (e.g., a combo box).
*
* @return A data source type display string for this data source processor.
*/
@Override
public String getDataSourceType() {
return DATA_SOURCE_TYPE;
}
/**
* Gets the panel that allows a user to select a data source and do any
* configuration required by the data source. The panel is less than 544
* pixels wide and less than 173 pixels high.
*
* @return A selection and configuration panel for this data source
* processor.
*/
@Override
public JPanel getPanel() {
configPanel.readSettings();
configPanel.select();
return configPanel;
}
/**
* Indicates whether the settings in the selection and configuration panel
* are valid and complete.
*
* @return True if the settings are valid and complete and the processor is
* ready to have its run method called, false otherwise.
*/
@Override
public boolean isPanelValid() {
return configPanel.validatePanel();
}
/**
* Adds a data source to the case database using a background task in a
* separate thread and the settings provided by the selection and
* configuration panel. Returns as soon as the background task is started.
* The background task uses a callback object to signal task completion and
* return results.
*
* This method should not be called unless isPanelValid returns true.
*
* @param progressMonitor Progress monitor that will be used by the
* background task to report progress.
* @param callback Callback that will be used by the background task
* to return results.
*/
@Override
public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
configPanel.storeSettings();
run(UUID.randomUUID().toString(), configPanel.getContentPaths(), configPanel.getTimeZone(), progressMonitor, callback);
}
/**
* Adds a data source to the case database using a background task in a
* separate thread and the given settings instead of those provided by the
* selection and configuration panel. Returns as soon as the background task
* is started and uses the callback object to signal task completion and
* return results.
*
* This method should not be called unless isPanelValid returns true.
*
* @param progressMonitor Progress monitor that will be used by the
* background task to report progress.
* @param callback Callback that will be used by the background task
* to return results.
*/
public void run(String deviceId, String imageFolderPath, String timeZone, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
List<String> imageFilePaths = getImageFilePaths(imageFolderPath);
addImagesTask = new AddCellebritePhysicalReportTask(deviceId, imageFilePaths, timeZone, progressMonitor, callback);
new Thread(addImagesTask).start();
}
/**
* Requests cancellation of the background task that adds a data source to
* the case database, after the task is started using the run method. This
* is a "best effort" cancellation, with no guarantees that the case
* database will be unchanged. If cancellation succeeded, the list of new
* data sources returned by the background task will be empty.
*/
@Override
public void cancel() {
if (null != addImagesTask) {
addImagesTask.cancelTask();
}
}
/**
* Resets the selection and configuration panel for this data source
* processor.
*/
@Override
public void reset() {
configPanel.reset();
}
/**
* Gets the paths of the image files in a Cellebrite UFED output folder.
*
* @param folderPath The path to a Cellebrite UFED output folder
*
* @return A list of image file paths.
*/
private static List<String> getImageFilePaths(String folderPath) {
List<String> imageFilePaths = new ArrayList<>();
File folder = new File(folderPath);
File[] listOfFiles = folder.listFiles();
for (File file : listOfFiles) {
if (file.isFile() && isValidDataSource(file.toPath())){
Path filePathName = Paths.get(folderPath, file.getName());
imageFilePaths.add(filePathName.toString());
}
}
return imageFilePaths;
}
/**
* Extracts the contents of a ZIP archive submitted as a data source to a
* subdirectory of the auto ingest module output directory.
*
* @throws IOException if there is a problem extracting the data source from
* the archive.
*/
private static Path extractDataSource(Path outputDirectoryPath, Path dataSourcePath) throws IOException {
String dataSourceFileNameNoExt = FilenameUtils.removeExtension(dataSourcePath.getFileName().toString());
Path destinationFolder = Paths.get(outputDirectoryPath.toString(),
AUTO_INGEST_MODULE_OUTPUT_DIR,
dataSourceFileNameNoExt + "_" + TimeStampUtils.createTimeStamp());
Files.createDirectories(destinationFolder);
int BUFFER_SIZE = 524288; // Read/write 500KB at a time
File sourceZipFile = dataSourcePath.toFile();
ZipFile zipFile;
zipFile = new ZipFile(sourceZipFile, ZipFile.OPEN_READ);
Enumeration<? extends ZipEntry> zipFileEntries = zipFile.entries();
try {
while (zipFileEntries.hasMoreElements()) {
ZipEntry entry = zipFileEntries.nextElement();
String currentEntry = entry.getName();
File destFile = new File(destinationFolder.toString(), currentEntry);
destFile = new File(destinationFolder.toString(), destFile.getName());
File destinationParent = destFile.getParentFile();
destinationParent.mkdirs();
if (!entry.isDirectory()) {
BufferedInputStream is = new BufferedInputStream(zipFile.getInputStream(entry));
int currentByte;
byte data[] = new byte[BUFFER_SIZE];
try (FileOutputStream fos = new FileOutputStream(destFile); BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER_SIZE)) {
currentByte = is.read(data, 0, BUFFER_SIZE);
while (currentByte != -1) {
dest.write(data, 0, currentByte);
currentByte = is.read(data, 0, BUFFER_SIZE);
}
}
}
}
} finally {
zipFile.close();
}
return destinationFolder;
}
private static boolean isValidDataSource(Path dataSourcePath) {
String fileName = dataSourcePath.getFileName().toString();
// is it a ".bin" image
if (!isAcceptedByFiler(new File(fileName), filtersList)) {
return false;
}
// this needs to identify and handle different Cellebrite scenarios:
// i single image in a single file
// ii. Single image split over multiple files - just need to pass the first to TSK and it will combine the split image files.
// Note there may be more than than one split images in a single dir,
// e.g. blk0_mmcblk0.bin, blk0_mmcblk0(1).bin......, and blk24_mmcblk1.bin, blk24_mmcblk1(1).bin......
//iii. Multiple image files - one per volume - need to handle each one separately
// e.g. blk0_mmcblk0.bin, mtd0_system.bin, mtd1_cache.bin, mtd2_userdata.bin
String fNameNoExt = FilenameUtils.removeExtension(fileName);
return (! fNameNoExt.toLowerCase().matches("\\w+\\(\\d+\\)"));
}
private static boolean isAcceptedByFiler(File file, List<FileFilter> filters) {
for (FileFilter filter : filters) {
if (filter.accept(file)) {
return true;
}
}
return false;
}
private static boolean isArchive(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException {
String fileName = dataSourcePath.getFileName().toString();
// check whether it's a zip archive file that can be extracted
if (isAcceptedByFiler(new File(fileName), archiveFilters)) {
return true;
}
return false;
}
@Override
public int canProcess(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException {
// check whether this is an archive or a ".bin" file
if (isArchive(dataSourcePath) || isValidDataSource(dataSourcePath)) {
// return "high confidence" value
return 90;
}
return 0;
}
@Override
public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) throws AutomatedIngestDataSourceProcessorException {
List<String> dataSourcePathList = Collections.emptyList();
if (isArchive(dataSourcePath)) {
// extract the archive and pass the extracted folder as input
Path extractedDataSourcePath = Paths.get("");
try {
Case currentCase = Case.getCurrentCase();
extractedDataSourcePath = extractDataSource(Paths.get(currentCase.getModuleDirectory()), dataSourcePath);
} catch (Exception ex) {
throw new AutomatedIngestDataSourceProcessorException(NbBundle.getMessage(CellebritePhysicalReportProcessor.class, "CellebritePhysicalReportProcessor.process.exception.text"), ex);
}
run(deviceId, extractedDataSourcePath.toString(), "", progressMonitor, callBack);
} else if (isValidDataSource(dataSourcePath)) {
// pass the single ".bin" file as input
dataSourcePathList = Arrays.asList(new String[]{dataSourcePath.toString()});
// in this particular case we don't want to call run() method as it will try to identify and process all ".bin" files in data source folder
addImagesTask = new AddCellebritePhysicalReportTask(deviceId, dataSourcePathList, "", progressMonitor, callBack);
new Thread(addImagesTask).start();
}
}
}

View File

@ -1,132 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.cellex.datasourceprocessors;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import static java.lang.Math.min;
import java.util.Collection;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException;
/**
*
* @author flynn
*/
public class Util {
public interface Mapper<T1, T2> {
public T2 apply(T1 x);
}
public static void copyToFile(AbstractFile inputFile, String destPath)
throws IOException, TskCoreException {
long inputSize = inputFile.getSize();
long bufSize = min(inputSize, 65536L);
byte[] buffer = new byte[(int) bufSize];
FileOutputStream output = new FileOutputStream(destPath);
long offset = 0;
long bytesLeft = inputSize;
try {
while (bytesLeft > 0) {
int bytesRead = inputFile.read(buffer, offset, bufSize);
if (bytesRead < 0) {
throw new IOException("I/O error (rc " + bytesRead + ")");
}
output.write(buffer);
bytesLeft -= bytesRead;
offset += bytesRead;
}
} finally {
// Always always always close output, no matter what.
output.close();
}
}
public static String getBasename(String path) {
String baseName = path;
int lastSlash = baseName.lastIndexOf(File.separator);
if (lastSlash >= 0) {
baseName = baseName.substring(lastSlash + 1);
}
return baseName;
}
public static String stripExtension(String path) {
String noExt = path;
int lastPeriod = path.lastIndexOf('.');
if (lastPeriod > 0) { // Not >=, >. A single "." should be preserved.
noExt = path.substring(0, lastPeriod);
}
return noExt;
}
public static String joinPath(String... elements) {
return join(File.separator, elements);
}
public static String join(String delim, Mapper<String, String> mapFunc,
String... elements) {
String joined = "";
for (String element : elements) {
if (mapFunc != null) {
element = mapFunc.apply(element);
}
if ((element != null) && (element.length() > 0)) {
if (joined.length() > 0) {
joined += delim;
}
joined += element;
}
}
return joined;
}
public static String join(String delim, Mapper<String, String> mapFunc,
Collection<String> elements) {
return join(delim, mapFunc,
elements.toArray(new String[elements.size()]));
}
public static String join(String delim, String... elements) {
return join(delim, null, elements);
}
public static String join(String delim, Collection<String> elements) {
return join(delim, null, elements);
}
}