org.roda.wui.api.controllers.BrowserHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.roda.wui.api.controllers.BrowserHelper.java

Source

/**
 * 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.wui.api.controllers;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.core.MultivaluedMap;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.XMLUnit;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.ClassificationPlanUtils;
import org.roda.core.common.ConsumesOutputStream;
import org.roda.core.common.DownloadUtils;
import org.roda.core.common.EntityResponse;
import org.roda.core.common.HandlebarsUtility;
import org.roda.core.common.Messages;
import org.roda.core.common.PremisV3Utils;
import org.roda.core.common.RodaUtils;
import org.roda.core.common.StreamResponse;
import org.roda.core.common.UserUtility;
import org.roda.core.common.iterables.CloseableIterable;
import org.roda.core.common.iterables.CloseableIterables;
import org.roda.core.common.monitor.TransferredResourcesScanner;
import org.roda.core.common.tools.ZipEntryInfo;
import org.roda.core.common.validation.ValidationUtils;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.common.RodaConstants.PreservationEventType;
import org.roda.core.data.common.RodaConstants.RODA_TYPE;
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.IsStillUpdatingException;
import org.roda.core.data.exceptions.JobAlreadyStartedException;
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.utils.JsonUtils;
import org.roda.core.data.v2.LinkingObjectUtils;
import org.roda.core.data.v2.common.ObjectPermission;
import org.roda.core.data.v2.common.ObjectPermissionResult;
import org.roda.core.data.v2.common.OptionalWithCause;
import org.roda.core.data.v2.common.Pair;
import org.roda.core.data.v2.formats.Format;
import org.roda.core.data.v2.index.IndexResult;
import org.roda.core.data.v2.index.IsIndexed;
import org.roda.core.data.v2.index.facet.FacetFieldResult;
import org.roda.core.data.v2.index.facet.FacetValue;
import org.roda.core.data.v2.index.facet.Facets;
import org.roda.core.data.v2.index.facet.SimpleFacetParameter;
import org.roda.core.data.v2.index.filter.EmptyKeyFilterParameter;
import org.roda.core.data.v2.index.filter.Filter;
import org.roda.core.data.v2.index.filter.OneOfManyFilterParameter;
import org.roda.core.data.v2.index.filter.SimpleFilterParameter;
import org.roda.core.data.v2.index.select.SelectedItems;
import org.roda.core.data.v2.index.select.SelectedItemsFilter;
import org.roda.core.data.v2.index.select.SelectedItemsList;
import org.roda.core.data.v2.index.sort.SortParameter;
import org.roda.core.data.v2.index.sort.Sorter;
import org.roda.core.data.v2.index.sublist.Sublist;
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.IndexedAIP;
import org.roda.core.data.v2.ip.IndexedDIP;
import org.roda.core.data.v2.ip.IndexedFile;
import org.roda.core.data.v2.ip.IndexedRepresentation;
import org.roda.core.data.v2.ip.Permissions;
import org.roda.core.data.v2.ip.Representation;
import org.roda.core.data.v2.ip.StoragePath;
import org.roda.core.data.v2.ip.TransferredResource;
import org.roda.core.data.v2.ip.metadata.DescriptiveMetadata;
import org.roda.core.data.v2.ip.metadata.DescriptiveMetadataList;
import org.roda.core.data.v2.ip.metadata.IndexedPreservationAgent;
import org.roda.core.data.v2.ip.metadata.IndexedPreservationEvent;
import org.roda.core.data.v2.ip.metadata.LinkingIdentifier;
import org.roda.core.data.v2.ip.metadata.OtherMetadata;
import org.roda.core.data.v2.ip.metadata.OtherMetadataList;
import org.roda.core.data.v2.ip.metadata.PreservationMetadata;
import org.roda.core.data.v2.ip.metadata.PreservationMetadata.PreservationMetadataType;
import org.roda.core.data.v2.ip.metadata.PreservationMetadataList;
import org.roda.core.data.v2.jobs.IndexedReport;
import org.roda.core.data.v2.jobs.Job;
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.jobs.Reports;
import org.roda.core.data.v2.risks.IndexedRisk;
import org.roda.core.data.v2.risks.Risk;
import org.roda.core.data.v2.risks.Risk.SEVERITY_LEVEL;
import org.roda.core.data.v2.risks.RiskIncidence;
import org.roda.core.data.v2.risks.RiskIncidence.INCIDENCE_STATUS;
import org.roda.core.data.v2.user.User;
import org.roda.core.data.v2.validation.ValidationException;
import org.roda.core.data.v2.validation.ValidationReport;
import org.roda.core.index.IndexService;
import org.roda.core.index.utils.IterableIndexResult;
import org.roda.core.model.ModelService;
import org.roda.core.model.utils.ModelUtils;
import org.roda.core.plugins.plugins.PluginHelper;
import org.roda.core.plugins.plugins.characterization.SiegfriedPlugin;
import org.roda.core.plugins.plugins.ingest.AutoAcceptSIPPlugin;
import org.roda.core.plugins.plugins.internal.DeleteRODAObjectPlugin;
import org.roda.core.plugins.plugins.internal.MovePlugin;
import org.roda.core.plugins.plugins.internal.UpdateAIPPermissionsPlugin;
import org.roda.core.storage.Binary;
import org.roda.core.storage.BinaryVersion;
import org.roda.core.storage.ContentPayload;
import org.roda.core.storage.DefaultStoragePath;
import org.roda.core.storage.Directory;
import org.roda.core.storage.StorageService;
import org.roda.core.storage.fs.FSPathContentPayload;
import org.roda.core.storage.fs.FSUtils;
import org.roda.core.util.IdUtils;
import org.roda.wui.api.v1.utils.ApiUtils;
import org.roda.wui.api.v1.utils.ObjectResponse;
import org.roda.wui.client.browse.MetadataValue;
import org.roda.wui.client.browse.bundle.BinaryVersionBundle;
import org.roda.wui.client.browse.bundle.BrowseAIPBundle;
import org.roda.wui.client.browse.bundle.BrowseFileBundle;
import org.roda.wui.client.browse.bundle.BrowseRepresentationBundle;
import org.roda.wui.client.browse.bundle.DescriptiveMetadataEditBundle;
import org.roda.wui.client.browse.bundle.DescriptiveMetadataVersionsBundle;
import org.roda.wui.client.browse.bundle.DescriptiveMetadataViewBundle;
import org.roda.wui.client.browse.bundle.DipBundle;
import org.roda.wui.client.browse.bundle.PreservationEventViewBundle;
import org.roda.wui.client.browse.bundle.SupportedMetadataTypeBundle;
import org.roda.wui.client.planning.MitigationPropertiesBundle;
import org.roda.wui.client.planning.RiskMitigationBundle;
import org.roda.wui.client.planning.RiskVersionsBundle;
import org.roda.wui.common.HTMLUtils;
import org.roda.wui.common.server.ServerTools;
import org.roda.wui.server.common.XMLSimilarityIgnoreElements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

/**
 * @author Luis Faria <lfaria@keep.pt>
 */
public class BrowserHelper {
    private static final String HTML_EXT = ".html";
    private static final Logger LOGGER = LoggerFactory.getLogger(BrowserHelper.class);
    private static final List<String> aipAncestorsFieldsToReturn = Arrays.asList(RodaConstants.INDEX_UUID,
            RodaConstants.AIP_GHOST, RodaConstants.AIP_LEVEL, RodaConstants.AIP_TITLE, RodaConstants.AIP_PARENT_ID);

    private BrowserHelper() {
        // do nothing
    }

    protected static BrowseAIPBundle retrieveBrowseAipBundle(User user, IndexedAIP aip, Locale locale)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        BrowseAIPBundle bundle = new BrowseAIPBundle();

        // set aip
        bundle.setAIP(aip);
        String aipId = aip.getId();
        boolean justActive = aip.getState().equals(AIPState.ACTIVE);

        // set aip ancestors
        try {
            List<IndexedAIP> ancestors = retrieveAncestors(aip, aipAncestorsFieldsToReturn);
            bundle.setAIPAncestors(ancestors);
        } catch (NotFoundException e) {
            LOGGER.warn("Found an item with invalid ancestors: {}", aip.getId(), e);
        }

        // set descriptive metadata
        try {
            List<DescriptiveMetadataViewBundle> descriptiveMetadataList = retrieveDescriptiveMetadataBundles(aipId,
                    locale);
            bundle.setDescriptiveMetadata(descriptiveMetadataList);
        } catch (NotFoundException e) {
            // do nothing
        }

        // Count child AIPs
        Filter childAIPfilter = new Filter(new SimpleFilterParameter(RodaConstants.AIP_PARENT_ID, aip.getId()));
        Long childAIPCount = RodaCoreFactory.getIndexService().count(IndexedAIP.class, childAIPfilter, user,
                justActive);
        bundle.setChildAIPCount(childAIPCount);

        // Count representations
        Filter repFilter = new Filter(new SimpleFilterParameter(RodaConstants.REPRESENTATION_AIP_ID, aipId));
        Long repCount = RodaCoreFactory.getIndexService().count(IndexedRepresentation.class, repFilter, user,
                justActive);
        bundle.setRepresentationCount(repCount);

        // Count DIPs
        Filter dipsFilter = new Filter(new SimpleFilterParameter(RodaConstants.DIP_AIP_UUIDS, aip.getId()));
        Long dipCount = RodaCoreFactory.getIndexService().count(IndexedDIP.class, dipsFilter, user, justActive);
        bundle.setDipCount(dipCount);

        return bundle;
    }

    public static BrowseRepresentationBundle retrieveBrowseRepresentationBundle(IndexedAIP aip,
            IndexedRepresentation representation, Locale locale)
            throws GenericException, RequestNotValidException, AuthorizationDeniedException {
        BrowseRepresentationBundle bundle = new BrowseRepresentationBundle();

        bundle.setAip(aip);
        bundle.setRepresentation(representation);

        // set aip ancestors
        try {
            List<IndexedAIP> ancestors = retrieveAncestors(aip, aipAncestorsFieldsToReturn);
            bundle.setAipAncestors(ancestors);
        } catch (NotFoundException e) {
            LOGGER.warn("Found an item with invalid ancestors: {}", aip.getId(), e);
        }

        // set representation desc. metadata
        try {
            bundle.setRepresentationDescriptiveMetadata(
                    retrieveDescriptiveMetadataBundles(aip.getId(), representation.getId(), locale));
        } catch (NotFoundException e) {
            // do nothing
        }

        // Count DIPs
        Filter dipsFilter = new Filter(
                new SimpleFilterParameter(RodaConstants.DIP_REPRESENTATION_UUIDS, representation.getUUID()));
        Long dipCount = RodaCoreFactory.getIndexService().count(IndexedDIP.class, dipsFilter);
        bundle.setDipCount(dipCount);

        return bundle;
    }

    public static BrowseFileBundle retrieveBrowseFileBundle(IndexedAIP aip, IndexedRepresentation representation,
            IndexedFile file, User user) throws NotFoundException, GenericException, RequestNotValidException {
        BrowseFileBundle bundle = new BrowseFileBundle();

        bundle.setAip(aip);
        bundle.setRepresentation(representation);
        bundle.setFile(file);

        // set aip ancestors
        try {
            List<IndexedAIP> ancestors = retrieveAncestors(aip, aipAncestorsFieldsToReturn);
            bundle.setAipAncestors(ancestors);
        } catch (NotFoundException e) {
            LOGGER.warn("Found an item with invalid ancestors: {}", aip.getId(), e);
        }

        // set sibling count
        String parentUUID = bundle.getFile().getParentUUID();

        Filter siblingFilter = new Filter(new SimpleFilterParameter(RodaConstants.FILE_REPRESENTATION_UUID,
                bundle.getFile().getRepresentationUUID()));

        if (parentUUID != null) {
            siblingFilter.add(new SimpleFilterParameter(RodaConstants.FILE_PARENT_UUID, parentUUID));
        } else {
            siblingFilter.add(new EmptyKeyFilterParameter(RodaConstants.FILE_PARENT_UUID));
        }

        boolean justActive = AIPState.ACTIVE.equals(aip.getState());
        bundle.setTotalSiblingCount(count(IndexedFile.class, siblingFilter, justActive, user));

        // Count DIPs
        Filter dipsFilter = new Filter(new SimpleFilterParameter(RodaConstants.DIP_FILE_UUIDS, file.getUUID()));
        Long dipCount = RodaCoreFactory.getIndexService().count(IndexedDIP.class, dipsFilter);
        bundle.setDipCount(dipCount);
        return bundle;
    }

    private static List<DescriptiveMetadataViewBundle> retrieveDescriptiveMetadataBundles(String aipId,
            Locale locale)
            throws GenericException, RequestNotValidException, AuthorizationDeniedException, NotFoundException {
        return retrieveDescriptiveMetadataBundles(aipId, null, locale);
    }

    private static List<DescriptiveMetadataViewBundle> retrieveDescriptiveMetadataBundles(String aipId,
            String representationId, final Locale locale)
            throws GenericException, RequestNotValidException, AuthorizationDeniedException, NotFoundException {
        ModelService model = RodaCoreFactory.getModelService();
        List<DescriptiveMetadata> listDescriptiveMetadataBinaries;
        if (representationId != null) {
            listDescriptiveMetadataBinaries = model.retrieveRepresentation(aipId, representationId)
                    .getDescriptiveMetadata();
        } else {
            listDescriptiveMetadataBinaries = model.retrieveAIP(aipId).getDescriptiveMetadata();
        }
        List<DescriptiveMetadataViewBundle> descriptiveMetadataList = new ArrayList<>();

        // Can be null when the AIP is a ghost
        if (listDescriptiveMetadataBinaries != null) {
            for (DescriptiveMetadata descriptiveMetadata : listDescriptiveMetadataBinaries) {
                DescriptiveMetadataViewBundle bundle = retrieveDescriptiveMetadataBundle(aipId, representationId,
                        descriptiveMetadata, locale);
                descriptiveMetadataList.add(bundle);
            }
        }

        return descriptiveMetadataList;
    }

    private static DescriptiveMetadataViewBundle retrieveDescriptiveMetadataBundle(String aipId,
            String representationId, DescriptiveMetadata descriptiveMetadata, final Locale locale)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        Messages messages = RodaCoreFactory.getI18NMessages(locale);
        DescriptiveMetadataViewBundle bundle = new DescriptiveMetadataViewBundle();
        bundle.setId(descriptiveMetadata.getId());

        if (descriptiveMetadata.getType() != null) {
            try {
                String labelWithoutVersion = messages
                        .getTranslation(RodaConstants.I18N_UI_BROWSE_METADATA_DESCRIPTIVE_TYPE_PREFIX
                                + descriptiveMetadata.getType().toLowerCase());
                if (descriptiveMetadata.getVersion() != null) {
                    String labelWithVersion = messages
                            .getTranslation(RodaConstants.I18N_UI_BROWSE_METADATA_DESCRIPTIVE_TYPE_PREFIX
                                    + descriptiveMetadata.getType().toLowerCase()
                                    + RodaConstants.METADATA_VERSION_SEPARATOR
                                    + descriptiveMetadata.getVersion().toLowerCase(), labelWithoutVersion);
                    bundle.setLabel(labelWithVersion);
                } else {
                    bundle.setLabel(labelWithoutVersion);
                }

            } catch (MissingResourceException e) {
                bundle.setLabel(descriptiveMetadata.getId());
            }
        }

        try {
            StoragePath storagePath;
            if (representationId != null) {
                storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                        descriptiveMetadata.getId());
            } else {
                storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, descriptiveMetadata.getId());
            }

            bundle.setHasHistory(!CloseableIterables.isEmpty(model.getStorage().listBinaryVersions(storagePath)));

        } catch (RODAException | RuntimeException t) {
            bundle.setHasHistory(false);
        }
        return bundle;
    }

    private static DescriptiveMetadataViewBundle retrieveDescriptiveMetadataBundle(String aipId,
            String representationId, String descriptiveMetadataId, final Locale locale)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        DescriptiveMetadata descriptiveMetadata = model.retrieveDescriptiveMetadata(aipId, representationId,
                descriptiveMetadataId);
        return retrieveDescriptiveMetadataBundle(aipId, representationId, descriptiveMetadata, locale);
    }

    public static DescriptiveMetadataEditBundle retrieveDescriptiveMetadataEditBundle(User user, IndexedAIP aip,
            IndexedRepresentation representation, String descriptiveMetadataId, final Locale locale)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        String representationId = representation != null ? representation.getId() : null;

        DescriptiveMetadata metadata = RodaCoreFactory.getModelService().retrieveDescriptiveMetadata(aip.getId(),
                representationId, descriptiveMetadataId);
        return retrieveDescriptiveMetadataEditBundle(user, aip, representation, descriptiveMetadataId,
                metadata.getType(), metadata.getVersion(), locale);
    }

    public static DescriptiveMetadataEditBundle retrieveDescriptiveMetadataEditBundle(User user, IndexedAIP aip,
            IndexedRepresentation representation, String descriptiveMetadataId, String type, String version,
            final Locale locale)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        DescriptiveMetadataEditBundle ret;
        InputStream inputStream = null;
        try {
            String representationId = representation != null ? representation.getId() : null;
            Binary binary = RodaCoreFactory.getModelService().retrieveDescriptiveMetadataBinary(aip.getId(),
                    representationId, descriptiveMetadataId);
            inputStream = binary.getContent().createInputStream();
            String xml = IOUtils.toString(inputStream, "UTF-8");

            // Get the supported metadata type with the same type and version
            // We need this to try to get get the values for the form
            SupportedMetadataTypeBundle metadataTypeBundle = null;
            List<SupportedMetadataTypeBundle> supportedMetadataTypeBundles = BrowserHelper
                    .retrieveSupportedMetadata(user, aip, representation, locale);
            for (SupportedMetadataTypeBundle typeBundle : supportedMetadataTypeBundles) {
                if (typeBundle.getType() != null && typeBundle.getType().equalsIgnoreCase(type)) {
                    if (typeBundle.getVersion() != null && typeBundle.getVersion().equalsIgnoreCase(version)) {
                        metadataTypeBundle = typeBundle;
                        break;
                    }
                }
            }

            boolean similar = false;
            // Get the values using XPath
            Set<MetadataValue> values = null;
            String template = null;

            if (metadataTypeBundle != null) {
                values = metadataTypeBundle.getValues();
                template = metadataTypeBundle.getTemplate();
                if (values != null) {
                    for (MetadataValue mv : values) {
                        // clear the auto-generated values
                        // mv.set("value", null);
                        String xpathRaw = mv.get("xpath");
                        if (xpathRaw != null && xpathRaw.length() > 0) {
                            String[] xpaths = xpathRaw.split("##%##");
                            String value;
                            List<String> allValues = new ArrayList<>();
                            for (String xpath : xpaths) {
                                allValues.addAll(ServerTools.applyXpath(xml, xpath));
                            }
                            // if any of the values is different, concatenate all values in a
                            // string, otherwise return the value
                            boolean allEqual = allValues.stream()
                                    .allMatch(s -> s.trim().equals(allValues.get(0).trim()));
                            if (allEqual && !allValues.isEmpty()) {
                                value = allValues.get(0);
                            } else {
                                value = String.join(" / ", allValues);
                            }
                            mv.set("value", value.trim());
                        }
                    }

                    // Identity check. Test if the original XML is equal to the result of
                    // applying the extracted values to the template
                    metadataTypeBundle.setValues(values);
                    String templateWithValues = retrieveDescriptiveMetadataPreview(metadataTypeBundle);
                    try {
                        XMLUnit.setIgnoreComments(true);
                        XMLUnit.setIgnoreWhitespace(true);
                        XMLUnit.setIgnoreAttributeOrder(true);
                        XMLUnit.setCompareUnmatched(false);

                        Diff xmlDiff = new Diff(xml, templateWithValues);
                        xmlDiff.overrideDifferenceListener(new XMLSimilarityIgnoreElements("schemaLocation"));
                        similar = xmlDiff.identical() || xmlDiff.similar();
                    } catch (SAXException e) {
                        LOGGER.warn("Could not check if template can loose info", e);
                    }
                }
            }

            ret = new DescriptiveMetadataEditBundle(descriptiveMetadataId, type, version, xml, template, values,
                    similar);
        } catch (IOException e) {
            throw new GenericException("Error getting descriptive metadata edit bundle: " + e.getMessage());
        } finally {
            IOUtils.closeQuietly(inputStream);
        }

        return ret;
    }

    public static DipBundle retrieveDipBundle(String dipUUID, String dipFileUUID, User user)
            throws GenericException, NotFoundException, RequestNotValidException {
        DipBundle bundle = new DipBundle();

        bundle.setDip(retrieve(IndexedDIP.class, dipUUID,
                Arrays.asList(RodaConstants.INDEX_UUID, RodaConstants.DIP_ID, RodaConstants.DIP_TITLE,
                        RodaConstants.DIP_AIP_IDS, RodaConstants.DIP_AIP_UUIDS, RodaConstants.DIP_FILE_IDS,
                        RodaConstants.DIP_REPRESENTATION_IDS)));

        List<String> dipFileFields = new ArrayList<>();

        if (dipFileUUID != null) {
            DIPFile dipFile = retrieve(DIPFile.class, dipFileUUID, dipFileFields);
            bundle.setDipFile(dipFile);

            List<DIPFile> dipFileAncestors = new ArrayList<>();
            for (String dipFileAncestor : dipFile.getAncestorsUUIDs()) {
                try {
                    dipFileAncestors
                            .add(retrieve(DIPFile.class, dipFileAncestor, Arrays.asList(RodaConstants.INDEX_UUID,
                                    RodaConstants.DIPFILE_DIP_ID, RodaConstants.DIPFILE_ID)));
                } catch (NotFoundException e) {
                    // ignore
                }
            }
            bundle.setDipFileAncestors(dipFileAncestors);
        } else {
            // if there is only one DIPFile in the DIP and it is NOT a directory
            // then select it
            Filter filter = new Filter(new SimpleFilterParameter(RodaConstants.DIPFILE_DIP_ID, dipUUID));
            Sublist sublist = new Sublist(0, 1);
            IndexResult<DIPFile> dipFiles = find(DIPFile.class, filter, Sorter.NONE, sublist, Facets.NONE, user,
                    false, dipFileFields);
            if (dipFiles.getTotalCount() == 1 && !dipFiles.getResults().get(0).isDirectory()) {
                bundle.setDipFile(dipFiles.getResults().get(0));
            }
        }

        List<String> aipFields = Arrays.asList(RodaConstants.INDEX_UUID, RodaConstants.AIP_TITLE,
                RodaConstants.AIP_LEVEL, RodaConstants.AIP_DATE_FINAL, RodaConstants.AIP_DATE_INITIAL,
                RodaConstants.AIP_GHOST);
        List<String> representationFields = Arrays.asList(RodaConstants.INDEX_UUID,
                RodaConstants.REPRESENTATION_TYPE, RodaConstants.REPRESENTATION_NUMBER_OF_DATA_FILES,
                RodaConstants.REPRESENTATION_ORIGINAL, RodaConstants.REPRESENTATION_AIP_ID,
                RodaConstants.REPRESENTATION_ID);
        List<String> fileFields = new ArrayList<>();

        // infer from DIP
        IndexedDIP dip = bundle.getDip();
        if (!dip.getFileIds().isEmpty()) {
            IndexedFile file = BrowserHelper.retrieve(IndexedFile.class, IdUtils.getFileId(dip.getFileIds().get(0)),
                    fileFields);
            bundle.setFile(file);
            bundle.setRepresentation(BrowserHelper.retrieve(IndexedRepresentation.class,
                    file.getRepresentationUUID(), representationFields));
            bundle.setAip(BrowserHelper.retrieve(IndexedAIP.class, file.getAipId(), aipFields));
        } else if (!dip.getRepresentationIds().isEmpty()) {
            IndexedRepresentation representation = BrowserHelper.retrieve(IndexedRepresentation.class,
                    IdUtils.getRepresentationId(dip.getRepresentationIds().get(0)), representationFields);
            bundle.setRepresentation(representation);
            bundle.setAip(BrowserHelper.retrieve(IndexedAIP.class, representation.getAipId(), aipFields));
        } else if (!dip.getAipIds().isEmpty()) {
            IndexedAIP aip = BrowserHelper.retrieve(IndexedAIP.class, dip.getAipIds().get(0).getAipId(), aipFields);
            bundle.setAip(aip);
        }

        return bundle;
    }

    protected static List<IndexedAIP> retrieveAncestors(IndexedAIP aip, List<String> fieldsToReturn)
            throws GenericException, NotFoundException {
        return RodaCoreFactory.getIndexService().retrieveAncestors(aip, fieldsToReturn);
    }

    protected static <T extends IsIndexed> IndexResult<T> find(Class<T> returnClass, Filter filter, Sorter sorter,
            Sublist sublist, Facets facets, User user, boolean justActive, List<String> fieldsToReturn)
            throws GenericException, RequestNotValidException {
        return RodaCoreFactory.getIndexService().find(returnClass, filter, sorter, sublist, facets, user,
                justActive, fieldsToReturn);
    }

    protected static <T extends IsIndexed> IterableIndexResult<T> findAll(final Class<T> returnClass,
            final Filter filter, final Sorter sorter, final Sublist sublist, final Facets facets, final User user,
            final boolean justActive, List<String> fieldsToReturn) {
        return RodaCoreFactory.getIndexService().findAll(returnClass, filter, sorter, sublist, facets, user,
                justActive, fieldsToReturn);
    }

    protected static <T extends IsIndexed> Long count(Class<T> returnClass, Filter filter, boolean justActive,
            User user) throws GenericException, RequestNotValidException {
        return RodaCoreFactory.getIndexService().count(returnClass, filter, user, justActive);
    }

    protected static <T extends IsIndexed> T retrieve(Class<T> returnClass, String id, List<String> fieldsToReturn)
            throws GenericException, NotFoundException {
        return RodaCoreFactory.getIndexService().retrieve(returnClass, id, fieldsToReturn);
    }

    protected static <T extends IsIndexed> void commit(Class<T> returnClass)
            throws GenericException, NotFoundException {
        RodaCoreFactory.getIndexService().commit(returnClass);
    }

    protected static <T extends IsIndexed> List<T> retrieve(Class<T> returnClass, SelectedItems<T> selectedItems,
            List<String> fieldsToReturn) throws GenericException, NotFoundException, RequestNotValidException {
        List<T> ret;

        if (selectedItems instanceof SelectedItemsList) {
            SelectedItemsList<T> selectedList = (SelectedItemsList<T>) selectedItems;
            ret = RodaCoreFactory.getIndexService().retrieve(returnClass, selectedList.getIds(), fieldsToReturn);
        } else if (selectedItems instanceof SelectedItemsFilter) {
            SelectedItemsFilter<T> selectedFilter = (SelectedItemsFilter<T>) selectedItems;
            int counter = RodaCoreFactory.getIndexService().count(returnClass, selectedFilter.getFilter())
                    .intValue();
            ret = RodaCoreFactory.getIndexService().find(returnClass, selectedFilter.getFilter(), Sorter.NONE,
                    new Sublist(0, counter), fieldsToReturn).getResults();
        } else {
            throw new RequestNotValidException(
                    "Unsupported SelectedItems implementation: " + selectedItems.getClass().getName());
        }

        return ret;
    }

    protected static <T extends IsIndexed> List<String> suggest(Class<T> returnClass, String field, String query,
            User user, boolean allowPartial) throws GenericException, NotFoundException {
        boolean justActive = true;
        return RodaCoreFactory.getIndexService().suggest(returnClass, field, query, user, allowPartial, justActive);
    }

    public static void validateGetFileParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN));
        }
    }

    protected static void validateGetAIPRepresentationParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP));
        }
    }

    protected static void validateGetDIPParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP));
        }
    }

    protected static EntityResponse retrieveAIPRepresentation(IndexedRepresentation representation,
            String acceptFormat)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        String aipId = representation.getAipId();
        String representationId = representation.getId();

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            StoragePath storagePath = ModelUtils.getRepresentationStoragePath(representation.getAipId(),
                    representation.getId());
            Directory directory = RodaCoreFactory.getStorageService().getDirectory(storagePath);
            return ApiUtils.download(directory, representationId);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            ModelService model = RodaCoreFactory.getModelService();
            Representation rep = model.retrieveRepresentation(aipId, representationId);
            return new ObjectResponse<Representation>(acceptFormat, rep);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static StreamResponse retrieveAIPRepresentationPart(IndexedRepresentation representation, String part)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        String aipId = representation.getAipId();
        String representationId = representation.getId();

        if (RodaConstants.STORAGE_DIRECTORY_DATA.equals(part)) {
            StoragePath storagePath = ModelUtils.getRepresentationDataStoragePath(aipId, representationId);
            Directory directory = RodaCoreFactory.getStorageService().getDirectory(storagePath);
            return ApiUtils.download(directory, part);
        } else if (RodaConstants.STORAGE_DIRECTORY_METADATA.equals(part)) {
            StoragePath storagePath = ModelUtils.getRepresentationMetadataStoragePath(aipId, representationId);
            Directory directory = RodaCoreFactory.getStorageService().getDirectory(storagePath);
            return ApiUtils.download(directory, part);
        } else if (RodaConstants.STORAGE_DIRECTORY_DOCUMENTATION.equals(part)) {
            Directory directory = RodaCoreFactory.getModelService().getDocumentationDirectory(aipId,
                    representationId);
            return ApiUtils.download(directory, part);
        } else if (RodaConstants.STORAGE_DIRECTORY_SCHEMAS.equals(part)) {
            Directory directory = RodaCoreFactory.getModelService().getSchemasDirectory(aipId, representationId);
            return ApiUtils.download(directory, part);
        } else {
            throw new GenericException("Unsupported part: " + part);
        }
    }

    protected static void validateListAIPDescriptiveMetadataParams(String acceptFormat)
            throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML));
        }
    }

    public static void validateGetOtherMetadataParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN));
        }
    }

    protected static EntityResponse listAIPDescriptiveMetadata(String aipId, String start, String limit,
            String acceptFormat)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        AIP aip = model.retrieveAIP(aipId);
        List<DescriptiveMetadata> metadata = aip.getDescriptiveMetadata();
        return listDescriptiveMetadata(metadata, aipId, start, limit, acceptFormat);
    }

    protected static EntityResponse listRepresentationDescriptiveMetadata(String aipId, String representationId,
            String start, String limit, String acceptFormat)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        Representation representation = model.retrieveRepresentation(aipId, representationId);
        List<DescriptiveMetadata> metadata = representation.getDescriptiveMetadata();
        return listDescriptiveMetadata(metadata, aipId, start, limit, acceptFormat);
    }

    private static EntityResponse listDescriptiveMetadata(List<DescriptiveMetadata> metadata, String aipId,
            String start, String limit, String acceptFormat)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StorageService storage = RodaCoreFactory.getStorageService();
        Pair<Integer, Integer> pagingParams = ApiUtils.processPagingParams(start, limit);
        int startInt = pagingParams.getFirst();
        int limitInt = pagingParams.getSecond();
        int counter = 0;

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            List<ZipEntryInfo> zipEntries = new ArrayList<>();
            for (DescriptiveMetadata dm : metadata) {
                if (counter >= startInt && (counter <= limitInt || limitInt == -1)) {
                    StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, dm.getId());
                    Binary binary = storage.getBinary(storagePath);
                    ZipEntryInfo info = new ZipEntryInfo(storagePath.getName(), binary.getContent());
                    zipEntries.add(info);
                } else {
                    break;
                }
                counter++;
            }

            return DownloadUtils.createZipStreamResponse(zipEntries, aipId);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            int endInt = limitInt == -1 ? metadata.size()
                    : (limitInt > metadata.size() ? metadata.size() : limitInt);
            DescriptiveMetadataList list = new DescriptiveMetadataList(metadata.subList(startInt, endInt));
            return new ObjectResponse<DescriptiveMetadataList>(acceptFormat, list);
        }

        return null;
    }

    protected static void validateGetPreservationMetadataParams(String acceptFormat)
            throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN));
        }
    }

    protected static void validateGetAIPDescriptiveMetadataParams(String acceptFormat)
            throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN));
        }
    }

    public static EntityResponse retrieveAIPDescritiveMetadata(String aipId, String metadataId, String acceptFormat,
            String language)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        final String filename;
        final String mediaType;
        final ConsumesOutputStream stream;
        StreamResponse ret;
        ModelService model = RodaCoreFactory.getModelService();

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            Binary descriptiveMetadataBinary = model.retrieveDescriptiveMetadataBinary(aipId, metadataId);
            filename = descriptiveMetadataBinary.getStoragePath().getName();
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_XML;
            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(descriptiveMetadataBinary.getContent().createInputStream(), out);
                }
            };
            ret = new StreamResponse(filename, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)) {
            Binary descriptiveMetadataBinary = model.retrieveDescriptiveMetadataBinary(aipId, metadataId);
            filename = descriptiveMetadataBinary.getStoragePath().getName() + HTML_EXT;
            DescriptiveMetadata descriptiveMetadata = model.retrieveDescriptiveMetadata(aipId, metadataId);
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_HTML;
            String htmlDescriptive = HTMLUtils.descriptiveMetadataToHtml(descriptiveMetadataBinary,
                    descriptiveMetadata.getType(), descriptiveMetadata.getVersion(),
                    ServerTools.parseLocale(language));
            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    PrintStream printStream = new PrintStream(out);
                    printStream.print(htmlDescriptive);
                    printStream.close();
                }
            };
            ret = new StreamResponse(filename, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {

            AIP aip = model.retrieveAIP(aipId);
            List<DescriptiveMetadata> resultList = aip.getDescriptiveMetadata().stream()
                    .filter(dm -> dm.getId().equals(metadataId)).collect(Collectors.toList());

            return new ObjectResponse<DescriptiveMetadata>(acceptFormat, resultList.get(0));

        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }

        return ret;
    }

    public static EntityResponse retrieveRepresentationDescriptiveMetadata(String aipId, String representationId,
            String metadataId, String acceptFormat, String language)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        final String filename;
        final String mediaType;
        final ConsumesOutputStream stream;
        StreamResponse ret;
        ModelService model = RodaCoreFactory.getModelService();

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            Binary descriptiveMetadataBinary = model.retrieveDescriptiveMetadataBinary(aipId, representationId,
                    metadataId);
            filename = descriptiveMetadataBinary.getStoragePath().getName();
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_XML;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(descriptiveMetadataBinary.getContent().createInputStream(), out);
                }
            };

            ret = new StreamResponse(filename, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)) {
            Binary descriptiveMetadataBinary = model.retrieveDescriptiveMetadataBinary(aipId, representationId,
                    metadataId);
            filename = descriptiveMetadataBinary.getStoragePath().getName() + HTML_EXT;
            DescriptiveMetadata descriptiveMetadata = model.retrieveDescriptiveMetadata(aipId, representationId,
                    metadataId);
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_HTML;
            String htmlDescriptive = HTMLUtils.descriptiveMetadataToHtml(descriptiveMetadataBinary,
                    descriptiveMetadata.getType(), descriptiveMetadata.getVersion(),
                    ServerTools.parseLocale(language));

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    PrintStream printStream = new PrintStream(out);
                    printStream.print(htmlDescriptive);
                    printStream.close();
                }
            };

            ret = new StreamResponse(filename, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {

            Representation representation = model.retrieveRepresentation(aipId, representationId);
            List<DescriptiveMetadata> resultList = representation.getDescriptiveMetadata().stream()
                    .filter(dm -> dm.getId().equals(metadataId)).collect(Collectors.toList());

            return new ObjectResponse<DescriptiveMetadata>(acceptFormat, resultList.get(0));

        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }

        return ret;
    }

    public static EntityResponse retrieveAIPDescritiveMetadataVersion(String aipId, String metadataId,
            String versionId, String acceptFormat, String language)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        final String filename;
        final String mediaType;
        final ConsumesOutputStream stream;

        ModelService model = RodaCoreFactory.getModelService();

        StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, metadataId);
        BinaryVersion binaryVersion = model.getStorage().getBinaryVersion(storagePath, versionId);
        Binary binary = binaryVersion.getBinary();

        String fileName = binary.getStoragePath().getName();
        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_XML;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return fileName;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(binary.getContent().createInputStream(), out);
                }
            };
            return new StreamResponse(fileName, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)) {
            filename = fileName + HTML_EXT;
            DescriptiveMetadata descriptiveMetadata = model.retrieveDescriptiveMetadata(aipId, metadataId);
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_HTML;
            String htmlDescriptive = HTMLUtils.descriptiveMetadataToHtml(binary, descriptiveMetadata.getType(),
                    descriptiveMetadata.getVersion(), ServerTools.parseLocale(language));

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    PrintStream printStream = new PrintStream(out);
                    printStream.print(htmlDescriptive);
                    printStream.close();
                }
            };

            return new StreamResponse(filename, mediaType, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {

            AIP aip = model.retrieveAIP(aipId);
            List<DescriptiveMetadata> resultList = aip.getDescriptiveMetadata().stream()
                    .filter(dm -> dm.getId().equals(metadataId)).collect(Collectors.toList());

            return new ObjectResponse<DescriptiveMetadata>(acceptFormat, resultList.get(0));

        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static EntityResponse retrieveRepresentationDescriptiveMetadataVersion(String aipId,
            String representationId, String metadataId, String versionId, String acceptFormat, String language)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        final String filename;
        final String mediaType;
        final ConsumesOutputStream stream;

        ModelService model = RodaCoreFactory.getModelService();

        StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId, metadataId);
        BinaryVersion binaryVersion = model.getStorage().getBinaryVersion(storagePath, versionId);
        Binary binary = binaryVersion.getBinary();

        String fileName = binary.getStoragePath().getName();
        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_XML;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return binary.getStoragePath().getName();
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(binary.getContent().createInputStream(), out);
                }
            };

            return new StreamResponse(fileName, mediaType, stream);

        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)) {
            filename = fileName + HTML_EXT;
            DescriptiveMetadata descriptiveMetadata = model.retrieveDescriptiveMetadata(aipId, representationId,
                    metadataId);
            mediaType = RodaConstants.MEDIA_TYPE_TEXT_HTML;
            String htmlDescriptive = HTMLUtils.descriptiveMetadataToHtml(binary, descriptiveMetadata.getType(),
                    descriptiveMetadata.getVersion(), ServerTools.parseLocale(language));

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return fileName + HTML_EXT;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    PrintStream printStream = new PrintStream(out);
                    printStream.print(htmlDescriptive);
                    printStream.close();
                }
            };

            return new StreamResponse(filename, mediaType, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {

            AIP aip = model.retrieveAIP(aipId);
            List<DescriptiveMetadata> resultList = new ArrayList<>();

            for (Representation representation : aip.getRepresentations()) {
                if (representation.getId().equals(representationId)) {
                    resultList = representation.getDescriptiveMetadata().stream()
                            .filter(dm -> dm.getId().equals(metadataId)).collect(Collectors.toList());
                }
            }

            return new ObjectResponse<DescriptiveMetadata>(acceptFormat, resultList.get(0));

        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    protected static void validateListAIPMetadataParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML));
        }
    }

    public static EntityResponse listAIPPreservationMetadata(String aipId, String acceptFormat)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            CloseableIterable<OptionalWithCause<PreservationMetadata>> preservationFiles = RodaCoreFactory
                    .getModelService().listPreservationMetadata(aipId, true);
            StorageService storage = RodaCoreFactory.getStorageService();
            List<ZipEntryInfo> zipEntries = new ArrayList<>();
            Map<String, ZipEntryInfo> agents = new HashMap<>();

            for (OptionalWithCause<PreservationMetadata> oPreservationFile : preservationFiles) {
                if (oPreservationFile.isPresent()) {
                    PreservationMetadata preservationFile = oPreservationFile.get();
                    StoragePath storagePath = ModelUtils.getPreservationMetadataStoragePath(preservationFile);
                    Binary binary = storage.getBinary(storagePath);

                    ZipEntryInfo info = new ZipEntryInfo(FSUtils.getStoragePathAsString(storagePath, true),
                            binary.getContent());
                    zipEntries.add(info);

                    if (preservationFile.getType() == PreservationMetadataType.EVENT) {
                        try {
                            List<LinkingIdentifier> agentIDS = PremisV3Utils.extractAgentsFromEvent(binary);
                            if (!agentIDS.isEmpty()) {
                                for (LinkingIdentifier li : agentIDS) {
                                    String agentID = li.getValue();
                                    if (!agents.containsKey(agentID)) {
                                        StoragePath agentPath = ModelUtils.getPreservationMetadataStoragePath(
                                                agentID, PreservationMetadataType.AGENT);
                                        Binary agentBinary = storage.getBinary(agentPath);
                                        info = new ZipEntryInfo(FSUtils.getStoragePathAsString(
                                                DefaultStoragePath.parse(preservationFile.getAipId()), false,
                                                agentPath, true), agentBinary.getContent());
                                        agents.put(agentID, info);
                                    }
                                }
                            }
                        } catch (ValidationException | GenericException e) {
                            // do nothing
                        }
                    }

                } else {
                    LOGGER.error("Cannot get AIP preservation metadata", oPreservationFile.getCause());
                }
            }

            if (agents.size() > 0) {
                for (Map.Entry<String, ZipEntryInfo> entry : agents.entrySet()) {
                    zipEntries.add(entry.getValue());
                }
            }

            IOUtils.closeQuietly(preservationFiles);
            return DownloadUtils.createZipStreamResponse(zipEntries, aipId);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            CloseableIterable<OptionalWithCause<PreservationMetadata>> preservationFiles = RodaCoreFactory
                    .getModelService().listPreservationMetadata(aipId, true);
            PreservationMetadataList metadataList = new PreservationMetadataList();

            for (OptionalWithCause<PreservationMetadata> oPreservationFile : preservationFiles) {
                if (oPreservationFile.isPresent()) {
                    metadataList.addObject(oPreservationFile.get());
                }
            }

            IOUtils.closeQuietly(preservationFiles);
            return new ObjectResponse<PreservationMetadataList>(acceptFormat, metadataList);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    protected static void validateGetAIPRepresentationPreservationMetadataParams(String acceptFormat)
            throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML));
        }
    }

    private static EntityResponse getAIPRepresentationPreservationMetadataEntityResponse(String aipId,
            String representationId, String startAgent, String limitAgent, String startEvent, String limitEvent,
            String startFile, String limitFile, String acceptFormat,
            CloseableIterable<OptionalWithCause<PreservationMetadata>> preservationFiles) throws GenericException,
            RequestNotValidException, NotFoundException, AuthorizationDeniedException, IOException {
        Pair<Integer, Integer> pagingParamsAgent = ApiUtils.processPagingParams(startAgent, limitAgent);
        int counterAgent = 0;
        Pair<Integer, Integer> pagingParamsEvent = ApiUtils.processPagingParams(startEvent, limitEvent);
        int counterEvent = 0;
        Pair<Integer, Integer> pagingParamsFile = ApiUtils.processPagingParams(startFile, limitFile);
        int counterFile = 0;

        List<ZipEntryInfo> zipEntries = new ArrayList<>();
        PreservationMetadataList pms = new PreservationMetadataList();

        for (OptionalWithCause<PreservationMetadata> oPreservationFile : preservationFiles) {
            if (oPreservationFile.isPresent()) {
                PreservationMetadata preservationFile = oPreservationFile.get();
                boolean add = false;

                if (preservationFile.getType().equals(PreservationMetadataType.AGENT)) {
                    if (counterAgent >= pagingParamsAgent.getFirst()
                            && (counterAgent <= pagingParamsAgent.getSecond()
                                    || pagingParamsAgent.getSecond() == -1)) {
                        add = true;
                    }
                    counterAgent++;
                } else if (preservationFile.getType().equals(PreservationMetadataType.EVENT)) {
                    if (counterEvent >= pagingParamsEvent.getFirst()
                            && (counterEvent <= pagingParamsEvent.getSecond()
                                    || pagingParamsEvent.getSecond() == -1)) {
                        add = true;
                    }
                    counterEvent++;
                } else if (preservationFile.getType().equals(PreservationMetadataType.FILE)) {
                    if (counterFile >= pagingParamsFile.getFirst() && (counterFile <= pagingParamsFile.getSecond()
                            || pagingParamsFile.getSecond() == -1)) {
                        add = true;
                    }
                    counterFile++;
                }

                if (add) {
                    if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
                        StoragePath storagePath = ModelUtils.getPreservationMetadataStoragePath(preservationFile);
                        Binary binary = RodaCoreFactory.getStorageService().getBinary(storagePath);
                        ZipEntryInfo info = new ZipEntryInfo(storagePath.getName(), binary.getContent());
                        zipEntries.add(info);
                    } else {
                        pms.addObject(preservationFile);
                    }
                }
            } else {
                LOGGER.error("Cannot get AIP preservation metadata", oPreservationFile.getCause());
            }
        }

        IOUtils.closeQuietly(preservationFiles);
        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            return DownloadUtils.createZipStreamResponse(zipEntries, aipId + "_" + representationId);
        } else {
            return new ObjectResponse<PreservationMetadataList>(acceptFormat, pms);
        }
    }

    public static EntityResponse retrieveAIPRepresentationPreservationMetadata(String aipId,
            String representationId, String startAgent, String limitAgent, String startEvent, String limitEvent,
            String startFile, String limitFile, String acceptFormat) throws GenericException, NotFoundException,
            RequestNotValidException, AuthorizationDeniedException, IOException {

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            CloseableIterable<OptionalWithCause<PreservationMetadata>> preservationFiles = RodaCoreFactory
                    .getModelService().listPreservationMetadata(aipId, representationId);
            return getAIPRepresentationPreservationMetadataEntityResponse(aipId, representationId, startAgent,
                    limitAgent, startEvent, limitEvent, startFile, limitFile, acceptFormat, preservationFiles);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static EntityResponse retrieveAIPRepresentationPreservationMetadataFile(String aipId,
            String representationId, List<String> filePath, String fileId, String acceptFormat)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            Binary binary = model.retrievePreservationRepresentation(aipId, representationId, filePath, fileId);
            String filename = binary.getStoragePath().getName();

            ConsumesOutputStream stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return acceptFormat;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(binary.getContent().createInputStream(), out);
                }
            };
            return new StreamResponse(filename, RodaConstants.MEDIA_TYPE_APPLICATION_OCTET_STREAM, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            PreservationMetadataType type = PreservationMetadataType.REPRESENTATION;
            if (fileId != null) {
                type = PreservationMetadataType.FILE;
            }
            PreservationMetadata pm = model.retrievePreservationMetadata(aipId, representationId, filePath, fileId,
                    type);
            return new ObjectResponse<PreservationMetadata>(acceptFormat, pm);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static void createOrUpdateAIPRepresentationPreservationMetadataFile(String aipId,
            String representationId, List<String> fileDirectoryPath, String fileId, InputStream is, boolean create)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException,
            ValidationException, AlreadyExistsException {
        Path file = null;
        try {
            ModelService model = RodaCoreFactory.getModelService();
            file = Files.createTempFile("preservation", ".tmp");
            Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING);
            ContentPayload payload = new FSPathContentPayload(file);
            boolean notify = true;
            if (create) {
                model.createPreservationMetadata(PreservationMetadataType.FILE, aipId, representationId,
                        fileDirectoryPath, fileId, payload, notify);
            } else {
                PreservationMetadataType type = PreservationMetadataType.FILE;
                String id = IdUtils.getPreservationFileId(fileId);
                model.updatePreservationMetadata(id, type, aipId, representationId, fileDirectoryPath, fileId,
                        payload, notify);
            }
        } catch (IOException e) {
            throw new GenericException("Error creating or updating AIP representation preservation metadata file",
                    e);
        } finally {
            if (FSUtils.exists(file)) {
                try {
                    Files.delete(file);
                } catch (IOException e) {
                    LOGGER.warn("Error while deleting temporary file", e);
                }
            }
        }
    }

    public static void deletePreservationMetadataFile(PreservationMetadataType type, String aipId,
            String representationId, String id, boolean notify)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().deletePreservationMetadata(type, aipId, representationId, id, notify);
    }

    public static EntityResponse retrievePreservationMetadataEvent(String id, String aipId,
            String representationUUID, String fileUUID, boolean onlyDetails, String acceptFormat, String language)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();
        String representationId = null;
        List<String> filePath = null;
        String fileId = null;

        if (fileUUID != null) {
            IndexedFile file = index.retrieve(IndexedFile.class, fileUUID, RodaConstants.FILE_FIELDS_TO_RETURN);
            representationId = file.getRepresentationId();
            filePath = file.getPath();
            fileId = file.getId();
        } else if (representationUUID != null) {
            IndexedRepresentation rep = index.retrieve(IndexedRepresentation.class, representationUUID,
                    Arrays.asList(RodaConstants.REPRESENTATION_ID));
            representationId = rep.getId();
        }

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            final Binary binary = model.retrievePreservationEvent(aipId, representationId, filePath, fileId, id);
            final String mediaType = RodaConstants.MEDIA_TYPE_TEXT_XML;

            final ConsumesOutputStream stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return binary.getStoragePath().getName();
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    IOUtils.copy(binary.getContent().createInputStream(), out);
                }
            };

            return new StreamResponse(stream.getFileName(), mediaType, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            PreservationMetadata pm = model.retrievePreservationMetadata(aipId, representationId, filePath, fileId,
                    PreservationMetadataType.EVENT);
            return new ObjectResponse<PreservationMetadata>(acceptFormat, pm);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_HTML.equals(acceptFormat)) {
            final Binary binary = model.retrievePreservationEvent(aipId, representationId, filePath, fileId, id);
            final String filename = binary.getStoragePath().getName() + HTML_EXT;
            final String mediaType = RodaConstants.MEDIA_TYPE_TEXT_HTML;
            final String htmlEvent = HTMLUtils.preservationMetadataEventToHtml(binary, onlyDetails,
                    ServerTools.parseLocale(language));
            final ConsumesOutputStream stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return mediaType;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    PrintStream printStream = new PrintStream(out);
                    printStream.print(htmlEvent);
                    printStream.close();
                }
            };
            return new StreamResponse(filename, mediaType, stream);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static EntityResponse listOtherMetadata(String aipId, String representationId, List<String> filePath,
            String fileId, String type, String acceptFormat)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {

        CloseableIterable<OptionalWithCause<OtherMetadata>> otherFiles = RodaCoreFactory.getModelService()
                .listOtherMetadata(aipId, representationId, filePath, fileId, type);

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            StorageService storage = RodaCoreFactory.getStorageService();
            List<ZipEntryInfo> zipEntries = new ArrayList<>();

            for (OptionalWithCause<OtherMetadata> oFile : otherFiles) {
                if (oFile.isPresent()) {
                    OtherMetadata file = oFile.get();
                    StoragePath storagePath = ModelUtils.getOtherMetadataStoragePath(aipId, representationId,
                            filePath, fileId, file.getFileSuffix(), file.getType());
                    Binary binary = storage.getBinary(storagePath);

                    ZipEntryInfo info = new ZipEntryInfo(FSUtils.getStoragePathAsString(storagePath, true),
                            binary.getContent());
                    zipEntries.add(info);
                } else {
                    LOGGER.error("Cannot get list other metadata", oFile.getCause());
                }
            }

            IOUtils.closeQuietly(otherFiles);
            return DownloadUtils.createZipStreamResponse(zipEntries, aipId);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            OtherMetadataList metadataList = new OtherMetadataList();

            for (OptionalWithCause<OtherMetadata> oFile : otherFiles) {
                if (oFile.isPresent()) {
                    metadataList.addObject(oFile.get());
                }
            }

            IOUtils.closeQuietly(otherFiles);
            return new ObjectResponse<OtherMetadataList>(acceptFormat, metadataList);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static EntityResponse retrieveOtherMetadata(String aipId, String representationId, List<String> filePath,
            String fileId, String type, String suffix, String acceptFormat)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            final ConsumesOutputStream stream;
            Binary otherMetadataBinary = RodaCoreFactory.getModelService().retrieveOtherMetadataBinary(aipId,
                    representationId, filePath, fileId, suffix, type);
            String filename = otherMetadataBinary.getStoragePath().getName();
            String mediaType = RodaConstants.MEDIA_TYPE_WILDCARD;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return acceptFormat;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    InputStream fileInputStream = null;
                    try {
                        fileInputStream = otherMetadataBinary.getContent().createInputStream();
                        IOUtils.copy(fileInputStream, out);
                    } finally {
                        IOUtils.closeQuietly(fileInputStream);
                    }
                }
            };
            return new StreamResponse(filename, mediaType, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            OtherMetadata other = RodaCoreFactory.getModelService().retrieveOtherMetadata(aipId, representationId,
                    filePath, fileId, suffix, type);
            return new ObjectResponse<OtherMetadata>(acceptFormat, other);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static OtherMetadata createOrUpdateOtherMetadataFile(String aipId, String representationId,
            List<String> fileDirectoryPath, String fileId, String type, String fileSuffix, InputStream is)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            Path tempFile = Files.createTempFile("descriptive", ".tmp");
            Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
            ContentPayload payload = new FSPathContentPayload(tempFile);

            return RodaCoreFactory.getModelService().createOrUpdateOtherMetadata(aipId, representationId,
                    fileDirectoryPath, fileId, fileSuffix, type, payload, false);
        } catch (IOException e) {
            throw new GenericException("Error creating or updating other metadata");
        }
    }

    public static void deleteOtherMetadataFile(String aipId, String representationId,
            List<String> fileDirectoryPath, String fileId, String fileSuffix, String type)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().deleteOtherMetadata(aipId, representationId, fileDirectoryPath, fileId,
                fileSuffix, type);
    }

    public static IndexedAIP moveAIPInHierarchy(User user, SelectedItems<IndexedAIP> selected, String parentId,
            String details) throws GenericException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException, AlreadyExistsException, ValidationException {

        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Move AIP in hierarchy");
        job.setSourceObjects(selected);
        job.setPlugin(MovePlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_ID, parentId);
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
        job.setPluginParameters(pluginParameters);

        RodaCoreFactory.getModelService().createJob(job);
        try {
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute move job", e);
        }

        IndexService index = RodaCoreFactory.getIndexService();
        index.commit(IndexedAIP.class);
        index.commit(IndexedRepresentation.class);
        index.commit(IndexedFile.class);

        return (parentId != null)
                ? index.retrieve(IndexedAIP.class, parentId, Arrays.asList(RodaConstants.INDEX_UUID))
                : null;
    }

    public static AIP createAIP(User user, String parentAipId, String type, Permissions permissions)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException,
            AlreadyExistsException {
        ModelService model = RodaCoreFactory.getModelService();
        return model.createAIP(parentAipId, type, permissions, user.getName());
    }

    public static AIP updateAIP(User user, AIP aip) throws GenericException, AuthorizationDeniedException,
            RequestNotValidException, NotFoundException, AlreadyExistsException {
        ModelService model = RodaCoreFactory.getModelService();
        return model.updateAIP(aip, user.getName());
    }

    public static void deleteAIP(User user, SelectedItems<IndexedAIP> selected, String details)
            throws AuthorizationDeniedException, GenericException, RequestNotValidException, NotFoundException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Delete AIP");
        job.setSourceObjects(selected);
        job.setPlugin(DeleteRODAObjectPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
        job.setPluginParameters(pluginParameters);

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute AIP delete action", e);
        }
    }

    public static void deleteRepresentation(User user, SelectedItems<IndexedRepresentation> selected,
            String details)
            throws AuthorizationDeniedException, GenericException, RequestNotValidException, NotFoundException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Delete representations");
        job.setSourceObjects(selected);
        job.setPlugin(DeleteRODAObjectPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
        job.setPluginParameters(pluginParameters);

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute representations delete action", e);
        }
    }

    public static void deleteFile(User user, SelectedItems<IndexedFile> selected, String details)
            throws AuthorizationDeniedException, GenericException, RequestNotValidException, NotFoundException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Delete files");
        job.setSourceObjects(selected);
        job.setPlugin(DeleteRODAObjectPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
        job.setPluginParameters(pluginParameters);

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute file delete action", e);
        }
    }

    public static DescriptiveMetadata createDescriptiveMetadataFile(String aipId, String descriptiveMetadataId,
            String descriptiveMetadataType, String descriptiveMetadataVersion,
            ContentPayload descriptiveMetadataPayload) throws GenericException, ValidationException,
            AuthorizationDeniedException, RequestNotValidException, AlreadyExistsException, NotFoundException {
        return createDescriptiveMetadataFile(aipId, null, descriptiveMetadataId, descriptiveMetadataType,
                descriptiveMetadataVersion, descriptiveMetadataPayload);
    }

    public static DescriptiveMetadata createDescriptiveMetadataFile(String aipId, String representationId,
            String descriptiveMetadataId, String descriptiveMetadataType, String descriptiveMetadataVersion,
            ContentPayload descriptiveMetadataPayload) throws GenericException, ValidationException,
            AuthorizationDeniedException, RequestNotValidException, AlreadyExistsException, NotFoundException {

        ValidationReport report = ValidationUtils.validateDescriptiveBinary(descriptiveMetadataPayload,
                descriptiveMetadataType, descriptiveMetadataVersion, false);

        if (!report.isValid()) {
            throw new ValidationException(report);
        }

        return RodaCoreFactory.getModelService().createDescriptiveMetadata(aipId, representationId,
                descriptiveMetadataId, descriptiveMetadataPayload, descriptiveMetadataType,
                descriptiveMetadataVersion);
    }

    public static DescriptiveMetadata updateDescriptiveMetadataFile(String aipId, String representationId,
            String descriptiveMetadataId, String descriptiveMetadataType, String descriptiveMetadataVersion,
            ContentPayload descriptiveMetadataPayload, Map<String, String> properties) throws GenericException,
            AuthorizationDeniedException, ValidationException, RequestNotValidException, NotFoundException {

        ValidationReport report = ValidationUtils.validateDescriptiveBinary(descriptiveMetadataPayload,
                descriptiveMetadataType, descriptiveMetadataVersion, false);

        if (!report.isValid()) {
            throw new ValidationException(report);
        }

        return RodaCoreFactory.getModelService().updateDescriptiveMetadata(aipId, representationId,
                descriptiveMetadataId, descriptiveMetadataPayload, descriptiveMetadataType,
                descriptiveMetadataVersion, properties);

    }

    public static void deleteDescriptiveMetadataFile(String aipId, String representationId,
            String descriptiveMetadataId)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().deleteDescriptiveMetadata(aipId, representationId, descriptiveMetadataId);
    }

    public static DescriptiveMetadata retrieveMetadataFile(String aipId, String descriptiveMetadataId)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        return RodaCoreFactory.getModelService().retrieveDescriptiveMetadata(aipId, descriptiveMetadataId);
    }

    public static Representation createRepresentation(User user, String aipId, String representationId, String type,
            String details) throws GenericException, AuthorizationDeniedException, RequestNotValidException,
            NotFoundException, AlreadyExistsException {
        String eventDescription = "The process of creating an object of the repository.";

        ModelService model = RodaCoreFactory.getModelService();

        try {
            Representation representation = model.createRepresentation(aipId, representationId, true, type, true);

            List<LinkingIdentifier> targets = new ArrayList<>();
            targets.add(PluginHelper.getLinkingIdentifier(aipId, representation.getId(),
                    RodaConstants.PRESERVATION_LINKING_OBJECT_OUTCOME));

            String outcomeText = "The representation '" + representationId + "' has been manually created";
            model.createEvent(aipId, null, null, null, PreservationEventType.CREATION, eventDescription, null,
                    targets, PluginState.SUCCESS, outcomeText, details, user.getName(), true);

            RodaCoreFactory.getIndexService().commit(IndexedRepresentation.class);
            return representation;
        } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException
                | AlreadyExistsException e) {
            String outcomeText = "The representation '" + representationId + "' has not been manually created";
            model.createUpdateAIPEvent(aipId, null, null, null, PreservationEventType.CREATION, eventDescription,
                    PluginState.FAILURE, outcomeText, details, user.getName(), true);

            throw e;
        }
    }

    public static Representation updateRepresentation(Representation representation) throws GenericException,
            AuthorizationDeniedException, RequestNotValidException, NotFoundException, AlreadyExistsException {
        return RodaCoreFactory.getModelService().updateRepresentationInfo(representation);
    }

    public static void deleteRepresentation(String aipId, String representationId)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().deleteRepresentation(aipId, representationId);
    }

    public static File createFile(User user, String aipId, String representationId, List<String> directoryPath,
            String fileId, ContentPayload content, String details) throws GenericException,
            AuthorizationDeniedException, RequestNotValidException, NotFoundException, AlreadyExistsException {
        String eventDescription = "The process of creating an object of the repository.";

        ModelService model = RodaCoreFactory.getModelService();

        try {
            File file = model.createFile(aipId, representationId, directoryPath, fileId, content);

            List<LinkingIdentifier> targets = new ArrayList<>();
            targets.add(PluginHelper.getLinkingIdentifier(aipId, file.getRepresentationId(), file.getPath(),
                    file.getId(), RodaConstants.PRESERVATION_LINKING_OBJECT_OUTCOME));

            String outcomeText = "The file '" + file.getId() + "' has been manually created.";
            model.createEvent(aipId, representationId, null, null, PreservationEventType.CREATION, eventDescription,
                    null, targets, PluginState.SUCCESS, outcomeText, details, user.getName(), true);

            return file;
        } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException
                | AlreadyExistsException e) {
            String outcomeText = "The file '" + fileId + "' has not been manually created.";
            model.createUpdateAIPEvent(aipId, representationId, null, null, PreservationEventType.CREATION,
                    eventDescription, PluginState.FAILURE, outcomeText, details, user.getName(), true);

            throw e;
        }
    }

    public static File updateFile(File file, ContentPayload contentPayload, boolean createIfNotExists,
            boolean notify) throws GenericException, AuthorizationDeniedException, RequestNotValidException,
            NotFoundException, AlreadyExistsException {
        return RodaCoreFactory.getModelService().updateFile(file, contentPayload, createIfNotExists, notify);
    }

    public static EntityResponse retrieveAIPRepresentationFile(IndexedFile iFile, String acceptFormat)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        StoragePath filePath = ModelUtils.getFileStoragePath(iFile.getAipId(), iFile.getRepresentationId(),
                iFile.getPath(), iFile.getId());

        if (!iFile.isDirectory() && RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            final String filename;
            final String mediaType;
            final ConsumesOutputStream stream;

            StorageService storage = RodaCoreFactory.getStorageService();
            Binary representationFileBinary = storage.getBinary(filePath);
            filename = representationFileBinary.getStoragePath().getName();
            mediaType = RodaConstants.MEDIA_TYPE_WILDCARD;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return acceptFormat;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    InputStream fileInputStream = null;
                    try {
                        fileInputStream = representationFileBinary.getContent().createInputStream();
                        IOUtils.copy(fileInputStream, out);
                    } finally {
                        IOUtils.closeQuietly(fileInputStream);
                    }
                }
            };
            return new StreamResponse(filename, mediaType, stream);
        } else if (iFile.isDirectory() && (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat))) {
            Directory directory = RodaCoreFactory.getStorageService().getDirectory(filePath);
            return ApiUtils.download(directory);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            File file = RodaCoreFactory.getModelService().retrieveFile(iFile.getAipId(),
                    iFile.getRepresentationId(), iFile.getPath(), iFile.getId());
            return new ObjectResponse<File>(acceptFormat, file);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static DescriptiveMetadata createOrUpdateAIPDescriptiveMetadataFile(String aipId,
            String representationId, String metadataId, String metadataType, String metadataVersion,
            Map<String, String> properties, InputStream is, boolean create)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException,
            AlreadyExistsException, ValidationException {
        Path file = null;
        DescriptiveMetadata dm = null;
        try {
            ModelService model = RodaCoreFactory.getModelService();
            file = Files.createTempFile("descriptive", ".tmp");
            Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING);
            ContentPayload payload = new FSPathContentPayload(file);

            if (create) {
                dm = model.createDescriptiveMetadata(aipId, representationId, metadataId, payload, metadataType,
                        metadataVersion);
            } else {
                dm = model.updateDescriptiveMetadata(aipId, representationId, metadataId, payload, metadataType,
                        metadataVersion, properties);
            }

        } catch (IOException e) {
            throw new GenericException("Error creating or updating AIP descriptive metadata file", e);
        } finally {
            FSUtils.deletePathQuietly(file);
        }

        return dm;
    }

    public static TransferredResource createTransferredResourcesFolder(String parentUUID, String folderName,
            boolean forceCommit) throws GenericException, RequestNotValidException, NotFoundException {
        TransferredResource transferredResource = RodaCoreFactory.getTransferredResourcesScanner()
                .createFolder(parentUUID, folderName);
        if (forceCommit) {
            RodaCoreFactory.getTransferredResourcesScanner().commit();
        }
        return transferredResource;
    }

    private static <T extends IsIndexed> List<String> consolidate(User user, Class<T> classToReturn,
            SelectedItems<T> selected)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException {
        List<String> ret;

        if (selected instanceof SelectedItemsList) {
            ret = ((SelectedItemsList<T>) selected).getIds();
        } else if (selected instanceof SelectedItemsFilter) {
            SelectedItemsFilter<T> selectedItemsFilter = (SelectedItemsFilter<T>) selected;
            Filter filter = selectedItemsFilter.getFilter();
            boolean justActive = selectedItemsFilter.justActive();
            Long count = count(classToReturn, filter, justActive, user);
            IndexResult<T> find = find(classToReturn, filter, Sorter.NONE, new Sublist(0, count.intValue()),
                    Facets.NONE, user, justActive, Arrays.asList(RodaConstants.INDEX_UUID));
            ret = find.getResults().stream().map(i -> i.getUUID()).collect(Collectors.toList());
        } else {
            throw new RequestNotValidException("Class not supported: " + selected.getClass().getName());
        }

        return ret;
    }

    public static void deleteTransferredResources(SelectedItems<TransferredResource> selected, User user)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {
        List<String> ids = consolidate(user, TransferredResource.class, selected);

        // check permissions
        UserUtility.checkTransferredResourceAccess(user, ids);

        RodaCoreFactory.getTransferredResourcesScanner().deleteTransferredResource(ids);
    }

    public static TransferredResource createTransferredResourceFile(String parentUUID, String fileName,
            InputStream inputStream, boolean forceCommit)
            throws GenericException, AlreadyExistsException, RequestNotValidException, NotFoundException {
        LOGGER.debug("createTransferredResourceFile(path={}, name={})", parentUUID, fileName);
        TransferredResource transferredResource = RodaCoreFactory.getTransferredResourcesScanner()
                .createFile(parentUUID, fileName, inputStream);
        if (forceCommit) {
            RodaCoreFactory.getTransferredResourcesScanner().commit();
        }

        return transferredResource;
    }

    protected static <T extends IsIndexed> void delete(User user, Class<T> returnClass, SelectedItems<T> ids)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        List<String> idList = consolidate(user, returnClass, ids);
        RodaCoreFactory.getIndexService().delete(returnClass, idList);
        RodaCoreFactory.getIndexService().commit(returnClass);
    }

    public static boolean retrieveScanUpdateStatus(Optional<String> folderRelativePath) {
        return RodaCoreFactory.getTransferredResourcesScannerUpdateStatus(folderRelativePath);
    }

    public static void updateTransferredResources(Optional<String> folderRelativePath, boolean waitToFinish)
            throws IsStillUpdatingException, GenericException {
        RodaCoreFactory.getTransferredResourcesScanner().updateTransferredResources(folderRelativePath,
                waitToFinish);
    }

    public static void updateTransferredResource(Optional<String> folderRelativePath, ContentPayload payload,
            String name, boolean waitToFinish)
            throws IsStillUpdatingException, GenericException, NotFoundException, IOException {
        RodaCoreFactory.getTransferredResourcesScanner().updateTransferredResource(folderRelativePath, payload,
                name, waitToFinish);
    }

    public static ConsumesOutputStream retrieveClassificationPlan(User user, String filename)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        return ClassificationPlanUtils.retrieveClassificationPlan(user, filename);

    }

    public static List<SupportedMetadataTypeBundle> retrieveSupportedMetadata(User user, IndexedAIP aip,
            IndexedRepresentation representation, Locale locale) throws GenericException {
        Messages messages = RodaCoreFactory.getI18NMessages(locale);
        List<String> types = RodaUtils.copyList(RodaCoreFactory.getRodaConfiguration()
                .getList(RodaConstants.UI_BROWSER_METADATA_DESCRIPTIVE_TYPES));

        List<SupportedMetadataTypeBundle> supportedMetadata = new ArrayList<>();

        if (types != null) {
            for (String id : types) {
                String type = id;
                String version = null;
                if (id.contains(RodaConstants.METADATA_VERSION_SEPARATOR)) {
                    version = id.substring(id.lastIndexOf(RodaConstants.METADATA_VERSION_SEPARATOR) + 1,
                            id.length());
                    type = id.substring(0, id.lastIndexOf(RodaConstants.METADATA_VERSION_SEPARATOR));
                }
                String key = RodaConstants.I18N_UI_BROWSE_METADATA_DESCRIPTIVE_TYPE_PREFIX + type;
                if (version != null) {
                    key += RodaConstants.METADATA_VERSION_SEPARATOR + version.toLowerCase();
                }
                String label = messages.getTranslation(key, type);

                String template = null;
                Set<MetadataValue> values = new HashSet<>();
                try (InputStream templateStream = RodaCoreFactory
                        .getConfigurationFileAsStream(RodaConstants.METADATA_TEMPLATE_FOLDER + "/"
                                + ((version != null) ? type + RodaConstants.METADATA_VERSION_SEPARATOR + version
                                        : type)
                                + RodaConstants.METADATA_TEMPLATE_EXTENSION)) {

                    if (templateStream != null) {
                        template = IOUtils.toString(templateStream, RodaConstants.DEFAULT_ENCODING);
                        values = ServerTools.transform(template);
                        for (MetadataValue mv : values) {
                            String generator = mv.get("auto-generate");
                            if (generator != null && generator.length() > 0) {
                                String value;
                                if (representation != null) {
                                    value = ServerTools.autoGenerateRepresentationValue(representation, generator);
                                } else {
                                    value = ServerTools.autoGenerateAIPValue(aip, user, generator);
                                }

                                if (value != null) {
                                    mv.set("value", value);
                                }
                            }
                            String labels = mv.get("label");
                            String labelI18N = mv.get("labeli18n");
                            if (labels != null && labelI18N != null) {
                                Map<String, String> labelsMaps = JsonUtils.getMapFromJson(labels);
                                try {
                                    labelsMaps.put(locale.toString(),
                                            RodaCoreFactory.getI18NMessages(locale).getTranslation(labelI18N));
                                } catch (MissingResourceException e) {
                                    LOGGER.debug("Missing resource: {}", labelI18N);
                                }
                                labels = JsonUtils.getJsonFromObject(labelsMaps);
                                mv.set("label", labels);
                            }

                            String i18nPrefix = mv.get("optionsLabelI18nKeyPrefix");
                            if (i18nPrefix != null) {
                                Map<String, String> terms = messages.getTranslations(i18nPrefix, String.class,
                                        false);
                                if (terms.size() > 0) {
                                    try {
                                        String options = mv.get("options");
                                        List<String> optionsList = JsonUtils.getListFromJson(options, String.class);

                                        if (optionsList != null) {
                                            Map<String, Map<String, String>> i18nMap = new HashMap<>();
                                            for (int i = 0; i < optionsList.size(); i++) {
                                                String value = optionsList.get(i);
                                                String translation = terms.get(i18nPrefix + "." + value);
                                                if (translation == null) {
                                                    translation = value;
                                                }
                                                Map<String, String> term = new HashMap<>();
                                                term.put(locale.toString(), translation);
                                                i18nMap.put(value, term);
                                            }
                                            mv.set("optionsLabels", JsonUtils.getJsonFromObject(i18nMap));
                                        }
                                    } catch (MissingResourceException e) {
                                        LOGGER.error(e.getMessage(), e);
                                    }
                                }
                            }

                        }
                    }
                } catch (IOException e) {
                    LOGGER.error("Error getting the template from the stream", e);
                }

                supportedMetadata.add(new SupportedMetadataTypeBundle(id, type, version, label, template, values));
            }
        }
        return supportedMetadata;
    }

    public static EntityResponse retrieveTransferredResource(final TransferredResource transferredResource,
            String acceptFormat) throws NotFoundException, RequestNotValidException, GenericException {

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {

            ConsumesOutputStream stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return acceptFormat;
                }

                @Override
                public String getFileName() {
                    return transferredResource.getName();
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    InputStream retrieveFile = null;
                    try {
                        retrieveFile = RodaCoreFactory.getTransferredResourcesScanner()
                                .retrieveFile(transferredResource.getFullPath());
                        IOUtils.copy(retrieveFile, out);
                    } catch (NotFoundException | RequestNotValidException | GenericException e) {
                        // do nothing
                    } finally {
                        IOUtils.closeQuietly(retrieveFile);
                    }
                }
            };

            return new StreamResponse(transferredResource.getName(),
                    RodaConstants.MEDIA_TYPE_APPLICATION_OCTET_STREAM, stream);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            return new ObjectResponse<TransferredResource>(acceptFormat, transferredResource);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static PreservationEventViewBundle retrievePreservationEventViewBundle(String eventId)
            throws NotFoundException, GenericException {
        PreservationEventViewBundle eventBundle = new PreservationEventViewBundle();
        Map<String, IndexedAIP> aips = new HashMap<>();
        Map<String, IndexedRepresentation> representations = new HashMap<>();
        Map<String, IndexedFile> files = new HashMap<>();
        Map<String, TransferredResource> transferredResources = new HashMap<>();

        List<String> eventFields = new ArrayList<>();
        IndexedPreservationEvent ipe = retrieve(IndexedPreservationEvent.class, eventId, eventFields);
        eventBundle.setEvent(ipe);
        if (ipe.getLinkingAgentIds() != null && !ipe.getLinkingAgentIds().isEmpty()) {
            Map<String, IndexedPreservationAgent> agents = new HashMap<>();
            for (LinkingIdentifier agentID : ipe.getLinkingAgentIds()) {
                try {
                    List<String> agentFields = Arrays.asList(RodaConstants.PRESERVATION_AGENT_ID,
                            RodaConstants.PRESERVATION_AGENT_NAME, RodaConstants.PRESERVATION_AGENT_TYPE,
                            RodaConstants.PRESERVATION_AGENT_ROLES, RodaConstants.PRESERVATION_AGENT_VERSION,
                            RodaConstants.PRESERVATION_AGENT_NOTE, RodaConstants.PRESERVATION_AGENT_EXTENSION);
                    IndexedPreservationAgent agent = retrieve(IndexedPreservationAgent.class, agentID.getValue(),
                            agentFields);
                    agents.put(agentID.getValue(), agent);
                } catch (NotFoundException | GenericException e) {
                    LOGGER.error("Error getting agent {}: {}", agentID, e.getMessage());
                }
            }
            eventBundle.setAgents(agents);
        }

        List<LinkingIdentifier> allLinkingIdentifiers = new ArrayList<>();

        if (ipe.getSourcesObjectIds() != null) {
            allLinkingIdentifiers.addAll(ipe.getSourcesObjectIds());
        }

        if (ipe.getOutcomeObjectIds() != null) {
            allLinkingIdentifiers.addAll(ipe.getOutcomeObjectIds());
        }

        for (LinkingIdentifier identifier : allLinkingIdentifiers) {
            String idValue = identifier.getValue();
            RODA_TYPE linkingType = LinkingObjectUtils.getLinkingIdentifierType(idValue);

            try {
                if (RODA_TYPE.AIP.equals(linkingType)) {
                    String uuid = LinkingObjectUtils.getAipIdFromLinkingId(idValue);
                    List<String> aipFields = Arrays.asList(RodaConstants.AIP_TITLE, RodaConstants.INDEX_UUID);
                    IndexedAIP aip = retrieve(IndexedAIP.class, uuid, aipFields);
                    aips.put(idValue, aip);
                } else if (RODA_TYPE.REPRESENTATION.equals(linkingType)) {
                    String uuid = LinkingObjectUtils.getRepresentationIdFromLinkingId(idValue);
                    List<String> representationFields = Arrays.asList(RodaConstants.REPRESENTATION_ID,
                            RodaConstants.REPRESENTATION_AIP_ID, RodaConstants.REPRESENTATION_ORIGINAL);
                    IndexedRepresentation rep = retrieve(IndexedRepresentation.class, uuid, representationFields);
                    representations.put(idValue, rep);
                } else if (RODA_TYPE.FILE.equals(linkingType)) {
                    List<String> fileFields = new ArrayList<>(RodaConstants.FILE_FIELDS_TO_RETURN);
                    fileFields.addAll(RodaConstants.FILE_FORMAT_FIELDS_TO_RETURN);
                    fileFields.addAll(Arrays.asList(RodaConstants.FILE_ORIGINALNAME, RodaConstants.FILE_SIZE,
                            RodaConstants.FILE_FILEFORMAT, RodaConstants.FILE_FORMAT_VERSION));
                    IndexedFile file = retrieve(IndexedFile.class,
                            LinkingObjectUtils.getFileIdFromLinkingId(idValue), fileFields);
                    files.put(idValue, file);
                } else if (RODA_TYPE.TRANSFERRED_RESOURCE.equals(linkingType)) {
                    String id = LinkingObjectUtils.getTransferredResourceIdFromLinkingId(idValue);
                    if (id != null) {
                        List<String> resourceFields = Arrays.asList(RodaConstants.INDEX_UUID,
                                RodaConstants.TRANSFERRED_RESOURCE_NAME,
                                RodaConstants.TRANSFERRED_RESOURCE_FULLPATH);
                        TransferredResource tr = retrieve(TransferredResource.class, IdUtils.createUUID(id),
                                resourceFields);
                        transferredResources.put(idValue, tr);
                    }
                } else {
                    LOGGER.warn("No support for linking object type: {}", linkingType);
                }
            } catch (NotFoundException e) {
                // nothing to do
            }
        }

        eventBundle.setAips(aips);
        eventBundle.setRepresentations(representations);
        eventBundle.setFiles(files);
        eventBundle.setTransferredResources(transferredResources);

        return eventBundle;
    }

    public static CloseableIterable<BinaryVersion> listDescriptiveMetadataVersions(String aipId,
            String representationId, String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        return RodaCoreFactory.getStorageService().listBinaryVersions(storagePath);

    }

    public static DescriptiveMetadataVersionsBundle retrieveDescriptiveMetadataVersionsBundle(String aipId,
            String representationId, String metadataId, Locale locale)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        DescriptiveMetadataVersionsBundle bundle = new DescriptiveMetadataVersionsBundle();

        DescriptiveMetadataViewBundle descriptiveMetadataBundle = retrieveDescriptiveMetadataBundle(aipId,
                representationId, metadataId, locale);
        bundle.setDescriptiveMetadata(descriptiveMetadataBundle);

        List<BinaryVersionBundle> versionBundles = new ArrayList<>();

        CloseableIterable<BinaryVersion> it = listDescriptiveMetadataVersions(aipId, representationId, metadataId);
        for (BinaryVersion v : it) {
            versionBundles.add(new BinaryVersionBundle(v.getId(), v.getCreatedDate(), v.getProperties()));
        }
        IOUtils.closeQuietly(it);

        bundle.setVersions(versionBundles);
        return bundle;
    }

    public static void revertDescriptiveMetadataVersion(String aipId, String representationId,
            String descriptiveMetadataId, String versionId, Map<String, String> properties)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().revertDescriptiveMetadataVersion(aipId, representationId,
                descriptiveMetadataId, versionId, properties);
    }

    public static void deleteDescriptiveMetadataVersion(String aipId, String representationId,
            String descriptiveMetadataId, String versionId)
            throws NotFoundException, GenericException, RequestNotValidException {
        StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        RodaCoreFactory.getStorageService().deleteBinaryVersion(storagePath, versionId);
    }

    public static void updateAIPPermissions(User user, IndexedAIP indexedAIP, Permissions permissions,
            String details, boolean recursive) throws GenericException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException, JobAlreadyStartedException {
        final String eventDescription = "The process of updating an object of the repository.";

        final ModelService model = RodaCoreFactory.getModelService();
        AIP aip = model.retrieveAIP(indexedAIP.getId());
        aip.setPermissions(permissions);
        List<LinkingIdentifier> sources = Arrays.asList(
                PluginHelper.getLinkingIdentifier(aip.getId(), RodaConstants.PRESERVATION_LINKING_OBJECT_OUTCOME));

        try {
            model.updateAIPPermissions(aip, user.getName());
            String outcomeText = PluginHelper.createOutcomeTextForAIP(indexedAIP,
                    " permissions has been manually updated");
            model.createEvent(aip.getId(), null, null, null, PreservationEventType.UPDATE, eventDescription,
                    sources, null, PluginState.SUCCESS, outcomeText, details, user.getName(), true);
        } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException e) {
            String outcomeText = PluginHelper.createOutcomeTextForAIP(indexedAIP,
                    " permissions has not been manually updated");
            model.createEvent(aip.getId(), null, null, null, PreservationEventType.UPDATE, eventDescription,
                    sources, null, PluginState.FAILURE, outcomeText, details, user.getName(), true);

            throw e;
        }

        if (recursive) {
            Filter filter = new Filter(new SimpleFilterParameter(RodaConstants.AIP_ANCESTORS, indexedAIP.getId()));
            SelectedItemsFilter<IndexedAIP> selectedItems = new SelectedItemsFilter<>(filter,
                    IndexedAIP.class.getName(), Boolean.FALSE);

            Job job = new Job();
            job.setId(IdUtils.createUUID());
            job.setName("Update AIP permissions recursively");
            job.setSourceObjects(selectedItems);
            job.setPlugin(UpdateAIPPermissionsPlugin.class.getCanonicalName());
            job.setPluginType(PluginType.INTERNAL);
            job.setUsername(user.getName());

            Map<String, String> pluginParameters = new HashMap<>();
            pluginParameters.put(RodaConstants.PLUGIN_PARAMS_AIP_ID, aip.getId());
            pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
            pluginParameters.put(RodaConstants.PLUGIN_PARAMS_EVENT_DESCRIPTION, eventDescription);
            pluginParameters.put(RodaConstants.PLUGIN_PARAMS_OUTCOME_TEXT,
                    "AIP " + indexedAIP.getId() + " permissions were updated and all sublevels will be too");
            job.setPluginParameters(pluginParameters);

            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        }
    }

    public static void updateDIPPermissions(IndexedDIP indexedDIP, Permissions permissions, String details)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        // TODO 20170222 nvieira it should create an event associated with DIP
        ModelService model = RodaCoreFactory.getModelService();
        DIP dip = model.retrieveDIP(indexedDIP.getId());
        dip.setPermissions(permissions);
        model.updateDIPPermissions(dip);
    }

    public static Risk createRisk(Risk risk, User user, boolean commit)
            throws GenericException, RequestNotValidException {
        risk.setCreatedBy(user.getName());
        risk.setUpdatedBy(user.getName());
        return RodaCoreFactory.getModelService().createRisk(risk, commit);
    }

    public static void updateRisk(Risk risk, User user, Map<String, String> properties, boolean commit,
            int incidences) throws GenericException, RequestNotValidException {
        risk.setUpdatedBy(user.getName());
        RodaCoreFactory.getModelService().updateRisk(risk, properties, commit, incidences);
    }

    public static Format createFormat(Format format, boolean commit)
            throws GenericException, RequestNotValidException {
        return RodaCoreFactory.getModelService().createFormat(format, commit);
    }

    public static void updateFormat(Format format, boolean commit)
            throws GenericException, RequestNotValidException {
        RodaCoreFactory.getModelService().updateFormat(format, commit);
    }

    public static RiskVersionsBundle retrieveRiskVersions(String riskId) throws RequestNotValidException,
            GenericException, NotFoundException, AuthorizationDeniedException, IOException {
        StoragePath storagePath = ModelUtils.getRiskStoragePath(riskId);
        CloseableIterable<BinaryVersion> iterable = RodaCoreFactory.getStorageService()
                .listBinaryVersions(storagePath);
        List<BinaryVersionBundle> versionList = new ArrayList<>();
        boolean versionFlag = false;
        Date newestDate = new Date();
        Binary lastRiskBinary = null;

        for (BinaryVersion bv : iterable) {
            versionList.add(new BinaryVersionBundle(bv.getId(), bv.getCreatedDate(), bv.getProperties()));

            if (!versionFlag) {
                lastRiskBinary = bv.getBinary();
                newestDate = bv.getCreatedDate();
                versionFlag = true;
            } else if (newestDate.before(bv.getCreatedDate())) {
                lastRiskBinary = bv.getBinary();
                newestDate = bv.getCreatedDate();
            }
        }

        iterable.close();

        if (lastRiskBinary != null) {
            Risk lastRisk = JsonUtils.getObjectFromJson(lastRiskBinary.getContent().createInputStream(),
                    Risk.class);
            return new RiskVersionsBundle(lastRisk, versionList);
        } else {
            return new RiskVersionsBundle();
        }
    }

    public static boolean hasRiskVersions(String id)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getRiskStoragePath(id);
        CloseableIterable<BinaryVersion> iterable = RodaCoreFactory.getStorageService()
                .listBinaryVersions(storagePath);
        boolean hasRiskVersion = iterable.iterator().hasNext();
        IOUtils.closeQuietly(iterable);
        return hasRiskVersion;
    }

    public static void revertRiskVersion(String riskId, String versionId, Map<String, String> properties,
            int incidences)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        RodaCoreFactory.getModelService().revertRiskVersion(riskId, versionId, properties, false, incidences);
    }

    public static void deleteRiskVersion(String riskId, String versionId)
            throws NotFoundException, GenericException, RequestNotValidException {
        StoragePath storagePath = ModelUtils.getRiskStoragePath(riskId);
        RodaCoreFactory.getStorageService().deleteBinaryVersion(storagePath, versionId);
    }

    public static Risk retrieveRiskVersion(String riskId, String selectedVersion) throws RequestNotValidException,
            GenericException, NotFoundException, AuthorizationDeniedException, IOException {
        BinaryVersion bv = RodaCoreFactory.getModelService().retrieveVersion(riskId, selectedVersion);
        return JsonUtils.getObjectFromJson(bv.getBinary().getContent().createInputStream(), Risk.class);
    }

    public static void validateExportAIPParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN));
        }
    }

    public static void validateListingParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException("Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT
                    + "' value. Expected values: " + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                            RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML));
        }

    }

    public static void validateCreateAndUpdateParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            throw new RequestNotValidException("Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT
                    + "' value. Expected values: " + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                            RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML));
        }

    }

    public static void validateGetAIPParams(String acceptFormat) throws RequestNotValidException {
        if (!RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)
                && !RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            throw new RequestNotValidException(
                    "Invalid '" + RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT + "' value. Expected values: "
                            + Arrays.asList(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML,
                                    RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP));
        }

    }

    public static StreamResponse retrieveAIPPart(IndexedAIP aip, String part)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        String aipId = aip.getId();

        if (RodaConstants.STORAGE_DIRECTORY_SUBMISSION.equals(part)) {
            Directory directory = RodaCoreFactory.getModelService().getSubmissionDirectory(aipId);
            return ApiUtils.download(directory, part);
        } else if (RodaConstants.STORAGE_DIRECTORY_DOCUMENTATION.equals(part)) {
            Directory directory = RodaCoreFactory.getModelService().getDocumentationDirectory(aipId);
            return ApiUtils.download(directory, part);
        } else if (RodaConstants.STORAGE_DIRECTORY_SCHEMAS.equals(part)) {
            Directory directory = RodaCoreFactory.getModelService().getSchemasDirectory(aipId);
            return ApiUtils.download(directory, part);
        } else {
            throw new GenericException("Unsupported part: " + part);
        }
    }

    public static StreamResponse retrieveAIPs(SelectedItems<IndexedAIP> selected, String acceptFormat)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException,
            IOException {
        IndexService index = RodaCoreFactory.getIndexService();
        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            List<ZipEntryInfo> zipEntries = new ArrayList<>();
            if (selected instanceof SelectedItemsFilter) {
                SelectedItemsFilter<IndexedAIP> selectedItems = (SelectedItemsFilter<IndexedAIP>) selected;
                long count = index.count(IndexedAIP.class, selectedItems.getFilter());
                for (int i = 0; i < count; i += RodaConstants.DEFAULT_PAGINATION_VALUE) {
                    List<IndexedAIP> aips = index.find(IndexedAIP.class, selectedItems.getFilter(), null,
                            new Sublist(i, RodaConstants.DEFAULT_PAGINATION_VALUE), null).getResults();
                    zipEntries.addAll(ModelUtils.zipIndexedAIP(aips));
                }
            } else {
                SelectedItemsList<IndexedAIP> selectedItems = (SelectedItemsList<IndexedAIP>) selected;
                zipEntries.addAll(ModelUtils.zipIndexedAIP(ModelUtils.getIndexedAIPsFromObjectIds(selectedItems)));
            }
            return DownloadUtils.createZipStreamResponse(zipEntries, "export");
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)) {
            throw new GenericException("Not yet supported: " + acceptFormat);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static RiskMitigationBundle retrieveShowMitigationTerms(int preMitigationProbability,
            int preMitigationImpact, int posMitigationProbability, int posMitigationImpact) {

        int lowLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationSeverity", "lowLimit");
        int highLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationSeverity", "highLimit");

        String preProbability = RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationProbability",
                Integer.toString(preMitigationProbability));
        String preImpact = RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationImpact",
                Integer.toString(preMitigationImpact));
        String posProbability = RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationProbability",
                Integer.toString(posMitigationProbability));
        String posImpact = RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationImpact",
                Integer.toString(posMitigationImpact));

        return new RiskMitigationBundle(lowLimit, highLimit, preProbability, preImpact, posProbability, posImpact);
    }

    public static List<String> retrieveShowMitigationTerms() {
        List<String> terms = new ArrayList<>();
        terms.add(RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationSeverity", "lowLimit"));
        terms.add(RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationSeverity", "highLimit"));
        return terms;
    }

    public static MitigationPropertiesBundle retrieveAllMitigationProperties() {
        int lowLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationSeverity", "lowLimit");
        int highLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationSeverity", "highLimit");

        int probabilityLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationProbability",
                "limit");
        int impactLimit = RodaCoreFactory.getRodaConfigurationAsInt("ui", "risk", "mitigationImpact", "limit");

        // second list contains probability content
        List<String> probabilities = new ArrayList<>();
        for (int i = 0; i <= probabilityLimit; i++) {
            String value = Integer.toString(i);
            probabilities.add(
                    RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationProbability", value));
        }

        // third list contains impact content
        List<String> impacts = new ArrayList<>();
        for (int i = 0; i <= impactLimit; i++) {
            String value = Integer.toString(i);
            impacts.add(RodaCoreFactory.getRodaConfigurationAsString("ui", "risk", "mitigationImpact", value));
        }

        return new MitigationPropertiesBundle(lowLimit, highLimit, probabilities, impacts);
    }

    public static void deleteRisk(User user, SelectedItems<IndexedRisk> selected)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException,
            InvalidParameterException, JobAlreadyStartedException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Delete risks");
        job.setSourceObjects(selected);
        job.setPlugin(DeleteRODAObjectPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute risk delete action", e);
        }
    }

    public static void deleteFormat(User user, SelectedItems<Format> selected)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Delete formats");
        job.setSourceObjects(selected);
        job.setPlugin(DeleteRODAObjectPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute format delete action", e);
        }
    }

    public static void updateRiskCounters() throws GenericException, RequestNotValidException, NotFoundException {
        IndexService index = RodaCoreFactory.getIndexService();

        IndexResult<RiskIncidence> findAllRiskIncidences = index.find(RiskIncidence.class, Filter.ALL, Sorter.NONE,
                new Sublist(0, 0), new Facets(new SimpleFacetParameter(RodaConstants.RISK_INCIDENCE_RISK_ID)),
                Arrays.asList(RodaConstants.INDEX_UUID));

        Filter filter = new Filter(new SimpleFilterParameter(RodaConstants.RISK_INCIDENCE_STATUS,
                INCIDENCE_STATUS.UNMITIGATED.toString()));
        IndexResult<RiskIncidence> findNotMitigatedRiskIncidences = index.find(RiskIncidence.class, filter,
                Sorter.NONE, new Sublist(0, 0),
                new Facets(new SimpleFacetParameter(RodaConstants.RISK_INCIDENCE_RISK_ID)),
                Arrays.asList(RodaConstants.INDEX_UUID));

        Map<String, IndexedRisk> allRisks = new HashMap<>();

        // retrieve risks and set default object count to zero
        IterableIndexResult<IndexedRisk> risks = index.findAll(IndexedRisk.class, Filter.ALL, new ArrayList<>());
        for (IndexedRisk indexedRisk : risks) {
            indexedRisk.setIncidencesCount(0);
            indexedRisk.setUnmitigatedIncidencesCount(0);
            allRisks.put(indexedRisk.getId(), indexedRisk);
        }

        // update risks from facets (all incidences)
        for (FacetFieldResult fieldResult : findAllRiskIncidences.getFacetResults()) {
            for (FacetValue facetValue : fieldResult.getValues()) {
                String riskId = facetValue.getValue();
                long counter = facetValue.getCount();

                IndexedRisk risk = allRisks.get(riskId);
                if (risk != null) {
                    risk.setIncidencesCount((int) counter);
                } else {
                    LOGGER.warn("Updating risk counters found incidences pointing to non-existing risk: {}",
                            riskId);
                }
            }
        }

        // update risks from facets (not mitigated incidences)
        for (FacetFieldResult fieldResult : findNotMitigatedRiskIncidences.getFacetResults()) {
            for (FacetValue facetValue : fieldResult.getValues()) {
                String riskId = facetValue.getValue();
                long counter = facetValue.getCount();

                IndexedRisk risk = allRisks.get(riskId);
                if (risk != null) {
                    risk.setUnmitigatedIncidencesCount((int) counter);
                } else {
                    LOGGER.warn("Updating risk counters found incidences pointing to non-existing risk: {}",
                            riskId);
                }
            }
        }

        // update all in index
        for (IndexedRisk risk : allRisks.values()) {
            index.reindexRisk(risk);
        }

        index.commit(IndexedRisk.class);
    }

    public static void appraisal(User user, SelectedItems<IndexedAIP> selected, boolean accept, String rejectReason,
            Locale locale)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException {
        List<String> listOfIds = consolidate(user, IndexedAIP.class, selected);

        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();
        Date now = new Date();

        // map of job id -> (total, accepted)
        Map<String, Pair<Integer, Integer>> jobState = new HashMap<>();
        List<String> aipsToDelete = new ArrayList<>();

        String userAgentId;
        try {
            boolean notifyAgent = true;
            PreservationMetadata pm = PremisV3Utils.createPremisUserAgentBinary(user.getName(), model, index,
                    notifyAgent);
            userAgentId = pm.getId();
        } catch (AlreadyExistsException e) {
            userAgentId = IdUtils.getUserAgentId(user.getName());
        } catch (ValidationException e) {
            throw new GenericException(e);
        }

        for (String aipId : listOfIds) {
            AIP aip = model.retrieveAIP(aipId);
            String jobId = aip.getIngestJobId();
            if (accept) {
                // Accept AIP
                aip.setState(AIPState.ACTIVE);
                model.updateAIPState(aip, user.getName());

                // create preservation event
                String id = IdUtils.createPreservationMetadataId(PreservationMetadataType.EVENT);
                PreservationEventType type = PreservationEventType.ACCESSION;
                String preservationEventDescription = AutoAcceptSIPPlugin.DESCRIPTION;
                List<LinkingIdentifier> sources = new ArrayList<>();
                List<LinkingIdentifier> outcomes = Arrays.asList(PluginHelper.getLinkingIdentifier(aipId,
                        RodaConstants.PRESERVATION_LINKING_OBJECT_OUTCOME));
                PluginState outcome = PluginState.SUCCESS;
                String outcomeDetailNote = AutoAcceptSIPPlugin.SUCCESS_MESSAGE;
                String outcomeDetailExtension = null;
                boolean notifyEvent = true;
                try {
                    ContentPayload premisEvent = PremisV3Utils.createPremisEventBinary(id, now, type.toString(),
                            preservationEventDescription, sources, outcomes, outcome.name(), outcomeDetailNote,
                            outcomeDetailExtension, Arrays.asList(userAgentId));

                    model.createPreservationMetadata(PreservationMetadataType.EVENT, id, aipId, null, null, null,
                            premisEvent, notifyEvent);
                } catch (AlreadyExistsException | ValidationException e) {
                    throw new GenericException(e);
                }

            } else {
                // Reject AIP
                model.deleteAIP(aipId);
                aipsToDelete.add(aipId);
            }

            // create job report
            Job job = model.retrieveJob(jobId);
            Report report = model.retrieveJobReport(jobId, aipId, true);

            Report reportItem = new Report();
            Messages messages = RodaCoreFactory.getI18NMessages(locale);
            reportItem.setTitle(messages.getTranslation(RodaConstants.I18N_UI_APPRAISAL));
            reportItem.setPlugin(messages.getTranslation(RodaConstants.I18N_UI_APPRAISAL));
            reportItem.setPluginDetails(rejectReason);
            reportItem.setPluginState(accept ? PluginState.SUCCESS : PluginState.FAILURE);
            reportItem.setOutcomeObjectState(accept ? AIPState.ACTIVE : AIPState.DELETED);
            reportItem.setDateCreated(now);
            report.addReport(reportItem);

            model.createOrUpdateJobReport(report, job);

            // save job state
            Pair<Integer, Integer> pair = jobState.get(jobId);
            if (pair == null) {
                jobState.put(jobId, Pair.of(1, accept ? 1 : 0));
            } else {
                jobState.put(jobId, Pair.of(pair.getFirst() + 1, pair.getSecond() + (accept ? 1 : 0)));
            }
        }

        // update job counters
        for (Entry<String, Pair<Integer, Integer>> entry : jobState.entrySet()) {
            String jobId = entry.getKey();
            int total = entry.getValue().getFirst();
            int accepted = entry.getValue().getSecond();
            int rejected = total - accepted;
            Job job = model.retrieveJob(jobId);
            if (rejected > 0) {
                // change counter to failure
                job.getJobStats().setSourceObjectsProcessedWithSuccess(
                        job.getJobStats().getSourceObjectsProcessedWithSuccess() - rejected);
                job.getJobStats().setSourceObjectsProcessedWithFailure(
                        job.getJobStats().getSourceObjectsProcessedWithFailure() + rejected);
            }

            // decrement manual interaction counter
            job.getJobStats().setOutcomeObjectsWithManualIntervention(
                    job.getJobStats().getOutcomeObjectsWithManualIntervention() - total);

            model.createOrUpdateJob(job);

        }

        RodaCoreFactory.getIndexService().commit(IndexedAIP.class, Job.class, IndexedReport.class,
                IndexedPreservationEvent.class);
    }

    public static String retrieveDescriptiveMetadataPreview(SupportedMetadataTypeBundle bundle)
            throws GenericException {
        String rawTemplate = bundle.getTemplate();
        String result;
        if (StringUtils.isNotBlank(rawTemplate)) {

            Map<String, String> data = new HashMap<>();
            Set<MetadataValue> values = bundle.getValues();
            if (values != null) {
                values.forEach(metadataValue -> {
                    String val = metadataValue.get("value");
                    if (val != null) {
                        val = val.replaceAll("\\s", "");
                        if (!"".equals(val)) {
                            data.put(metadataValue.get("name"), metadataValue.get("value"));
                        }
                    }
                });
            }

            result = HandlebarsUtility.executeHandlebars(rawTemplate, data);
            // result = RodaUtils.indentXML(result);

        } else {
            result = rawTemplate;
        }
        return result;
    }

    public static String renameTransferredResource(String transferredResourceId, String newName)
            throws GenericException, RequestNotValidException, AlreadyExistsException, IsStillUpdatingException,
            NotFoundException {
        List<String> resourceFields = Arrays.asList(RodaConstants.INDEX_UUID,
                RodaConstants.TRANSFERRED_RESOURCE_FULLPATH, RodaConstants.TRANSFERRED_RESOURCE_PARENT_UUID);

        Filter filter = new Filter(new SimpleFilterParameter(RodaConstants.INDEX_UUID, transferredResourceId));
        IndexResult<TransferredResource> resources = RodaCoreFactory.getIndexService()
                .find(TransferredResource.class, filter, Sorter.NONE, new Sublist(0, 1), resourceFields);

        if (!resources.getResults().isEmpty()) {
            TransferredResource resource = resources.getResults().get(0);
            return RodaCoreFactory.getTransferredResourcesScanner().renameTransferredResource(resource, newName,
                    true, true);
        } else {
            return transferredResourceId;
        }
    }

    public static IndexedFile renameFolder(User user, String folderUUID, String newName, String details)
            throws GenericException, RequestNotValidException, AlreadyExistsException, NotFoundException,
            AuthorizationDeniedException {
        String eventDescription = "The process of updating an object of the repository.";

        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();
        IndexedFile ifolder = index.retrieve(IndexedFile.class, folderUUID, RodaConstants.FILE_FIELDS_TO_RETURN);
        String oldName = ifolder.getId();

        try {
            File folder = model.retrieveFile(ifolder.getAipId(), ifolder.getRepresentationId(), ifolder.getPath(),
                    ifolder.getId());
            File newFolder = model.renameFolder(folder, newName, true, true);
            String outcomeText = "The folder '" + oldName + "' has been manually renamed to '" + newName + "'.";
            model.createUpdateAIPEvent(ifolder.getAipId(), ifolder.getRepresentationId(), null, null,
                    PreservationEventType.UPDATE, eventDescription, PluginState.SUCCESS, outcomeText, details,
                    user.getName(), true);

            index.commitAIPs();
            return index.retrieve(IndexedFile.class, IdUtils.getFileId(newFolder),
                    RodaConstants.FILE_FIELDS_TO_RETURN);
        } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException e) {
            String outcomeText = "The folder '" + oldName + "' has not been manually renamed to '" + newName + "'.";

            model.createUpdateAIPEvent(ifolder.getAipId(), ifolder.getRepresentationId(), null, null,
                    PreservationEventType.UPDATE, eventDescription, PluginState.FAILURE, outcomeText, details,
                    user.getName(), true);

            throw e;
        }
    }

    public static void moveFiles(User user, String aipId, String representationId,
            SelectedItems<IndexedFile> selectedFiles, IndexedFile toFolder, String details) throws GenericException,
            RequestNotValidException, AlreadyExistsException, NotFoundException, AuthorizationDeniedException {

        if (toFolder != null && (!toFolder.getAipId().equals(aipId)
                || !toFolder.getRepresentationId().equals(representationId))) {
            throw new RequestNotValidException("Cannot move to a file outside defined representation");
        }

        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Move files");
        job.setSourceObjects(selectedFiles);
        job.setPlugin(MovePlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        if (toFolder != null) {
            pluginParameters.put(RodaConstants.PLUGIN_PARAMS_ID, toFolder.getUUID());
        }
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_DETAILS, details);
        job.setPluginParameters(pluginParameters);

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException e) {
            LOGGER.error("Could not execute move job", e);
        }
    }

    public static String moveTransferredResource(User user, SelectedItems<TransferredResource> selected,
            TransferredResource transferredResource) throws GenericException, RequestNotValidException,
            AlreadyExistsException, IsStillUpdatingException, NotFoundException {

        String resourceRelativePath = "";
        String uuid = null;
        if (transferredResource != null) {
            resourceRelativePath = transferredResource.getRelativePath();
            uuid = transferredResource.getUUID();
        }

        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Move transferred resources");
        job.setSourceObjects(selected);
        job.setPlugin(MovePlugin.class.getCanonicalName());
        job.setPluginType(PluginType.INTERNAL);
        job.setUsername(user.getName());

        Map<String, String> pluginParameters = new HashMap<>();
        pluginParameters.put(RodaConstants.PLUGIN_PARAMS_ID, resourceRelativePath);
        job.setPluginParameters(pluginParameters);

        try {
            RodaCoreFactory.getModelService().createJob(job);
            RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
        } catch (JobAlreadyStartedException | AuthorizationDeniedException e) {
            LOGGER.error("Could not execute move job", e);
        }

        return uuid;
    }

    public static IndexedFile createFolder(User user, String aipId, String representationId, String folderUUID,
            String newName, String details) throws GenericException, RequestNotValidException,
            AlreadyExistsException, NotFoundException, AuthorizationDeniedException {
        String eventDescription = "The process of creating an object of the repository.";

        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();
        File newFolder;
        IndexedRepresentation irep = index.retrieve(IndexedRepresentation.class,
                IdUtils.getRepresentationId(aipId, representationId),
                RodaConstants.REPRESENTATION_FIELDS_TO_RETURN);

        try {
            if (folderUUID != null) {
                IndexedFile ifolder = index.retrieve(IndexedFile.class, folderUUID,
                        RodaConstants.FILE_FIELDS_TO_RETURN);
                newFolder = model.createFile(ifolder.getAipId(), ifolder.getRepresentationId(), ifolder.getPath(),
                        ifolder.getId(), newName, true);
            } else {
                newFolder = model.createFile(irep.getAipId(), irep.getId(), null, null, newName, true);
            }

            String outcomeText = "The folder '" + newName + "' has been manually created.";
            model.createUpdateAIPEvent(aipId, irep.getId(), null, null, PreservationEventType.CREATION,
                    eventDescription, PluginState.SUCCESS, outcomeText, details, user.getName(), true);

            index.commit(IndexedFile.class);
            return index.retrieve(IndexedFile.class, IdUtils.getFileId(newFolder), new ArrayList<>());
        } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException e) {
            String outcomeText = "The folder '" + newName + "' has not been manually created.";
            model.createUpdateAIPEvent(aipId, irep.getId(), null, null, PreservationEventType.CREATION,
                    eventDescription, PluginState.FAILURE, outcomeText, details, user.getName(), true);

            throw e;
        }
    }

    public static List<TransferredResource> retrieveSelectedTransferredResource(
            SelectedItems<TransferredResource> selected) throws GenericException, RequestNotValidException {
        if (selected instanceof SelectedItemsList) {
            SelectedItemsList<TransferredResource> selectedList = (SelectedItemsList<TransferredResource>) selected;
            Filter filter = new Filter(
                    new OneOfManyFilterParameter(RodaConstants.INDEX_UUID, selectedList.getIds()));
            IndexResult<TransferredResource> iresults = RodaCoreFactory.getIndexService().find(
                    TransferredResource.class, filter, Sorter.NONE, new Sublist(0, selectedList.getIds().size()),
                    new ArrayList<>());
            return iresults.getResults();
        } else if (selected instanceof SelectedItemsFilter) {
            SelectedItemsFilter<TransferredResource> selectedFilter = (SelectedItemsFilter<TransferredResource>) selected;
            Long counter = RodaCoreFactory.getIndexService().count(TransferredResource.class,
                    selectedFilter.getFilter());
            IndexResult<TransferredResource> iresults = RodaCoreFactory.getIndexService().find(
                    TransferredResource.class, selectedFilter.getFilter(), Sorter.NONE,
                    new Sublist(0, counter.intValue()), new ArrayList<>());
            return iresults.getResults();
        } else {
            return new ArrayList<>();
        }
    }

    public static void updateRiskIncidence(RiskIncidence incidence) throws GenericException {
        RodaCoreFactory.getModelService().updateRiskIncidence(incidence, true);
    }

    protected static Reports listReports(int start, int limit)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        Sorter sorter = new Sorter(new SortParameter(RodaConstants.JOB_REPORT_DATE_UPDATED, true));
        IndexResult<IndexedReport> indexReports = RodaCoreFactory.getIndexService().find(IndexedReport.class,
                Filter.ALL, sorter, new Sublist(start, limit), new ArrayList<>());
        List<Report> results = indexReports.getResults().stream().map(ireport -> (Report) ireport)
                .collect(Collectors.toList());
        return new Reports(results);
    }

    protected static Reports listTransferredResourcesReports(String resourceId, int start, int limit)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        Filter filter = new Filter();
        filter.add(new SimpleFilterParameter(RodaConstants.JOB_REPORT_SOURCE_OBJECT_CLASS,
                TransferredResource.class.getName()));
        filter.add(new SimpleFilterParameter(RodaConstants.JOB_REPORT_SOURCE_OBJECT_ID, resourceId));
        filter.add(new OneOfManyFilterParameter(RodaConstants.JOB_REPORT_OUTCOME_OBJECT_CLASS,
                Arrays.asList(AIP.class.getName(), IndexedAIP.class.getName())));

        Sorter sorter = new Sorter(new SortParameter(RodaConstants.JOB_REPORT_DATE_UPDATED, true));
        IndexResult<IndexedReport> reports = RodaCoreFactory.getIndexService().find(IndexedReport.class, filter,
                sorter, new Sublist(start, limit), new ArrayList<>());
        List<Report> results = reports.getResults().stream().map(ireport -> (Report) ireport)
                .collect(Collectors.toList());
        return new Reports(results);
    }

    protected static Reports listTransferredResourcesReportsWithSIP(String sipId, int start, int limit)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        Filter filter = new Filter();
        filter.add(new OneOfManyFilterParameter(RodaConstants.JOB_REPORT_OUTCOME_OBJECT_CLASS,
                Arrays.asList(AIP.class.getName(), IndexedAIP.class.getName())));
        filter.add(new SimpleFilterParameter(RodaConstants.JOB_REPORT_SOURCE_OBJECT_ORIGINAL_IDS, sipId));

        Sorter sorter = new Sorter(new SortParameter(RodaConstants.JOB_REPORT_DATE_UPDATED, true));
        IndexResult<IndexedReport> indexReports = RodaCoreFactory.getIndexService().find(IndexedReport.class,
                filter, sorter, new Sublist(start, limit), new ArrayList<>());
        List<Report> results = indexReports.getResults().stream().map(ireport -> (Report) ireport)
                .collect(Collectors.toList());
        return new Reports(results);
    }

    public static void deleteRiskIncidences(User user, SelectedItems<RiskIncidence> selected)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException,
            InvalidParameterException, JobAlreadyStartedException {
        List<String> idList = consolidate(user, RiskIncidence.class, selected);
        for (String incidenceId : idList) {
            RodaCoreFactory.getModelService().deleteRiskIncidence(incidenceId, true);
        }
    }

    public static void updateMultipleIncidences(SelectedItems<RiskIncidence> selected, String status,
            String severity, Date mitigatedOn, String mitigatedBy, String mitigatedDescription)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        IndexService index = RodaCoreFactory.getIndexService();
        ModelService model = RodaCoreFactory.getModelService();

        if (selected instanceof SelectedItemsList) {
            SelectedItemsList<RiskIncidence> list = (SelectedItemsList<RiskIncidence>) selected;
            List<String> ids = list.getIds();

            for (String id : ids) {
                RiskIncidence incidence = RodaCoreFactory.getModelService().retrieveRiskIncidence(id);
                incidence.setStatus(INCIDENCE_STATUS.valueOf(status));
                incidence.setSeverity(SEVERITY_LEVEL.valueOf(severity));
                incidence.setMitigatedOn(mitigatedOn);
                incidence.setMitigatedBy(mitigatedBy);
                incidence.setMitigatedDescription(mitigatedDescription);
                model.updateRiskIncidence(incidence, false);
            }

            index.commit(RiskIncidence.class);
        } else if (selected instanceof SelectedItemsFilter) {
            SelectedItemsFilter<RiskIncidence> filter = (SelectedItemsFilter<RiskIncidence>) selected;

            int counter = index.count(RiskIncidence.class, filter.getFilter()).intValue();
            IndexResult<RiskIncidence> incidences = index.find(RiskIncidence.class, filter.getFilter(), Sorter.NONE,
                    new Sublist(0, counter), new ArrayList<>());

            for (RiskIncidence incidence : incidences.getResults()) {
                incidence.setStatus(INCIDENCE_STATUS.valueOf(status));
                incidence.setSeverity(SEVERITY_LEVEL.valueOf(severity));
                incidence.setMitigatedOn(mitigatedOn);
                incidence.setMitigatedBy(mitigatedBy);
                incidence.setMitigatedDescription(mitigatedDescription);
                model.updateRiskIncidence(incidence, false);
            }

            index.commit(RiskIncidence.class);
        }
    }

    public static TransferredResource reindexTransferredResource(String path)
            throws IsStillUpdatingException, NotFoundException, GenericException {
        TransferredResourcesScanner scanner = RodaCoreFactory.getTransferredResourcesScanner();
        scanner.updateTransferredResources(Optional.of(path), true);
        return RodaCoreFactory.getIndexService().retrieve(TransferredResource.class,
                IdUtils.getTransferredResourceUUID(path), new ArrayList<>());
    }

    public static DIP createDIP(DIP dip) throws GenericException, AuthorizationDeniedException {
        return RodaCoreFactory.getModelService().createDIP(dip, true);
    }

    public static DIP updateDIP(DIP dip) throws GenericException, AuthorizationDeniedException, NotFoundException {
        return RodaCoreFactory.getModelService().updateDIP(dip);
    }

    public static void deleteDIPs(SelectedItems<IndexedDIP> selected, User user)
            throws GenericException, AuthorizationDeniedException, NotFoundException, RequestNotValidException {
        for (String dipId : consolidate(user, IndexedDIP.class, selected)) {
            RodaCoreFactory.getModelService().deleteDIP(dipId);
        }
        RodaCoreFactory.getIndexService().commit(IndexedDIP.class);
    }

    protected static EntityResponse retrieveDIP(String dipId, String acceptFormat)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)) {
            StorageService storage = RodaCoreFactory.getStorageService();
            StoragePath storagePath = ModelUtils.getDIPDataStoragePath(dipId);

            if (!storage.hasDirectory(storagePath)) {
                storagePath = ModelUtils.getDIPStoragePath(dipId);
            }

            return ApiUtils.download(storage.getDirectory(storagePath), dipId);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            DIP dip = RodaCoreFactory.getModelService().retrieveDIP(dipId);
            return new ObjectResponse<DIP>(acceptFormat, dip);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static EntityResponse retrieveDIPFile(String fileUuid, String acceptFormat)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {

        DIPFile iFile = RodaCoreFactory.getIndexService().retrieve(DIPFile.class, fileUuid,
                RodaConstants.DIPFILE_FIELDS_TO_RETURN);

        if (!iFile.isDirectory() && RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat)) {
            final String filename;
            final String mediaType;
            final ConsumesOutputStream stream;

            StorageService storage = RodaCoreFactory.getStorageService();
            Binary representationFileBinary = storage.getBinary(ModelUtils.getDIPFileStoragePath(iFile));
            filename = representationFileBinary.getStoragePath().getName();
            mediaType = RodaConstants.MEDIA_TYPE_WILDCARD;

            stream = new ConsumesOutputStream() {

                @Override
                public String getMediaType() {
                    return acceptFormat;
                }

                @Override
                public String getFileName() {
                    return filename;
                }

                @Override
                public void consumeOutputStream(OutputStream out) throws IOException {
                    InputStream fileInputStream = null;
                    try {
                        fileInputStream = representationFileBinary.getContent().createInputStream();
                        IOUtils.copy(fileInputStream, out);
                    } finally {
                        IOUtils.closeQuietly(fileInputStream);
                    }
                }
            };
            return new StreamResponse(filename, mediaType, stream);
        } else if (iFile.isDirectory() && (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_ZIP.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN.equals(acceptFormat))) {
            StoragePath filePath = ModelUtils.getDIPFileStoragePath(iFile);
            Directory directory = RodaCoreFactory.getStorageService().getDirectory(filePath);
            return ApiUtils.download(directory);
        } else if (RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_JSON.equals(acceptFormat)
                || RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_XML.equals(acceptFormat)) {
            DIPFile file = RodaCoreFactory.getModelService().retrieveDIPFile(iFile.getDipId(), iFile.getPath(),
                    iFile.getId());
            return new ObjectResponse<DIPFile>(acceptFormat, file);
        } else {
            throw new GenericException("Unsupported accept format: " + acceptFormat);
        }
    }

    public static DIPFile createDIPFile(String dipId, List<String> directoryPath, String fileId, long size,
            ContentPayload content, boolean notify) throws GenericException, AuthorizationDeniedException,
            RequestNotValidException, NotFoundException, AlreadyExistsException {
        return RodaCoreFactory.getModelService().createDIPFile(dipId, directoryPath, fileId, size, content, notify);
    }

    public static String createDIPFolder(String dipId, String folderUUID, String newName) throws GenericException,
            RequestNotValidException, AlreadyExistsException, NotFoundException, AuthorizationDeniedException {
        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();

        if (folderUUID != null) {
            DIPFile ifolder = index.retrieve(DIPFile.class, folderUUID, RodaConstants.DIPFILE_FIELDS_TO_RETURN);
            DIPFile newFolder = model.createDIPFile(ifolder.getDipId(), ifolder.getPath(), ifolder.getId(), newName,
                    true);
            index.commit(DIPFile.class);
            return IdUtils.getDIPFileId(newFolder);
        } else {
            DIPFile newFolder = model.createDIPFile(dipId, null, null, newName, true);
            index.commit(DIPFile.class);
            return IdUtils.getDIPFileId(newFolder);
        }
    }

    public static DIPFile updateDIPFile(String dipId, List<String> directoryPath, String oldFileId, String fileId,
            long size, ContentPayload content, boolean notify) throws GenericException,
            AuthorizationDeniedException, NotFoundException, RequestNotValidException, AlreadyExistsException {
        return RodaCoreFactory.getModelService().updateDIPFile(dipId, directoryPath, oldFileId, fileId, size,
                content, true, notify);
    }

    public static void deleteDIPFiles(SelectedItems<DIPFile> selected, User user)
            throws AuthorizationDeniedException, GenericException, RequestNotValidException, NotFoundException {
        List<String> fileIds = consolidate(user, DIPFile.class, selected);

        Filter filter = new Filter();
        filter.add(new OneOfManyFilterParameter(RodaConstants.INDEX_UUID, fileIds));
        IndexResult<DIPFile> files = RodaCoreFactory.getIndexService().find(DIPFile.class, filter, Sorter.NONE,
                new Sublist(0, fileIds.size()), RodaConstants.DIPFILE_FIELDS_TO_RETURN);

        for (DIPFile file : files.getResults()) {
            RodaCoreFactory.getModelService().deleteDIPFile(file.getDipId(), file.getPath(), file.getId(), true);
        }

        RodaCoreFactory.getIndexService().commit(DIPFile.class);
    }

    public static void createFormatIdentificationJob(User user, SelectedItems<?> selected) throws GenericException,
            JobAlreadyStartedException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        Job job = new Job();
        job.setId(IdUtils.createUUID());
        job.setName("Format identification using Siegfried");
        job.setSourceObjects(selected);
        job.setPlugin(SiegfriedPlugin.class.getCanonicalName());
        job.setPluginType(PluginType.MISC);
        job.setUsername(user.getName());
        RodaCoreFactory.getModelService().createJob(job);
        RodaCoreFactory.getPluginOrchestrator().executeJob(job, true);
    }

    public static void changeRepresentationType(User user, SelectedItems<IndexedRepresentation> selected,
            String newType, String details)
            throws GenericException, AuthorizationDeniedException, RequestNotValidException, NotFoundException {
        String eventDescription = "The process of updating an object of the repository.";

        List<String> representationIds = consolidate(user, IndexedRepresentation.class, selected);
        ModelService model = RodaCoreFactory.getModelService();
        IndexService index = RodaCoreFactory.getIndexService();

        Filter filter = new Filter();
        filter.add(new OneOfManyFilterParameter(RodaConstants.INDEX_UUID, representationIds));
        IndexResult<IndexedRepresentation> reps = index.find(IndexedRepresentation.class, filter, Sorter.NONE,
                new Sublist(0, representationIds.size()), Arrays.asList(RodaConstants.REPRESENTATION_ID,
                        RodaConstants.REPRESENTATION_TYPE, RodaConstants.REPRESENTATION_AIP_ID));

        for (IndexedRepresentation irep : reps.getResults()) {
            String oldType = irep.getType();
            List<LinkingIdentifier> sources = new ArrayList<>();
            sources.add(PluginHelper.getLinkingIdentifier(irep.getAipId(), irep.getId(),
                    RodaConstants.PRESERVATION_LINKING_OBJECT_SOURCE));

            try {
                model.updateRepresentationType(irep.getAipId(), irep.getId(), newType);
                index.commit(IndexedRepresentation.class);
                StringBuilder outcomeText = new StringBuilder().append("The representation '").append(irep.getId())
                        .append("' changed its type from '").append(oldType).append("' to '").append(newType)
                        .append("'.");

                model.createEvent(irep.getAipId(), irep.getId(), null, null, PreservationEventType.UPDATE,
                        eventDescription, sources, null, PluginState.SUCCESS, outcomeText.toString(), details,
                        user.getName(), true);
            } catch (RequestNotValidException | NotFoundException | GenericException
                    | AuthorizationDeniedException e) {
                StringBuilder outcomeText = new StringBuilder().append("The representation '").append(irep.getId())
                        .append("' did not change its type from '").append(oldType).append("' to '").append(newType)
                        .append("'.");

                model.createEvent(irep.getAipId(), irep.getId(), null, null, PreservationEventType.UPDATE,
                        eventDescription, sources, null, PluginState.FAILURE, outcomeText.toString(), details,
                        user.getName(), true);
                throw e;
            }
        }
    }

    public static ObjectPermissionResult verifyPermissions(String username, String permissionType,
            MultivaluedMap<String, String> queryParams)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {
        ObjectPermissionResult result = new ObjectPermissionResult();
        for (Entry<String, List<String>> entry : queryParams.entrySet()) {
            String queryKey = entry.getKey();
            try {
                Class.forName(queryKey);
                for (String queryValues : entry.getValue()) {
                    boolean hasPermission = RodaCoreFactory.getModelService().checkObjectPermission(username,
                            permissionType, queryKey, queryValues);
                    result.addObject(new ObjectPermission(queryKey, queryValues, hasPermission));
                }
            } catch (ClassNotFoundException e) {
                // do nothing
            }
        }
        return result;
    }

    public static boolean hasDocumentation(String aipId)
            throws RequestNotValidException, AuthorizationDeniedException, GenericException {
        StoragePath aipPath = ModelUtils.getAIPStoragePath(aipId);
        StoragePath documentationPath = DefaultStoragePath.parse(aipPath,
                RodaConstants.STORAGE_DIRECTORY_DOCUMENTATION);
        try {
            Long counter = RodaCoreFactory.getStorageService().countResourcesUnderContainer(documentationPath,
                    false);
            return counter > 0;
        } catch (NotFoundException e) {
            return false;
        }
    }

}