/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE file at the root of the source
* tree and available online at
*
* https://github.com/keeps/roda
*/
package org.roda.core.plugins.plugins.common;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.xmlbeans.XmlException;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.iterables.CloseableIterable;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.common.RodaConstants.PreservationEventType;
import org.roda.core.data.exceptions.AlreadyExistsException;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.InvalidParameterException;
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RODAException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.v2.IsRODAObject;
import org.roda.core.data.v2.common.OptionalWithCause;
import org.roda.core.data.v2.ip.AIP;
import org.roda.core.data.v2.ip.AIPState;
import org.roda.core.data.v2.ip.DIP;
import org.roda.core.data.v2.ip.DIPFile;
import org.roda.core.data.v2.ip.File;
import org.roda.core.data.v2.ip.FileLink;
import org.roda.core.data.v2.ip.IndexedFile;
import org.roda.core.data.v2.ip.Representation;
import org.roda.core.data.v2.ip.StoragePath;
import org.roda.core.data.v2.ip.metadata.LinkingIdentifier;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.PluginParameter;
import org.roda.core.data.v2.jobs.PluginParameter.PluginParameterType;
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.data.v2.jobs.Report.PluginState;
import org.roda.core.data.v2.validation.ValidationException;
import org.roda.core.data.v2.validation.ValidationIssue;
import org.roda.core.data.v2.validation.ValidationReport;
import org.roda.core.index.IndexService;
import org.roda.core.model.ModelService;
import org.roda.core.model.utils.ModelUtils;
import org.roda.core.plugins.AbstractAIPComponentsPlugin;
import org.roda.core.plugins.PluginException;
import org.roda.core.plugins.orchestrate.SimpleJobPluginInfo;
import org.roda.core.plugins.plugins.PluginHelper;
import org.roda.core.plugins.plugins.characterization.PremisSkeletonPluginUtils;
import org.roda.core.storage.Binary;
import org.roda.core.storage.ContentPayload;
import org.roda.core.storage.DirectResourceAccess;
import org.roda.core.storage.StorageService;
import org.roda.core.storage.fs.FSPathContentPayload;
import org.roda.core.util.CommandException;
import org.roda.core.util.IdUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractConvertPlugin<T extends IsRODAObject> extends AbstractAIPComponentsPlugin<T> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractConvertPlugin.class);
private String inputFormat;
private String outputFormat;
private boolean ignoreFiles = true;
private boolean createDIP = false;
private boolean hasPartialSuccessOnOutcome = false;
private String dipTitle = "";
private String dipDescription = "";
private static Map<String, PluginParameter> pluginParameters = new HashMap<>();
static {
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_INPUT_FORMAT,
new PluginParameter(RodaConstants.PLUGIN_PARAMS_INPUT_FORMAT, "Input format", PluginParameterType.STRING, "",
true, false,
"Input file format to be converted (check documentation for list of supported formats). If the input file format is not specified, the task will"
+ " run on all supported formats (check roda-core-formats.properties for list of supported formats)."));
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_OUTPUT_FORMAT,
new PluginParameter(RodaConstants.PLUGIN_PARAMS_OUTPUT_FORMAT, "Output format", PluginParameterType.STRING, "",
true, false, "Output file format to be converted (check documentation for list of supported formats)."));
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES,
new PluginParameter(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES, "Ignore other files",
PluginParameterType.BOOLEAN, "true", false, false,
"Do not process files that have a different format from the indicated."));
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP, new PluginParameter(
RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP, "Create dissemination", PluginParameterType.BOOLEAN, "true",
false, false,
"If this is selected then the plugin will convert the files and create a new dissemination. If not, a new representation will be created."));
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_TITLE,
new PluginParameter(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_TITLE, "Dissemination title",
PluginParameterType.STRING, "Dissemination title", false, false,
"If the 'create dissemination' option is checked, then this will be the respective dissemination title."));
pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_DESCRIPTION, new PluginParameter(
RodaConstants.PLUGIN_PARAMS_DISSEMINATION_DESCRIPTION, "Dissemination description", PluginParameterType.STRING,
"Dissemination description", false, false,
"If the 'create dissemination' option is checked, then this will be the respective dissemination description."));
}
protected AbstractConvertPlugin() {
super();
inputFormat = "";
outputFormat = "";
}
protected Map<String, PluginParameter> getDefaultParameters() {
return pluginParameters.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(), e -> new PluginParameter(e.getValue())));
}
protected List<PluginParameter> orderParameters(Map<String, PluginParameter> params) {
List<PluginParameter> orderedList = new ArrayList<>();
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_INPUT_FORMAT));
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_OUTPUT_FORMAT));
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES));
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP));
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_TITLE));
orderedList.add(params.get(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_DESCRIPTION));
return orderedList;
}
@Override
public void init() throws PluginException {
// do nothing
}
@Override
public void shutdown() {
// do nothing
}
public boolean hasPartialSuccessOnOutcome() {
return Boolean.parseBoolean(RodaCoreFactory.getRodaConfigurationAsString("core", "tools", "convert", "allplugins",
"hasPartialSuccessOnOutcome"));
}
public abstract List<String> getApplicableTo();
public abstract List<String> getConvertableTo();
public abstract Map<String, List<String>> getPronomToExtension();
public abstract Map<String, List<String>> getMimetypeToExtension();
public String getInputFormat() {
return this.inputFormat;
}
public String getOutputFormat() {
return this.outputFormat;
}
public void setInputFormat(String format) {
this.inputFormat = format;
}
public void setOutputFormat(String format) {
this.outputFormat = format;
}
@Override
public PluginType getType() {
return PluginType.AIP_TO_AIP;
}
@Override
public boolean areParameterValuesValid() {
return true;
}
@Override
public List<PluginParameter> getParameters() {
return orderParameters(getDefaultParameters());
}
@Override
public void setParameterValues(Map<String, String> parameters) throws InvalidParameterException {
super.setParameterValues(parameters);
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_INPUT_FORMAT)) {
setInputFormat(parameters.get(RodaConstants.PLUGIN_PARAMS_INPUT_FORMAT));
}
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_OUTPUT_FORMAT)) {
setOutputFormat(parameters.get(RodaConstants.PLUGIN_PARAMS_OUTPUT_FORMAT));
}
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES)) {
ignoreFiles = Boolean.parseBoolean(parameters.get(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES));
}
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP)) {
createDIP = Boolean.parseBoolean(parameters.get(RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP));
}
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_TITLE)) {
dipTitle = parameters.get(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_TITLE);
}
if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_DESCRIPTION)) {
dipDescription = parameters.get(RodaConstants.PLUGIN_PARAMS_DISSEMINATION_DESCRIPTION);
}
hasPartialSuccessOnOutcome = Boolean.parseBoolean(RodaCoreFactory.getRodaConfigurationAsString("core", "tools",
"convert", "allplugins", "hasPartialSuccessOnOutcome"));
}
@Override
protected Report executeOnAIP(IndexService index, ModelService model, StorageService storage, Report report,
SimpleJobPluginInfo jobPluginInfo, List<AIP> list, Job job) throws PluginException {
for (AIP aip : list) {
LOGGER.debug("Processing AIP {}", aip.getId());
List<String> newRepresentations = new ArrayList<>();
String newRepresentationID = null;
boolean notify = true;
PluginState reportState = PluginState.SUCCESS;
ValidationReport validationReport = new ValidationReport();
boolean hasNonPdfFiles = false;
List<File> alteredFiles = new ArrayList<>();
List<File> newFiles = new ArrayList<>();
List<DIPFile> newDIPFiles = new ArrayList<>();
List<File> unchangedFiles = new ArrayList<>();
for (Representation representation : aip.getRepresentations()) {
newRepresentationID = IdUtils.createUUID();
PluginState pluginResultState = PluginState.SUCCESS;
Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getRepresentationId(representation),
IdUtils.getRepresentationId(representation), Representation.class, AIPState.ACTIVE);
if (createDIP) {
reportItem.setOutcomeObjectClass(DIP.class.getName());
}
try {
LOGGER.debug("Processing representation {}", representation);
boolean recursive = true;
CloseableIterable<OptionalWithCause<File>> allFiles = model.listFilesUnder(aip.getId(),
representation.getId(), recursive);
for (OptionalWithCause<File> oFile : allFiles) {
if (oFile.isPresent()) {
File file = oFile.get();
LOGGER.debug("Processing file {}", file);
if (!file.isDirectory()) {
IndexedFile ifile = index.retrieve(IndexedFile.class, IdUtils.getFileId(file),
RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN);
String fileMimetype = ifile.getFileFormat().getMimeType();
String filePronom = ifile.getFileFormat().getPronom();
String fileFormat = ifile.getId().substring(ifile.getId().lastIndexOf('.') + 1, ifile.getId().length());
List<String> applicableTo = getApplicableTo();
List<String> convertableTo = getConvertableTo();
Map<String, List<String>> pronomToExtension = getPronomToExtension();
Map<String, List<String>> mimetypeToExtension = getMimetypeToExtension();
if (doPluginExecute(fileFormat, filePronom, fileMimetype, applicableTo, convertableTo,
pronomToExtension, mimetypeToExtension)) {
fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype, applicableTo, pronomToExtension,
mimetypeToExtension);
StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file);
DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath);
LOGGER.debug("Running a ConvertPlugin ({} to {}) on {}", fileFormat, outputFormat, file.getId());
try {
Path pluginResult = Files.createTempFile("converted", "." + getOutputFormat());
String result = executePlugin(directAccess.getPath(), pluginResult, fileFormat);
String newFileId = file.getId().replaceFirst("[.][^.]+$", "." + outputFormat);
ContentPayload payload = new FSPathContentPayload(pluginResult);
if (createDIP) {
FileLink fileLink = new FileLink(file.getAipId(), file.getRepresentationId(), file.getPath(),
file.getId());
List<FileLink> links = new ArrayList<>();
links.add(fileLink);
DIP dip = new DIP();
dip.setId(IdUtils.createUUID());
dip.setFileIds(links);
dip.setPermissions(aip.getPermissions());
dip.setTitle(dipTitle);
dip.setDescription(dipDescription);
dip.setType(RodaConstants.DIP_TYPE_CONVERSION);
dip = model.createDIP(dip, true);
newRepresentationID = dip.getId();
DIPFile f = model.createDIPFile(newRepresentationID, file.getPath(), newFileId,
directAccess.getPath().toFile().length(), payload, notify);
newDIPFiles.add(f);
} else {
// create a new representation if it does not exist
if (!newRepresentations.contains(newRepresentationID)) {
LOGGER.debug("Creating a new representation {} on AIP {}", newRepresentationID, aip.getId());
boolean original = false;
newRepresentations.add(newRepresentationID);
String newRepresentationType = representation.getType();
model.createRepresentation(aip.getId(), newRepresentationID, original, newRepresentationType,
notify);
reportItem.setOutcomeObjectId(
IdUtils.getRepresentationId(representation.getAipId(), newRepresentationID));
}
File f = model.createFile(aip.getId(), newRepresentationID, file.getPath(), newFileId, payload,
notify);
newFiles.add(f);
}
alteredFiles.add(file);
IOUtils.closeQuietly(directAccess);
Report fileReportItem = PluginHelper.initPluginReportItem(this, file.getId(), File.class,
AIPState.ACTIVE);
fileReportItem.setPluginState(pluginResultState).setPluginDetails(result);
reportItem.addReport(fileReportItem);
} catch (CommandException e) {
pluginResultState = PluginState.PARTIAL_SUCCESS;
reportState = pluginResultState;
reportItem.setPluginState(pluginResultState)
.addPluginDetails(e.getMessage() + "\n" + e.getOutput() + "\n");
LOGGER.debug("Conversion ({} to {}) failed on file {} of representation {} from AIP {}", fileFormat,
outputFormat, file.getId(), representation.getId(), aip.getId());
}
} else {
unchangedFiles.add(file);
if (ignoreFiles) {
validationReport.addIssue(new ValidationIssue(ModelUtils.getFileStoragePath(file).toString()));
} else {
pluginResultState = PluginState.FAILURE;
reportState = pluginResultState;
hasNonPdfFiles = true;
}
}
}
} else {
LOGGER.error("Cannot process AIP representation file", oFile.getCause());
}
}
IOUtils.closeQuietly(allFiles);
reportItem.setPluginState(pluginResultState);
if (reportState.equals(PluginState.SUCCESS)) {
if (ignoreFiles && !validationReport.getIssues().isEmpty()) {
reportItem.setHtmlPluginDetails(true)
.setPluginDetails(validationReport.toHtml(false, false, false, "Ignored files"));
}
}
if (hasNonPdfFiles) {
reportItem.setPluginDetails("Certain files were not ignored");
}
// add unchanged files to the new representation if created
if (!alteredFiles.isEmpty() && !createDIP) {
createNewFilesOnRepresentation(storage, model, unchangedFiles, newRepresentationID, notify);
}
} catch (RuntimeException | NotFoundException | GenericException | RequestNotValidException
| AuthorizationDeniedException | IOException | AlreadyExistsException e) {
LOGGER.error("Error processing AIP " + aip.getId() + ": " + e.getMessage(), e);
pluginResultState = PluginState.FAILURE;
reportState = pluginResultState;
reportItem.setPluginState(pluginResultState).setPluginDetails(e.getMessage());
} finally {
report.addReport(reportItem);
PluginHelper.updatePartialJobReport(this, model, reportItem, true, job);
if (!createDIP) {
try {
Representation rep = model.retrieveRepresentation(aip.getId(), newRepresentationID);
createPremisSkeletonOnRepresentation(model, aip.getId(), rep);
} catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException
| ValidationException | IOException | XmlException e) {
LOGGER.error("Error running premis skeleton on new representation: {}", e.getMessage());
}
}
}
}
try {
LOGGER.debug("Creating convert plugin event for the AIP {}", aip.getId());
boolean notifyEvent = false;
createEvent(model, index, aip.getId(), null, null, null, outputFormat, reportState, alteredFiles, newFiles,
notifyEvent);
model.notifyAipUpdated(aip.getId());
jobPluginInfo.incrementObjectsProcessed(reportState);
} catch (PluginException | RequestNotValidException | GenericException | NotFoundException
| AuthorizationDeniedException e) {
LOGGER.debug("Error on update AIP notify");
}
}
return report;
}
@Override
protected Report executeOnRepresentation(IndexService index, ModelService model, StorageService storage,
Report report, SimpleJobPluginInfo jobPluginInfo, List<Representation> list, Job job) throws PluginException {
List<String> newRepresentations = new ArrayList<>();
String aipId = null;
for (Representation representation : list) {
List<File> unchangedFiles = new ArrayList<>();
String newRepresentationID = IdUtils.createUUID();
List<File> alteredFiles = new ArrayList<>();
List<File> newFiles = new ArrayList<>();
List<DIPFile> newDIPFiles = new ArrayList<>();
aipId = representation.getAipId();
PluginState reportState = PluginState.SUCCESS;
boolean notify = true;
Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getRepresentationId(representation),
Representation.class, AIPState.ACTIVE);
if (createDIP) {
reportItem.setOutcomeObjectClass(DIP.class.getName());
}
ValidationReport validationReport = new ValidationReport();
boolean hasNonPdfFiles = false;
try {
LOGGER.debug("Processing representation {}", representation);
boolean recursive = true;
CloseableIterable<OptionalWithCause<File>> allFiles = model.listFilesUnder(representation.getAipId(),
representation.getId(), recursive);
for (OptionalWithCause<File> oFile : allFiles) {
if (oFile.isPresent()) {
File file = oFile.get();
LOGGER.debug("Processing file {}", file);
if (!file.isDirectory()) {
IndexedFile ifile = index.retrieve(IndexedFile.class, IdUtils.getFileId(file),
RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN);
String fileMimetype = ifile.getFileFormat().getMimeType();
String filePronom = ifile.getFileFormat().getPronom();
String fileFormat = ifile.getId().substring(ifile.getId().lastIndexOf('.') + 1);
List<String> applicableTo = getApplicableTo();
List<String> convertableTo = getConvertableTo();
Map<String, List<String>> pronomToExtension = getPronomToExtension();
Map<String, List<String>> mimetypeToExtension = getMimetypeToExtension();
if (doPluginExecute(fileFormat, filePronom, fileMimetype, applicableTo, convertableTo, pronomToExtension,
mimetypeToExtension)) {
fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype, applicableTo, pronomToExtension,
mimetypeToExtension);
StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file);
DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath);
LOGGER.debug("Running a ConvertPlugin ({} to {}) on {}", fileFormat, outputFormat, file.getId());
try {
Path pluginResult = Files.createTempFile("converted", "." + getOutputFormat());
String result = executePlugin(directAccess.getPath(), pluginResult, fileFormat);
ContentPayload payload = new FSPathContentPayload(pluginResult);
if (!newRepresentations.contains(newRepresentationID)) {
LOGGER.debug("Creating a new representation {} on AIP {}", newRepresentationID, aipId);
boolean original = false;
newRepresentations.add(newRepresentationID);
if (createDIP) {
FileLink fileLink = new FileLink(file.getAipId(), file.getRepresentationId(), file.getPath(),
file.getId());
List<FileLink> links = new ArrayList<>();
links.add(fileLink);
AIP aip = model.retrieveAIP(aipId);
DIP dip = new DIP();
dip.setId(IdUtils.createUUID());
dip.setFileIds(links);
dip.setPermissions(aip.getPermissions());
dip.setTitle(dipTitle);
dip.setDescription(dipDescription);
dip.setType(RodaConstants.DIP_TYPE_CONVERSION);
dip = model.createDIP(dip, true);
newRepresentationID = dip.getId();
} else {
// INFO will be a parameter
String newRepresentationType = RodaConstants.REPRESENTATION_TYPE_MIXED;
model.createRepresentation(aipId, newRepresentationID, original, newRepresentationType, notify);
reportItem.setOutcomeObjectId(
IdUtils.getRepresentationId(representation.getAipId(), newRepresentationID));
}
}
String newFileId = file.getId().replaceFirst("[.][^.]+$", "." + outputFormat);
if (createDIP) {
DIPFile f = model.createDIPFile(newRepresentationID, file.getPath(), newFileId,
directAccess.getPath().toFile().length(), payload, notify);
newDIPFiles.add(f);
} else {
File newFile = model.createFile(aipId, newRepresentationID, file.getPath(), newFileId, payload,
notify);
newFiles.add(newFile);
}
alteredFiles.add(file);
IOUtils.closeQuietly(directAccess);
Report fileReportItem = PluginHelper.initPluginReportItem(this, file.getId(), File.class,
AIPState.ACTIVE);
fileReportItem.setPluginState(PluginState.SUCCESS).setPluginDetails(result);
reportItem.addReport(fileReportItem);
} catch (CommandException e) {
reportState = PluginState.PARTIAL_SUCCESS;
reportItem.setPluginState(reportState).addPluginDetails(e.getMessage() + "\n" + e.getOutput() + "\n");
LOGGER.debug("Conversion ({} to {}) failed on file {} of representation {} from AIP {}", fileFormat,
outputFormat, file.getId(), representation.getId(), representation.getAipId());
}
} else {
unchangedFiles.add(file);
if (ignoreFiles) {
validationReport.addIssue(new ValidationIssue(file.getId()));
} else {
reportState = PluginState.FAILURE;
hasNonPdfFiles = true;
}
}
}
} else {
LOGGER.error("Cannot process AIP representation file", oFile.getCause());
}
}
IOUtils.closeQuietly(allFiles);
reportItem.setPluginState(reportState);
if (reportState.equals(PluginState.SUCCESS) && ignoreFiles && !validationReport.getIssues().isEmpty()) {
reportItem.setHtmlPluginDetails(true)
.setPluginDetails(validationReport.toHtml(false, false, false, "Ignored files"));
}
if (hasNonPdfFiles) {
reportItem.setPluginDetails("Certain files were not ignored");
}
report.addReport(reportItem);
PluginHelper.updatePartialJobReport(this, model, reportItem, true, job);
// add unchanged files to the new representation
if (!alteredFiles.isEmpty()) {
if (createDIP) {
createNewFilesOnDIP(storage, model, unchangedFiles, newRepresentationID, notify);
} else {
createNewFilesOnRepresentation(storage, model, unchangedFiles, newRepresentationID, notify);
}
}
} catch (RuntimeException | NotFoundException | GenericException | RequestNotValidException
| AuthorizationDeniedException | IOException | AlreadyExistsException e) {
LOGGER.error("Error processing Representation {}: {}", representation.getId(), e.getMessage(), e);
reportState = PluginState.FAILURE;
reportItem.setPluginState(PluginState.FAILURE).setPluginDetails(e.getMessage());
report.addReport(reportItem);
}
jobPluginInfo.incrementObjectsProcessed(reportState);
LOGGER.debug("Creating convert plugin event for the representation " + representation.getId());
boolean notifyEvent = false;
createEvent(model, index, aipId, representation.getId(), null, null, outputFormat, reportState, alteredFiles,
newFiles, notifyEvent);
if (!createDIP) {
try {
Representation rep = model.retrieveRepresentation(representation.getAipId(), newRepresentationID);
createPremisSkeletonOnRepresentation(model, representation.getAipId(), rep);
} catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException
| ValidationException | IOException | XmlException e) {
LOGGER.error("Error running premis skeleton on new representation: {}", e.getMessage());
}
}
}
try {
model.notifyAipUpdated(aipId);
} catch (RODAException e) {
LOGGER.error("Error running creating agent for AbstractConvertPlugin", e);
}
return report;
}
@Override
protected Report executeOnFile(IndexService index, ModelService model, StorageService storage, Report report,
SimpleJobPluginInfo jobPluginInfo, List<File> list, Job job) throws PluginException {
Map<String, String> changedRepresentationsOnAIPs = new HashMap<>();
boolean notify = true;
String newRepresentationID = null;
String newFileId = null;
ArrayList<File> newFiles = new ArrayList<>();
ArrayList<DIPFile> newDIPFiles = new ArrayList<>();
Report reportItem = null;
PluginState reportState = PluginState.SUCCESS;
PluginState pluginResultState = PluginState.SUCCESS;
for (File file : list) {
try {
LOGGER.debug("Processing file {}", file.getId());
newRepresentationID = IdUtils.createUUID();
pluginResultState = PluginState.SUCCESS;
reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getFileId(file), File.class, AIPState.ACTIVE);
if (createDIP) {
reportItem.setOutcomeObjectClass(DIP.class.getName());
}
if (!file.isDirectory()) {
IndexedFile ifile = index.retrieve(IndexedFile.class, IdUtils.getFileId(file),
RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN);
String fileMimetype = ifile.getFileFormat().getMimeType();
String filePronom = ifile.getFileFormat().getPronom();
String fileFormat = ifile.getId().substring(ifile.getId().lastIndexOf('.') + 1);
List<String> applicableTo = getApplicableTo();
List<String> convertableTo = getConvertableTo();
Map<String, List<String>> pronomToExtension = getPronomToExtension();
Map<String, List<String>> mimetypeToExtension = getMimetypeToExtension();
if (doPluginExecute(fileFormat, filePronom, fileMimetype, applicableTo, convertableTo, pronomToExtension,
mimetypeToExtension)) {
fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype, applicableTo, pronomToExtension,
mimetypeToExtension);
StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file);
DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath);
LOGGER.debug("Running a ConvertPlugin ({} to {}) on {}", fileFormat, outputFormat, file.getId());
try {
Path pluginResult = Files.createTempFile("converted", "." + getOutputFormat());
String result = executePlugin(directAccess.getPath(), pluginResult, fileFormat);
ContentPayload payload = new FSPathContentPayload(pluginResult);
StoragePath storagePath = ModelUtils.getRepresentationStoragePath(file.getAipId(),
file.getRepresentationId());
// create a new representation if it does not exist
LOGGER.debug("Creating a new representation {} on AIP {}", newRepresentationID, file.getAipId());
boolean original = false;
if (createDIP) {
FileLink fileLink = new FileLink(file.getAipId(), file.getRepresentationId(), file.getPath(),
file.getId());
List<FileLink> links = new ArrayList<>();
links.add(fileLink);
AIP aip = model.retrieveAIP(file.getAipId());
DIP dip = new DIP();
dip.setId(IdUtils.createUUID());
dip.setFileIds(links);
dip.setPermissions(aip.getPermissions());
dip.setTitle(dipTitle);
dip.setDescription(dipDescription);
dip.setType(RodaConstants.DIP_TYPE_CONVERSION);
dip = model.createDIP(dip, true);
newRepresentationID = dip.getId();
} else {
// INFO will be a parameter
String newRepresentationType = RodaConstants.REPRESENTATION_TYPE_MIXED;
model.createRepresentation(file.getAipId(), newRepresentationID, original, newRepresentationType,
model.getStorage(), storagePath, true);
}
// update file on new representation
newFileId = file.getId().replaceFirst("[.][^.]+$", "." + outputFormat);
if (createDIP) {
DIPFile f = model.createDIPFile(newRepresentationID, file.getPath(), newFileId,
directAccess.getPath().toFile().length(), payload, notify);
newDIPFiles.add(f);
} else {
model.deleteFile(file.getAipId(), newRepresentationID, file.getPath(), file.getId(), notify);
File f = model.createFile(file.getAipId(), newRepresentationID, file.getPath(), newFileId, payload,
notify);
newFiles.add(f);
reportItem.setOutcomeObjectId(IdUtils.getFileId(f));
changedRepresentationsOnAIPs.put(file.getRepresentationId(), file.getAipId());
}
Report fileReportItem = PluginHelper.initPluginReportItem(this, file.getId(), File.class,
AIPState.ACTIVE);
fileReportItem.setPluginState(PluginState.SUCCESS).setPluginDetails(result);
reportItem.addReport(fileReportItem);
} catch (CommandException e) {
pluginResultState = PluginState.PARTIAL_SUCCESS;
Report fileReportItem = PluginHelper.initPluginReportItem(this, file.getId(), File.class,
AIPState.ACTIVE);
fileReportItem.setPluginState(PluginState.PARTIAL_SUCCESS)
.setPluginDetails(e.getMessage() + "\n" + e.getOutput());
reportItem.addReport(fileReportItem);
LOGGER.debug("Conversion ({} to {}) failed on file {} of representation {} from AIP {}", fileFormat,
outputFormat, file.getId(), file.getRepresentationId(), file.getAipId());
}
} else {
if (ignoreFiles) {
reportItem.setPluginDetails("This file was ignored.");
} else {
pluginResultState = PluginState.FAILURE;
reportItem.setPluginDetails(
"This file was not ignored and it is not listed on the supported input file formats.");
}
}
}
if (!pluginResultState.equals(PluginState.SUCCESS)) {
reportState = PluginState.FAILURE;
jobPluginInfo.incrementObjectsProcessedWithFailure();
} else {
jobPluginInfo.incrementObjectsProcessedWithSuccess();
}
} catch (RuntimeException | NotFoundException | GenericException | RequestNotValidException
| AuthorizationDeniedException | ValidationException | IOException | AlreadyExistsException e) {
LOGGER.error("Error processing File {}: {}", file.getId(), e.getMessage(), e);
reportState = PluginState.FAILURE;
reportItem.setPluginDetails(e.getMessage());
jobPluginInfo.incrementObjectsProcessedWithFailure();
} finally {
reportItem.setPluginState(pluginResultState);
report.addReport(reportItem);
PluginHelper.updatePartialJobReport(this, model, reportItem, true, job);
}
boolean notifyEvent = false;
createEvent(model, index, file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId(), outputFormat,
reportState, Arrays.asList(file), newFiles, notifyEvent);
if (!createDIP) {
try {
Representation rep = model.retrieveRepresentation(file.getAipId(), newRepresentationID);
createPremisSkeletonOnRepresentation(model, file.getAipId(), rep);
} catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException
| ValidationException | IOException | XmlException e) {
LOGGER.error("Error running premis skeleton on new representation: {}", e.getMessage());
}
}
}
return report;
}
public abstract String executePlugin(Path inputPath, Path outputPath, String fileFormat)
throws UnsupportedOperationException, IOException, CommandException;
private void createPremisSkeletonOnRepresentation(ModelService model, String aipId, Representation representation)
throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
ValidationException, IOException, XmlException {
List<String> algorithms = RodaCoreFactory.getFixityAlgorithms();
PremisSkeletonPluginUtils.createPremisSkeletonOnRepresentation(model, aipId, representation.getId(), algorithms);
model.notifyRepresentationUpdated(representation);
}
private void createEvent(ModelService model, IndexService index, String aipId, String representationId,
List<String> filePath, String fileId, String outputFormat, PluginState outcome, List<File> alteredFiles,
List<File> newFiles, boolean notify) throws PluginException {
List<LinkingIdentifier> premisSourceFilesIdentifiers = new ArrayList<>();
List<LinkingIdentifier> premisTargetFilesIdentifiers = new ArrayList<>();
// building the detail for the plugin event
StringBuilder stringBuilder = new StringBuilder();
if (alteredFiles.isEmpty()) {
stringBuilder
.append("No file was successfully converted on this representation due to plugin or command line issues.");
} else {
for (File file : alteredFiles) {
premisSourceFilesIdentifiers.add(PluginHelper.getLinkingIdentifier(aipId, file.getRepresentationId(),
file.getPath(), file.getId(), RodaConstants.PRESERVATION_LINKING_OBJECT_SOURCE));
}
for (File file : newFiles) {
premisTargetFilesIdentifiers.add(PluginHelper.getLinkingIdentifier(aipId, file.getRepresentationId(),
file.getPath(), file.getId(), RodaConstants.PRESERVATION_LINKING_OBJECT_OUTCOME));
}
stringBuilder.append("The source files were converted to a new format (." + outputFormat + ")");
}
// Conversion plugin did not run correctly
if (PluginState.FAILURE.equals(outcome)
|| (outcome == PluginState.PARTIAL_SUCCESS && !hasPartialSuccessOnOutcome)) {
outcome = PluginState.FAILURE;
stringBuilder.setLength(0);
}
try {
PluginHelper.createPluginEvent(this, aipId, representationId, filePath, fileId, model, index,
premisSourceFilesIdentifiers, premisTargetFilesIdentifiers, outcome, stringBuilder.toString(), notify);
} catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException
| ValidationException | AlreadyExistsException e) {
throw new PluginException(e.getMessage(), e);
}
}
private boolean doPluginExecute(String fileFormat, String filePronom, String fileMimetype, List<String> applicableTo,
List<String> convertableTo, Map<String, List<String>> pronomToExtension,
Map<String, List<String>> mimetypeToExtension) {
String lowerCaseFileFormat = fileFormat == null ? null : fileFormat.toLowerCase();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Testing if input and output formats are correct: [{}, {}, {}, {}, {}, {}, {}]", lowerCaseFileFormat,
filePronom, fileMimetype, applicableTo, convertableTo, pronomToExtension, mimetypeToExtension);
}
boolean format = getInputFormat().isEmpty() || getInputFormat().equalsIgnoreCase(lowerCaseFileFormat);
boolean applicable = applicableTo.isEmpty() || (filePronom != null && pronomToExtension.containsKey(filePronom))
|| (fileMimetype != null && mimetypeToExtension.containsKey(fileMimetype))
|| (applicableTo.contains(lowerCaseFileFormat));
boolean convertable = convertableTo.isEmpty() || convertableTo.contains(outputFormat.toLowerCase());
LOGGER.debug("Input and ouput test results: format={} applicable={} convertable={}", format, applicable,
convertable);
return format && applicable && convertable;
}
private String getNewFileFormat(String fileFormat, String filePronom, String fileMimetype, List<String> applicableTo,
Map<String, List<String>> pronomToExtension, Map<String, List<String>> mimetypeToExtension) {
String newFileFormat = fileFormat;
if (!applicableTo.isEmpty()) {
if (filePronom != null && !filePronom.isEmpty() && pronomToExtension.get(filePronom) != null
&& !pronomToExtension.get(filePronom).contains(fileFormat)) {
newFileFormat = pronomToExtension.get(filePronom).get(0);
} else if (fileMimetype != null && !fileMimetype.isEmpty() && mimetypeToExtension.get(fileMimetype) != null
&& !mimetypeToExtension.get(fileMimetype).contains(fileFormat)) {
newFileFormat = mimetypeToExtension.get(fileMimetype).get(0);
}
}
return newFileFormat;
}
private void createNewFilesOnRepresentation(StorageService storage, ModelService model, List<File> unchangedFiles,
String newRepresentationID, boolean notify) throws RequestNotValidException, GenericException, NotFoundException,
AuthorizationDeniedException, UnsupportedOperationException, IOException, AlreadyExistsException {
for (File f : unchangedFiles) {
StoragePath fileStoragePath = ModelUtils.getFileStoragePath(f);
Binary binary = storage.getBinary(fileStoragePath);
Path uriPath = Paths.get(binary.getContent().getURI());
ContentPayload payload = new FSPathContentPayload(uriPath);
model.createFile(f.getAipId(), newRepresentationID, f.getPath(), f.getId(), payload, notify);
}
}
private void createNewFilesOnDIP(StorageService storage, ModelService model, List<File> unchangedFiles,
String newRepresentationID, boolean notify) throws RequestNotValidException, GenericException, NotFoundException,
AuthorizationDeniedException, UnsupportedOperationException, IOException, AlreadyExistsException {
for (File f : unchangedFiles) {
StoragePath fileStoragePath = ModelUtils.getFileStoragePath(f);
Binary binary = storage.getBinary(fileStoragePath);
Path uriPath = Paths.get(binary.getContent().getURI());
ContentPayload payload = new FSPathContentPayload(uriPath);
model.createDIPFile(newRepresentationID, f.getPath(), f.getId(), uriPath.toFile().length(), payload, notify);
}
}
@Override
public PreservationEventType getPreservationEventType() {
return PreservationEventType.MIGRATION;
}
@Override
public String getPreservationEventDescription() {
return "Converted, if possible, files to a new format (" + outputFormat + ").";
}
@Override
public String getPreservationEventSuccessMessage() {
return "Files were successfully converted to a new format.";
}
@Override
public String getPreservationEventFailureMessage() {
return "File conversion failed.";
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public List<Class<T>> getObjectClasses() {
List<Class<? extends IsRODAObject>> list = new ArrayList<>();
list.add(AIP.class);
list.add(Representation.class);
list.add(File.class);
return (List) list;
}
@Override
public List<String> getCategories() {
return Arrays.asList(RodaConstants.PLUGIN_CATEGORY_CONVERSION, RodaConstants.PLUGIN_CATEGORY_DISSEMINATION);
}
}