/* * Copyright (C) 2014 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.common.export; import com.yourmediashelf.fedora.generated.foxml.DigitalObject; import cz.cas.lib.proarc.common.export.desa.Const; import cz.cas.lib.proarc.common.export.desa.DesaContext; import cz.cas.lib.proarc.common.export.desa.DesaServices; import cz.cas.lib.proarc.common.export.desa.DesaServices.DesaConfiguration; import cz.cas.lib.proarc.common.export.desa.structure.DesaElement; import cz.cas.lib.proarc.common.export.desa.structure.DesaElementVisitor; import cz.cas.lib.proarc.common.export.mets.MetsExportException; import cz.cas.lib.proarc.common.export.mets.MetsExportException.MetsExportExceptionElement; import cz.cas.lib.proarc.common.export.mets.MetsUtils; import cz.cas.lib.proarc.common.fedora.DigitalObjectException; import cz.cas.lib.proarc.common.fedora.FoxmlUtils; import cz.cas.lib.proarc.common.fedora.RemoteStorage; import cz.cas.lib.proarc.common.fedora.RemoteStorage.RemoteObject; import cz.cas.lib.proarc.common.fedora.relation.RelationResource; import cz.cas.lib.proarc.common.object.model.MetaModelRepository; import cz.cas.lib.proarc.common.user.UserProfile; import cz.cas.lib.proarc.desa.SIP2DESATransporter; import java.io.File; import java.util.List; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; /** * The exporter of digital objects. It can build and validate SIP in format * expected by the DESA registry. * * @author Jan Pokorsky */ public final class DesaExport { private static final Logger LOG = Logger.getLogger(DesaExport.class.getName()); private final RemoteStorage rstorage; private final DesaServices desaServices; private final MetaModelRepository models; public DesaExport(RemoteStorage rstorage, DesaServices desaServices, MetaModelRepository models) { this.rstorage = rstorage; this.desaServices = desaServices; this.models = models; } /** * Runs export to validate inputs. It cleans outputs on exit. * @param exportsFolder folder with user exports * @param pid PID to validate * @param hierarchy export PID ant its children * @return validation report * @throws ExportException unexpected failure */ public List<MetsExportExceptionElement> validate(File exportsFolder, String pid, boolean hierarchy) throws ExportException { Result export = export(exportsFolder, pid, null, true, hierarchy, false, null, null); if (export.getValidationError() != null) { return export.getValidationError().getExceptions(); } else { return null; } } /** * Prepares export package of a single PID without children for later download. * @param exportsFolder folder with user exports * @param pid PID to export * @return the result with token or validation errors * @throws ExportException unexpected failure */ public Result exportDownload(File exportsFolder, String pid) throws ExportException { return export(exportsFolder, pid, null, true, false, true, null, null); } /** * Finds zipped SIP from a previous export. * @param exportsFolder folder with user exports to scan * @param token folder name of the requested export * @return zip file or null */ public static File findExportedPackage(File exportsFolder, String token) { // take the first .zip File target = new File(exportsFolder, token); if (!target.exists()) { return null; } File[] files = target.listFiles(); for (File file : files) { if (file.isFile() && file.getName().endsWith(".zip")) { return file; } } return null; } /** * Exports PIDs in format suitable for DESA registry. It also writes SIP IDs * as a hasExport relation to corresponding digital objects in case of * successful package delivery to the registry. * * @param exportsFolder folder with user exports * @param pid PID to export * @param hierarchy export PID ant its children * @param dryRun build package without sending to the repository. * @param keepResult delete or not export folder on exit * @param log message for storage logging * @return the result * @throws ExportException unexpected failure */ public Result export(File exportsFolder, String pid, String packageId, boolean dryRun, boolean hierarchy, boolean keepResult, String log, UserProfile user ) throws ExportException { File target = ExportUtils.createFolder(exportsFolder, FoxmlUtils.pidAsUuid(pid)); Result result = new Result(); try { if (keepResult) { result.setTargetFolder(target); } RemoteObject fo = rstorage.find(pid); DesaContext dc = buildContext(fo, packageId, target); try { DigitalObject dobj = MetsUtils.readFoXML(fo.getPid(), fo.getClient()); DesaElement dElm = DesaElement.getElement(dobj, null, dc, hierarchy); DesaConfiguration desaCfg = transporterProperties(dryRun, dElm); if (desaCfg != null) { dc.setTransporter(getSipTransporter(desaCfg, user)); } dElm.accept(new DesaElementVisitor(), null); // dc.getMetsExportException() should be ignored now; validation warnings are always thrown // result.setValidationError(dc.getMetsExportException()); if (!dryRun) { storeExportResult(dElm, log); } return result; } catch (MetsExportException ex) { keepResult = false; if (ex.getExceptions().isEmpty()) { throw new ExportException(pid, ex); } return result.setValidationError(ex); } catch (Throwable ex) { keepResult = false; throw new ExportException(pid, ex); } } finally { if (!keepResult) { // run asynchronously not to block client request? boolean deleted = FileUtils.deleteQuietly(target); if (!deleted) { LOG.warning("Cannot delete: " + target.toString()); } } } } private SIP2DESATransporter getSipTransporter(DesaConfiguration desaCfg, UserProfile operator) throws ExportException { String operatorName = desaServices.getOperatorName(operator, desaCfg); String producerCode = desaServices.getProducerCode(operator, desaCfg); if (operatorName == null || producerCode == null) { throw new ExportException("Requires operator name and producer code!"); } return desaServices.getDesaClient(desaCfg).getSipTransporter(operatorName, producerCode); } private DesaContext buildContext(RemoteObject fo, String packageId, File targetFolder) { DesaContext dc = new DesaContext(); dc.setFedoraClient(fo.getClient()); dc.setRemoteStorage(rstorage); dc.setPackageID(packageId); dc.setOutputPath(targetFolder.getAbsolutePath()); // transporter logs dc.setDesaResultPath(new File(targetFolder, "transporter").getAbsolutePath()); return dc; } /** * Finds a transporter configuration for the element or {@code null} when * {@code dryRun} is {@code true}. * @throws MetsExportException no configuration found */ private DesaConfiguration transporterProperties(boolean dryRun, DesaElement dElm) throws MetsExportException { if (!dryRun) { String modelId = dElm.getModel(); if (modelId.startsWith(Const.FEDORAPREFIX)) { // uff, model is not model but fedora resource reference modelId = new RelationResource(modelId).getPid(); } DesaConfiguration desaCfg = desaServices.findConfiguration(models.find(modelId)); if (desaCfg != null) { return desaCfg; } else { throw new MetsExportException(dElm.getOriginalPid(), String.format("No configuration of the DESA registry found for type %s!", modelId), false, null); } } return null; } /** * Stores SIP ID to all digital objects referenced by the DESA element * hierarchy. * @param dElm exported elements * @throws MetsExportException write failure */ void storeExportResult(DesaElement dElm, String log) throws MetsExportException { for (DesaElement childElm : dElm.getChildren()) { storeExportResult(childElm, log); } String idSIPVersion = dElm.getIdSIPVersion(); String pid = dElm.getOriginalPid(); storeObjectExportResult(pid, idSIPVersion, log); } void storeObjectExportResult(String pid, String idSIPVersion, String log) throws MetsExportException { try { ExportUtils.storeObjectExportResult(pid, idSIPVersion, log); } catch (DigitalObjectException ex) { throw new MetsExportException(pid, "Cannot store SIP ID Version!", false, ex); } } /** * The export result. */ public static class Result { private File targetFolder; private MetsExportException validationError; public MetsExportException getValidationError() { return validationError; } Result setValidationError(MetsExportException validationError) { this.validationError = validationError; return this; } /** * Gets the folder with exported packages. * @return {@code null} if the result should not be kept */ public File getTargetFolder() { return targetFolder; } Result setTargetFolder(File targetFolder) { this.targetFolder = targetFolder; return this; } /** * Gets the token for future requests. * @return the token */ public String getDownloadToken() { return targetFolder == null ? null: targetFolder.getName(); } } }