/** * 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.characterization; import java.io.IOException; 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.Map.Entry; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; 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.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.Plugin; 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.common.FileFormatUtils; 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.IdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DigitalSignaturePlugin<T extends IsRODAObject> extends AbstractAIPComponentsPlugin<T> { private static final Logger LOGGER = LoggerFactory.getLogger(DigitalSignaturePlugin.class); private boolean doVerify; private boolean doExtract; private boolean doStrip; private boolean verificationAffectsOnOutcome; private List<String> applicableTo; private Map<String, List<String>> pronomToExtension; private Map<String, List<String>> mimetypeToExtension; private boolean ignoreFiles = true; private boolean createDIP = false; private static Map<String, PluginParameter> pluginParameters = new HashMap<>(); static { pluginParameters.put(RodaConstants.PLUGIN_PARAMS_SIGNATURE_VERIFY, new PluginParameter(RodaConstants.PLUGIN_PARAMS_SIGNATURE_VERIFY, "Verify digital signature", PluginParameterType.BOOLEAN, "true", true, false, "Verifies the digital signature of files.")); pluginParameters.put(RodaConstants.PLUGIN_PARAMS_SIGNATURE_EXTRACT, new PluginParameter(RodaConstants.PLUGIN_PARAMS_SIGNATURE_EXTRACT, "Extract digital signature", PluginParameterType.BOOLEAN, "false", true, false, "Extracts the digital signature and stores it in the AIP under metadata/other folder.")); pluginParameters.put(RodaConstants.PLUGIN_PARAMS_SIGNATURE_STRIP, new PluginParameter(RodaConstants.PLUGIN_PARAMS_SIGNATURE_STRIP, "Strip digital signature", PluginParameterType.BOOLEAN, "true", true, false, "Removes the digital signature from the original file.")); pluginParameters.put(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES, new PluginParameter(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES, "Ignore non PDF files", PluginParameterType.BOOLEAN, "true", false, false, "Ignore files that are not recognised as PDF.")); 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 strip the file to a new dissemination. If not, a new representation will be created.")); } public DigitalSignaturePlugin() { super(); doVerify = true; doExtract = false; doStrip = false; verificationAffectsOnOutcome = Boolean.parseBoolean(RodaCoreFactory.getRodaConfigurationAsString("core", "tools", "digitalsignature", "verificationAffectsOnOutcome")); applicableTo = FileFormatUtils.getInputExtensions("digitalsignature"); pronomToExtension = FileFormatUtils.getPronomToExtension("digitalsignature"); mimetypeToExtension = FileFormatUtils.getMimetypeToExtension("digitalsignature"); } @Override public void init() throws PluginException { // do nothing } @Override public void shutdown() { // do nothing } @Override public String getName() { return "Digital signature validation"; } @Override public String getDescription() { return "Checks if digital signatures embedded in files are valid. \nThe task supports the following formats: PDF, " + "Microsoft Office Formats (.docx, .xslx, .pptx) and OpenDocument formats (.odt, .ods, .odp).\nThe outcome of this action is " + "three-fold: \n1) the outcome of verification is stored in a PREMIS event; \n2) the extracted digital signatures are stored within " + "the AIP under the “metadata/other” folder; and \n3) the files with the digital signature removed are stored under a new representation " + "in the Archival Information Package (AIP)."; } private String getDIPTitle() { return "Signature-stripped file"; } private String getDIPDescription() { return "Result file after stripping its digital signature"; } @Override public String getVersionImpl() { return "1.0"; } @Override public List<PluginParameter> getParameters() { ArrayList<PluginParameter> parameters = new ArrayList<>(); parameters.add(pluginParameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_VERIFY)); parameters.add(pluginParameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_EXTRACT)); parameters.add(pluginParameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_STRIP)); parameters.add(pluginParameters.get(RodaConstants.PLUGIN_PARAMS_REPRESENTATION_OR_DIP)); parameters.add(pluginParameters.get(RodaConstants.PLUGIN_PARAMS_IGNORE_OTHER_FILES)); return parameters; } @Override public void setParameterValues(Map<String, String> parameters) throws InvalidParameterException { super.setParameterValues(parameters); // do the digital signature verification if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_SIGNATURE_VERIFY)) { doVerify = Boolean.parseBoolean(parameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_VERIFY)); } // do the digital signature information extraction if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_SIGNATURE_EXTRACT)) { doExtract = Boolean.parseBoolean(parameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_EXTRACT)); } // do the digital signature strip if (parameters.containsKey(RodaConstants.PLUGIN_PARAMS_SIGNATURE_STRIP)) { doStrip = Boolean.parseBoolean(parameters.get(RodaConstants.PLUGIN_PARAMS_SIGNATURE_STRIP)); } 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)); } } @Override public Report executeOnAIP(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<AIP> list, Job job) throws PluginException { List<String> newRepresentations = new ArrayList<>(); for (AIP aip : list) { Report reportItem = PluginHelper.initPluginReportItem(this, aip.getId(), AIP.class, AIPState.INGEST_PROCESSING); PluginHelper.updatePartialJobReport(this, model, reportItem, false, job); PluginState reportState = PluginState.SUCCESS; ValidationReport validationReport = new ValidationReport(); boolean hasNonPdfFiles = false; Map<String, String> verifiedFiles = new HashMap<>(); List<File> alteredFiles = new ArrayList<>(); List<File> extractedFiles = new ArrayList<>(); List<File> newFiles = new ArrayList<>(); try { for (Representation representation : aip.getRepresentations()) { List<File> unchangedFiles = new ArrayList<>(); String newRepresentationID = IdUtils.createUUID(); String verification = null; boolean notify = true; 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); String fileInfoPath = ModelUtils .getFileStoragePath(aip.getId(), representation.getId(), file.getPath(), file.getId()).toString(); if (((filePronom != null && pronomToExtension.containsKey(filePronom)) || (fileMimetype != null && getMimetypeToExtension().containsKey(fileMimetype)) || (applicableTo.contains(fileFormat)))) { fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype); StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file); DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath); LOGGER.debug("Running DigitalSignaturePlugin on {}", file.getId()); if (doVerify) { LOGGER.debug("Verifying digital signatures on {}", file.getId()); verification = DigitalSignaturePluginUtils.runDigitalSignatureVerify(directAccess.getPath(), fileFormat, fileMimetype); verifiedFiles.put(file.getId(), verification); if (!"Passed".equals(verification) && verificationAffectsOnOutcome) { reportState = PluginState.FAILURE; reportItem.addPluginDetails(" Signature validation failed on " + fileInfoPath + ".\n"); } } if (doExtract) { LOGGER.debug("Extracting digital signatures information of {}", file.getId()); int extractResultSize = DigitalSignaturePluginUtils.runDigitalSignatureExtraction(model, file, directAccess.getPath(), fileFormat, fileMimetype); if (extractResultSize > 0) { extractedFiles.add(file); } } if (doStrip) { LOGGER.debug("Stripping digital signatures from {}", file.getId()); Path pluginResult = DigitalSignaturePluginUtils.runDigitalSignatureStrip(directAccess.getPath(), fileFormat, fileMimetype); if (pluginResult != null) { ContentPayload payload = new FSPathContentPayload(pluginResult); String newFileId = file.getId().replaceFirst("[.][^.]+$", "." + fileFormat); if (createDIP) { FileLink fileLink = new FileLink(representation.getAipId(), representation.getId(), 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(getDIPTitle()); dip.setDescription(getDIPDescription()); dip.setType(RodaConstants.DIP_TYPE_DIGITAL_SIGNATURE); dip = model.createDIP(dip, true); model.createDIPFile(dip.getId(), file.getPath(), newFileId, 0L, payload, notify); } else { if (!newRepresentations.contains(newRepresentationID)) { LOGGER.debug("Creating a new representation {} on AIP {}", newRepresentationID, aip.getId()); boolean original = false; newRepresentations.add(newRepresentationID); model.createRepresentation(aip.getId(), newRepresentationID, original, representation.getType(), notify); } // update file on new representation File f = model.createFile(aip.getId(), newRepresentationID, file.getPath(), newFileId, payload, notify); newFiles.add(f); } alteredFiles.add(file); } else { LOGGER.debug("Process failed on file {} of representation {} from AIP {}", file.getId(), representation.getId(), aip.getId()); reportState = PluginState.FAILURE; reportItem.addPluginDetails(" Signature validation stripping on " + fileInfoPath + "."); } } IOUtils.closeQuietly(directAccess); } else { unchangedFiles.add(file); if (ignoreFiles) { validationReport.addIssue(new ValidationIssue(fileInfoPath)); } else { reportState = PluginState.FAILURE; hasNonPdfFiles = true; } } } } else { LOGGER.error("Cannot process representation file", oFile.getCause()); } } IOUtils.closeQuietly(allFiles); // add unchanged files to the new representation if (!alteredFiles.isEmpty() && !createDIP) { 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); } } } jobPluginInfo.incrementObjectsProcessed(reportState); reportItem.setPluginState(reportState); if (!reportState.equals(PluginState.FAILURE)) { if (ignoreFiles && !validationReport.getIssues().isEmpty()) { reportItem.setHtmlPluginDetails(true) .setPluginDetails(validationReport.toHtml(false, false, false, "Ignored files")); } } if (hasNonPdfFiles) { reportItem.setPluginDetails("Non PDF files were not ignored"); } } catch (RODAException | IOException | RuntimeException e) { LOGGER.error("Error processing AIP " + aip.getId() + ": " + e.getMessage(), e); reportItem.setPluginState(PluginState.FAILURE).setPluginDetails(e.getMessage()); jobPluginInfo.incrementObjectsProcessedWithFailure(); } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); LOGGER.debug("Creating digital signature plugin event on AIP {}", aip.getId()); boolean notifyEvent = true; createEvent(model, index, aip.getId(), null, null, null, reportState, alteredFiles, extractedFiles, newFiles, verifiedFiles, notifyEvent); } } return report; } @Override public Report executeOnRepresentation(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<Representation> list, Job job) throws PluginException { List<String> newRepresentations = new ArrayList<>(); for (Representation representation : list) { String newRepresentationID = IdUtils.createUUID(); List<File> unchangedFiles = new ArrayList<>(); List<File> alteredFiles = new ArrayList<>(); List<File> extractedFiles = new ArrayList<>(); List<File> newFiles = new ArrayList<>(); Map<String, String> verifiedFiles = new HashMap<>(); String aipId = representation.getAipId(); String verification = null; boolean notify = true; Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getRepresentationId(representation), Representation.class); PluginHelper.updatePartialJobReport(this, model, reportItem, false, job); PluginState reportState = PluginState.SUCCESS; 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); String fileInfoPath = ModelUtils .getFileStoragePath(representation.getAipId(), representation.getId(), file.getPath(), file.getId()) .toString(); if (((filePronom != null && pronomToExtension.containsKey(filePronom)) || (fileMimetype != null && getMimetypeToExtension().containsKey(fileMimetype)) || (applicableTo.contains(fileFormat)))) { fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype); StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file); DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath); LOGGER.debug("Running DigitalSignaturePlugin on {}", file.getId()); if (doVerify) { LOGGER.debug("Verifying digital signatures on {}", file.getId()); verification = DigitalSignaturePluginUtils.runDigitalSignatureVerify(directAccess.getPath(), fileFormat, fileMimetype); verifiedFiles.put(file.getId(), verification); if (!"Passed".equals(verification) && verificationAffectsOnOutcome) { reportState = PluginState.FAILURE; reportItem.addPluginDetails(" Signature validation failed on " + fileInfoPath + "."); } } if (doExtract) { LOGGER.debug("Extracting digital signatures information of {}", file.getId()); int extractResultSize = DigitalSignaturePluginUtils.runDigitalSignatureExtraction(model, file, directAccess.getPath(), fileFormat, fileMimetype); if (extractResultSize > 0) { extractedFiles.add(file); } } if (doStrip) { LOGGER.debug("Stripping digital signatures from {}", file.getId()); Path pluginResult = DigitalSignaturePluginUtils.runDigitalSignatureStrip(directAccess.getPath(), fileFormat, fileMimetype); if (pluginResult != null) { 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); model.createRepresentation(aipId, newRepresentationID, original, representation.getType(), notify); reportItem.setOutcomeObjectId( IdUtils.getRepresentationId(representation.getAipId(), newRepresentationID)); } // update file on new representation String newFileId = file.getId().replaceFirst("[.][^.]+$", "." + fileFormat); File f = model.createFile(aipId, newRepresentationID, file.getPath(), newFileId, payload, notify); alteredFiles.add(file); newFiles.add(f); reportItem.setPluginState(reportState); } else { LOGGER.debug("Process failed on file {} of representation {} from AIP {}", file.getId(), representation.getId(), aipId); reportState = PluginState.FAILURE; reportItem.addPluginDetails(" Signature validation stripping on " + fileInfoPath + "."); } } IOUtils.closeQuietly(directAccess); } else { unchangedFiles.add(file); if (ignoreFiles) { validationReport.addIssue(new ValidationIssue(fileInfoPath)); } else { reportState = PluginState.FAILURE; hasNonPdfFiles = true; } } } } else { LOGGER.error("Cannot process representation file", oFile.getCause()); } } IOUtils.closeQuietly(allFiles); // add unchanged files to the new representation if (!alteredFiles.isEmpty()) { 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); } } LOGGER.debug("Creating digital signature plugin event for the representation {}", representation.getId()); boolean notifyEvent = true; createEvent(model, index, aipId, representation.getId(), null, null, reportState, alteredFiles, extractedFiles, newFiles, verifiedFiles, notifyEvent); reportItem.setPluginState(reportState); jobPluginInfo.incrementObjectsProcessed(reportState); if (!reportState.equals(PluginState.FAILURE)) { if (ignoreFiles) { reportItem.setHtmlPluginDetails(true) .setPluginDetails(validationReport.toHtml(false, false, false, "Ignored files")); } } if (hasNonPdfFiles) { reportItem.setPluginDetails("Non PDF files were not ignored"); } } catch (RODAException | IOException | RuntimeException e) { LOGGER.error("Error processing Representation " + representation.getId() + ": " + e.getMessage(), e); reportState = PluginState.FAILURE; reportItem.setPluginState(reportState).setPluginDetails(e.getMessage()); jobPluginInfo.incrementObjectsProcessedWithFailure(); } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } } return report; } @Override public Report executeOnFile(IndexService index, ModelService model, StorageService storage, Report report, SimpleJobPluginInfo jobPluginInfo, List<File> list, Job job) throws PluginException { List<String> newRepresentations = new ArrayList<>(); String newRepresentationID = IdUtils.createUUID(); List<File> unchangedFiles = new ArrayList<>(); List<File> alteredFiles = new ArrayList<>(); List<File> extractedFiles = new ArrayList<>(); List<File> newFiles = new ArrayList<>(); Map<String, String> verifiedFiles = new HashMap<>(); String verification = null; boolean notify = true; for (File file : list) { LOGGER.debug("Processing file {}", file); Report reportItem = PluginHelper.initPluginReportItem(this, IdUtils.getFileId(file), File.class); PluginHelper.updatePartialJobReport(this, model, reportItem, false, job); PluginState reportState = PluginState.SUCCESS; try { 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); if (((filePronom != null && pronomToExtension.containsKey(filePronom)) || (fileMimetype != null && getMimetypeToExtension().containsKey(fileMimetype)) || (applicableTo.contains(fileFormat)))) { fileFormat = getNewFileFormat(fileFormat, filePronom, fileMimetype); StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file); DirectResourceAccess directAccess = storage.getDirectAccess(fileStoragePath); LOGGER.debug("Running DigitalSignaturePlugin on {}", file.getId()); if (doVerify) { LOGGER.debug("Verifying digital signatures on {}", file.getId()); verification = DigitalSignaturePluginUtils.runDigitalSignatureVerify(directAccess.getPath(), fileFormat, fileMimetype); verifiedFiles.put(file.getId(), verification); if (!"Passed".equals(verification) && verificationAffectsOnOutcome) { reportState = PluginState.FAILURE; reportItem.addPluginDetails("Signature validation failed on " + file.getId() + "."); } } if (doExtract) { LOGGER.debug("Extracting digital signatures information of {}", file.getId()); int extractResultSize = DigitalSignaturePluginUtils.runDigitalSignatureExtraction(model, file, directAccess.getPath(), fileFormat, fileMimetype); if (extractResultSize > 0) { extractedFiles.add(file); } } if (doStrip) { LOGGER.debug("Stripping digital signatures from {}", file.getId()); Path pluginResult = DigitalSignaturePluginUtils.runDigitalSignatureStrip(directAccess.getPath(), fileFormat, fileMimetype); if (pluginResult != null) { ContentPayload payload = new FSPathContentPayload(pluginResult); if (!newRepresentations.contains(newRepresentationID)) { LOGGER.debug("Creating a new representation {} on AIP {}", newRepresentationID, file.getAipId()); boolean original = false; newRepresentations.add(newRepresentationID); Representation representation = model.retrieveRepresentation(file.getAipId(), file.getRepresentationId()); model.createRepresentation(file.getAipId(), newRepresentationID, original, representation.getType(), notify); } // update file on new representation String newFileId = file.getId().replaceFirst("[.][^.]+$", "." + fileFormat); File f = model.createFile(file.getAipId(), newRepresentationID, file.getPath(), newFileId, payload, notify); alteredFiles.add(file); newFiles.add(f); reportItem.setOutcomeObjectId(IdUtils.getFileId(f)); reportItem.setPluginState(reportState); } else { LOGGER.debug("Process failed on file {} of representation {} from AIP {}", file.getId(), file.getRepresentationId(), file.getAipId()); reportState = PluginState.FAILURE; reportItem.setPluginState(reportState) .addPluginDetails(" Signature validation stripping on " + file.getId() + "."); } } IOUtils.closeQuietly(directAccess); } else { unchangedFiles.add(file); if (!reportState.equals(PluginState.FAILURE)) { if (ignoreFiles) { reportItem.setPluginDetails("This file was ignored."); } else { reportState = PluginState.FAILURE; reportItem.setPluginDetails("This file was not ignored."); } } } } // add unchanged files to the new representation if (!alteredFiles.isEmpty()) { 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); } } reportItem.setPluginState(reportState); } catch (RODAException | IOException | RuntimeException e) { LOGGER.error("Error processing Representation " + file.getRepresentationId() + ": " + e.getMessage(), e); reportState = PluginState.FAILURE; reportItem.setPluginState(reportState).setPluginDetails(e.getMessage()); } finally { report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } LOGGER.debug("Creating digital signature plugin event for the representation {}", file.getRepresentationId()); boolean notifyEvent = true; createEvent(model, index, file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId(), reportState, alteredFiles, extractedFiles, newFiles, verifiedFiles, notifyEvent); jobPluginInfo.incrementObjectsProcessed(reportState); } return report; } private void createEvent(ModelService model, IndexService index, String aipId, String representationId, List<String> filePath, String fileId, PluginState pluginResultState, List<File> alteredFiles, List<File> extractedFiles, List<File> newFiles, Map<String, String> verifiedFiles, 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 (doVerify) { if (!verifiedFiles.isEmpty()) { stringBuilder.append("The DS verification ran on: "); StringBuilder verifies = new StringBuilder(); for (Entry<String, String> fileEntry : verifiedFiles.entrySet()) { verifies.append(fileEntry.getKey()).append(" (").append(fileEntry.getValue()).append("), "); } String verifiesString = verifies.toString(); stringBuilder.append(verifiesString.substring(0, verifiesString.lastIndexOf(',')) + ". "); } } if (doExtract) { if (!extractedFiles.isEmpty()) { stringBuilder.append("The following files DS information were extracted: "); StringBuilder extracts = new StringBuilder(); for (File file : extractedFiles) { extracts.append(file.getId()).append(", "); } if (extracts.length() > 0) { String extractString = extracts.toString(); stringBuilder.append(extractString.substring(0, extractString.lastIndexOf(',')) + ". "); } } } if (alteredFiles.isEmpty()) { stringBuilder.append("No file was stripped on this representation."); } else { stringBuilder.append("The digital signature (DS) operation stripped some files. "); 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)); } } try { PluginHelper.createPluginEvent(this, aipId, representationId, filePath, fileId, model, index, premisSourceFilesIdentifiers, premisTargetFilesIdentifiers, pluginResultState, stringBuilder.toString(), notify); } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException | ValidationException | AlreadyExistsException e) { throw new PluginException(e.getMessage(), e); } } private String getNewFileFormat(String fileFormat, String filePronom, String fileMimetype) { String changedFileFormat = fileFormat; if (!applicableTo.isEmpty()) { if (StringUtils.isNotBlank(filePronom) && pronomToExtension.get(filePronom) != null && !pronomToExtension.get(filePronom).contains(fileFormat)) { changedFileFormat = pronomToExtension.get(filePronom).get(0); } else if (StringUtils.isNotBlank(fileMimetype) && mimetypeToExtension.get(fileMimetype) != null && !mimetypeToExtension.get(fileMimetype).contains(fileFormat)) { changedFileFormat = mimetypeToExtension.get(fileMimetype).get(0); } } return changedFileFormat; } @Override public PluginType getType() { return PluginType.AIP_TO_AIP; } @Override public Plugin<T> cloneMe() { return new DigitalSignaturePlugin<>(); } @Override public boolean areParameterValuesValid() { return true; } @Override public PreservationEventType getPreservationEventType() { return PreservationEventType.DIGITAL_SIGNATURE_VALIDATION; } @Override public String getPreservationEventDescription() { return "Checked if digital signatures were valid and/or stripped them."; } @Override public String getPreservationEventSuccessMessage() { return "Digital signatures were valid and/or they were stripped with success."; } @Override public String getPreservationEventFailureMessage() { return "Failed to validate and/or strip digital signatures."; } @Override public Report beforeAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { // do nothing return null; } @Override public Report afterAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { // do nothing return null; } @Override public List<String> getCategories() { return Arrays.asList(RodaConstants.PLUGIN_CATEGORY_VALIDATION, RodaConstants.PLUGIN_CATEGORY_CHARACTERIZATION); } public List<String> getApplicableTo() { return applicableTo; } public Map<String, List<String>> getPronomToExtension() { return pronomToExtension; } public Map<String, List<String>> getMimetypeToExtension() { return mimetypeToExtension; } @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; } }