org.roda.core.model.ModelService.java Source code

Java tutorial

Introduction

Here is the source code for org.roda.core.model.ModelService.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.core.model;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.PremisV3Utils;
import org.roda.core.common.UserUtility;
import org.roda.core.common.dips.DIPUtils;
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.notifications.NotificationProcessor;
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.exceptions.AlreadyExistsException;
import org.roda.core.data.exceptions.AuthenticationDeniedException;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.EmailAlreadyExistsException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.IllegalOperationException;
import org.roda.core.data.exceptions.InvalidTokenException;
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.exceptions.UserAlreadyExistsException;
import org.roda.core.data.utils.JsonUtils;
import org.roda.core.data.utils.URNUtils;
import org.roda.core.data.v2.IsModelObject;
import org.roda.core.data.v2.IsRODAObject;
import org.roda.core.data.v2.LiteRODAObject;
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.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.Permissions;
import org.roda.core.data.v2.ip.Permissions.PermissionType;
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.IndexedPreservationAgent;
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.PreservationMetadata;
import org.roda.core.data.v2.ip.metadata.PreservationMetadata.PreservationMetadataType;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.data.v2.jobs.Report.PluginState;
import org.roda.core.data.v2.log.LogEntry;
import org.roda.core.data.v2.notifications.Notification;
import org.roda.core.data.v2.risks.Risk;
import org.roda.core.data.v2.risks.RiskIncidence;
import org.roda.core.data.v2.user.Group;
import org.roda.core.data.v2.user.RODAMember;
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.model.iterables.LogEntryFileSystemIterable;
import org.roda.core.model.iterables.LogEntryStorageIterable;
import org.roda.core.model.utils.ModelUtils;
import org.roda.core.model.utils.ResourceListUtils;
import org.roda.core.model.utils.ResourceParseUtils;
import org.roda.core.storage.Binary;
import org.roda.core.storage.BinaryVersion;
import org.roda.core.storage.ContentPayload;
import org.roda.core.storage.DefaultBinary;
import org.roda.core.storage.DefaultStoragePath;
import org.roda.core.storage.Directory;
import org.roda.core.storage.EmptyClosableIterable;
import org.roda.core.storage.Entity;
import org.roda.core.storage.Resource;
import org.roda.core.storage.StorageService;
import org.roda.core.storage.StringContentPayload;
import org.roda.core.storage.fs.FSPathContentPayload;
import org.roda.core.storage.fs.FSUtils;
import org.roda.core.util.HTTPUtility;
import org.roda.core.util.IdUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Class that "relates" Model & Storage
 * 
 * 
 * @author Luis Faria <lfaria@keep.pt>
 * @author Hlder Silva <hsilva@keep.pt>
 */
public class ModelService extends ModelObservable {

    private static final Logger LOGGER = LoggerFactory.getLogger(ModelService.class);
    private static final DateTimeFormatter LOG_NAME_DATE_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd");
    private static final boolean FAIL_IF_NO_DESCRIPTIVE_METADATA_SCHEMA = false;
    private final StorageService storage;
    private Object logFileLock = new Object();

    public ModelService(StorageService storage) {
        super();
        this.storage = storage;
        ensureAllContainersExist();
        ensureAllDiretoriesExist();
    }

    private void ensureAllContainersExist() {
        try {
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_AIP);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_PRESERVATION);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_ACTIONLOG);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_JOB);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_JOB_REPORT);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_RISK);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_RISK_INCIDENCE);
            createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_DIP);
        } catch (RequestNotValidException | GenericException | AuthorizationDeniedException e) {
            LOGGER.error("Error while ensuring that all containers exist", e);
        }

    }

    private void createContainerIfNotExists(String containerName)
            throws RequestNotValidException, GenericException, AuthorizationDeniedException {
        try {
            storage.createContainer(DefaultStoragePath.parse(containerName));
        } catch (AlreadyExistsException e) {
            // do nothing
        }
    }

    private void ensureAllDiretoriesExist() {
        try {
            createDirectoryIfNotExists(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_PRESERVATION,
                    RodaConstants.STORAGE_DIRECTORY_AGENTS));
        } catch (RequestNotValidException | GenericException | AuthorizationDeniedException e) {
            LOGGER.error("Error initializing directories", e);
        }
    }

    private void createDirectoryIfNotExists(StoragePath directoryPath)
            throws GenericException, AuthorizationDeniedException {
        try {
            storage.createDirectory(directoryPath);
        } catch (AlreadyExistsException e) {
            // do nothing
        }

    }

    public StorageService getStorage() {
        return storage;
    }

    /***************** AIP related *****************/
    /***********************************************/

    private void createAIPMetadata(AIP aip) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        createAIPMetadata(aip, ModelUtils.getAIPStoragePath(aip.getId()));
    }

    private void createAIPMetadata(AIP aip, StoragePath storagePath) throws RequestNotValidException,
            GenericException, AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        String json = JsonUtils.getJsonFromObject(aip);
        DefaultStoragePath metadataStoragePath = DefaultStoragePath.parse(storagePath,
                RodaConstants.STORAGE_AIP_METADATA_FILENAME);
        boolean asReference = false;
        storage.createBinary(metadataStoragePath, new StringContentPayload(json), asReference);
    }

    private void updateAIPMetadata(AIP aip)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        updateAIPMetadata(aip, ModelUtils.getAIPStoragePath(aip.getId()));
    }

    private void updateAIPMetadata(AIP aip, StoragePath storagePath)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        String json = JsonUtils.getJsonFromObject(aip);
        DefaultStoragePath metadataStoragePath = DefaultStoragePath.parse(storagePath,
                RodaConstants.STORAGE_AIP_METADATA_FILENAME);
        boolean asReference = false;
        boolean createIfNotExists = true;
        storage.updateBinaryContent(metadataStoragePath, new StringContentPayload(json), asReference,
                createIfNotExists);
    }

    private void updateDIPMetadata(DIP dip)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        updateDIPMetadata(dip, ModelUtils.getDIPStoragePath(dip.getId()));
    }

    public CloseableIterable<OptionalWithCause<AIP>> listAIPs()
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        final boolean recursive = false;

        final CloseableIterable<Resource> resourcesIterable = storage
                .listResourcesUnderContainer(ModelUtils.getAIPContainerPath(), recursive);

        return ResourceParseUtils.convert(getStorage(), resourcesIterable, AIP.class);
    }

    public AIP retrieveAIP(String aipId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
    }

    /**
     * Create a new AIP
     * 
     * @param aipId
     *          Suggested ID for the AIP, if <code>null</code> then an ID will be
     *          automatically generated. If ID cannot be allowed because it
     *          already exists or is not valid, another ID will be provided.
     * @param sourceStorage
     * @param sourcePath
     * @return
     * @throws RequestNotValidException
     * @throws GenericException
     * @throws NotFoundException
     * @throws AuthorizationDeniedException
     * @throws AlreadyExistsException
     * @throws ValidationException
     */
    public AIP createAIP(String aipId, StorageService sourceStorage, StoragePath sourcePath, boolean notify,
            String createdBy) throws RequestNotValidException, GenericException, AuthorizationDeniedException,
            AlreadyExistsException, NotFoundException, ValidationException {
        // XXX possible optimization would be to allow move between storage
        ModelService sourceModelService = new ModelService(sourceStorage);
        AIP aip;

        Directory sourceDirectory = sourceStorage.getDirectory(sourcePath);
        ValidationReport validationReport = isAIPvalid(sourceModelService, sourceDirectory,
                FAIL_IF_NO_DESCRIPTIVE_METADATA_SCHEMA);
        if (validationReport.isValid()) {

            storage.copy(sourceStorage, sourcePath, ModelUtils.getAIPStoragePath(aipId));
            Directory newDirectory = storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));

            aip = ResourceParseUtils.getAIPMetadata(getStorage(), newDirectory.getStoragePath());
            aip.setCreatedBy(createdBy);
            aip.setCreatedOn(new Date());
            aip.setUpdatedBy(createdBy);
            aip.setUpdatedOn(new Date());

            if (notify) {
                notifyAipCreated(aip);
            }
        } else {
            throw new ValidationException(validationReport);
        }

        return aip;
    }

    public AIP createAIP(String parentId, String type, Permissions permissions, List<String> ingestSIPIds,
            String ingestJobId, boolean notify, String createdBy, boolean isGhost) throws RequestNotValidException,
            NotFoundException, GenericException, AlreadyExistsException, AuthorizationDeniedException {

        AIPState state = AIPState.ACTIVE;
        Directory directory = storage
                .createRandomDirectory(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_AIP));
        String id = directory.getStoragePath().getName();
        Permissions inheritedPermissions = this.addParentPermissions(permissions, parentId);

        AIP aip = new AIP(id, parentId, type, state, inheritedPermissions, createdBy);

        aip.setGhost(isGhost);
        aip.setIngestSIPIds(ingestSIPIds);
        aip.setIngestJobId(ingestJobId);

        createAIPMetadata(aip);

        if (notify) {
            notifyAipCreated(aip);
        }

        return aip;
    }

    public AIP createAIP(String parentId, String type, Permissions permissions, String createdBy)
            throws RequestNotValidException, NotFoundException, GenericException, AlreadyExistsException,
            AuthorizationDeniedException {
        AIPState state = AIPState.ACTIVE;
        boolean notify = true;
        return createAIP(state, parentId, type, permissions, notify, createdBy);
    }

    public AIP createAIP(AIPState state, String parentId, String type, Permissions permissions, String createdBy)
            throws RequestNotValidException, NotFoundException, GenericException, AlreadyExistsException,
            AuthorizationDeniedException {
        boolean notify = true;
        return createAIP(state, parentId, type, permissions, notify, createdBy);
    }

    public AIP createAIP(AIPState state, String parentId, String type, Permissions permissions, boolean notify,
            String createdBy) throws RequestNotValidException, NotFoundException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException {

        Directory directory = storage
                .createRandomDirectory(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_AIP));
        String id = directory.getStoragePath().getName();
        Permissions inheritedPermissions = this.addParentPermissions(permissions, parentId);

        AIP aip = new AIP(id, parentId, type, state, inheritedPermissions, createdBy);
        createAIPMetadata(aip);

        if (notify) {
            notifyAipCreated(aip);
        }

        return aip;
    }

    public AIP createAIP(AIPState state, String parentId, String type, Permissions permissions,
            List<String> ingestSIPIds, String ingestJobId, boolean notify, String createdBy)
            throws RequestNotValidException, NotFoundException, GenericException, AlreadyExistsException,
            AuthorizationDeniedException {

        Directory directory = storage
                .createRandomDirectory(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_AIP));
        String id = directory.getStoragePath().getName();
        Permissions inheritedPermissions = this.addParentPermissions(permissions, parentId);

        AIP aip = new AIP(id, parentId, type, state, inheritedPermissions, createdBy).setIngestSIPIds(ingestSIPIds)
                .setIngestJobId(ingestJobId);

        createAIPMetadata(aip);

        if (notify) {
            notifyAipCreated(aip);
        }

        return aip;
    }

    public AIP createAIP(String aipId, StorageService sourceStorage, StoragePath sourcePath, String createdBy)
            throws RequestNotValidException, GenericException, AuthorizationDeniedException, AlreadyExistsException,
            NotFoundException, ValidationException {
        return createAIP(aipId, sourceStorage, sourcePath, true, createdBy);
    }

    public AIP notifyAipCreated(String aipId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        notifyAipCreated(aip);
        return aip;
    }

    public AIP notifyAipUpdated(String aipId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        notifyAipUpdated(aip);
        return aip;
    }

    private Permissions addParentPermissions(Permissions permissions, String parentId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        if (parentId != null) {
            AIP parentAIP = this.retrieveAIP(parentId);
            Set<String> parentGroupnames = parentAIP.getPermissions().getGroupnames();
            Set<String> parentUsernames = parentAIP.getPermissions().getUsernames();
            Set<String> groupnames = permissions.getGroupnames();
            Set<String> usernames = permissions.getUsernames();

            for (String user : parentUsernames) {
                if (!usernames.contains(user)) {
                    permissions.setUserPermissions(user, parentAIP.getPermissions().getUserPermissions(user));
                }
            }

            for (String group : parentGroupnames) {
                if (!groupnames.contains(group)) {
                    permissions.setGroupPermissions(group, parentAIP.getPermissions().getGroupPermissions(group));
                }
            }
        }

        return permissions;
    }

    public AIP updateAIP(String aipId, StorageService sourceStorage, StoragePath sourcePath, String updatedBy)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException,
            AlreadyExistsException, ValidationException {
        // TODO verify structure of source AIP and update it in the storage
        ModelService sourceModelService = new ModelService(sourceStorage);
        AIP aip;

        Directory sourceDirectory = sourceStorage.getDirectory(sourcePath);
        ValidationReport validationReport = isAIPvalid(sourceModelService, sourceDirectory,
                FAIL_IF_NO_DESCRIPTIVE_METADATA_SCHEMA);
        if (validationReport.isValid()) {
            StoragePath aipPath = ModelUtils.getAIPStoragePath(aipId);

            // XXX possible optimization only creating new files, updating
            // changed and removing deleted ones.
            storage.deleteResource(aipPath);

            storage.copy(sourceStorage, sourcePath, aipPath);
            Directory directoryUpdated = storage.getDirectory(aipPath);

            aip = ResourceParseUtils.getAIPMetadata(getStorage(), directoryUpdated.getStoragePath());
            aip.setUpdatedBy(updatedBy);
            aip.setUpdatedOn(new Date());
            notifyAipUpdated(aip);
        } else {
            throw new ValidationException(validationReport);
        }

        return aip;
    }

    public AIP updateAIP(AIP aip, String updatedBy)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        aip.setUpdatedBy(updatedBy);
        aip.setUpdatedOn(new Date());
        updateAIPMetadata(aip);
        notifyAipUpdated(aip);
        return aip;
    }

    public AIP updateAIPState(AIP aip, String updatedBy)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        aip.setUpdatedBy(updatedBy);
        aip.setUpdatedOn(new Date());
        updateAIPMetadata(aip);

        notifyAipStateUpdated(aip);
        return aip;
    }

    public AIP moveAIP(String aipId, String parentId)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {

        if (aipId.equals(parentId)) {
            throw new RequestNotValidException("Cannot set itself as its parent: " + aipId);
        }

        // TODO ADD RESTRICTIONS
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        String oldParentId = aip.getParentId();
        aip.setParentId(parentId);
        updateAIPMetadata(aip);

        notifyAipMoved(aip, oldParentId, parentId);

        return aip;
    }

    public void deleteAIP(String aipId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath aipPath = ModelUtils.getAIPStoragePath(aipId);
        storage.deleteResource(aipPath);
        notifyAipDeleted(aipId);
    }

    private ValidationReport isAIPvalid(ModelService model, Directory directory,
            boolean failIfNoDescriptiveMetadataSchema)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        ValidationReport report = new ValidationReport();

        // validate metadata (against schemas)
        ValidationReport descriptiveMetadataValidationReport = ValidationUtils.isAIPDescriptiveMetadataValid(model,
                directory.getStoragePath().getName(), failIfNoDescriptiveMetadataSchema);

        report.setValid(descriptiveMetadataValidationReport.isValid());
        report.setIssues(descriptiveMetadataValidationReport.getIssues());

        // FIXME validate others aspects

        return report;
    }

    /***************** Descriptive Metadata related *****************/
    /****************************************************************/

    public Binary retrieveDescriptiveMetadataBinary(String aipId, String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        return retrieveDescriptiveMetadataBinary(aipId, null, descriptiveMetadataId);
    }

    public Binary retrieveDescriptiveMetadataBinary(String aipId, String representationId,
            String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        return storage.getBinary(binaryPath);
    }

    public DescriptiveMetadata retrieveDescriptiveMetadata(String aipId, String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return retrieveDescriptiveMetadata(aipId, null, descriptiveMetadataId);
    }

    public DescriptiveMetadata retrieveDescriptiveMetadata(String aipId, String representationId,
            String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);

        DescriptiveMetadata ret = null;
        for (DescriptiveMetadata descriptiveMetadata : getDescriptiveMetadata(aip, representationId)) {
            if (descriptiveMetadata.getId().equals(descriptiveMetadataId)) {
                ret = descriptiveMetadata;
                break;
            }
        }

        if (ret == null) {
            throw new NotFoundException("Could not find descriptive metadata: " + descriptiveMetadataId);
        }

        return ret;
    }

    public DescriptiveMetadata createDescriptiveMetadata(String aipId, String descriptiveMetadataId,
            ContentPayload payload, String descriptiveMetadataType, String descriptiveMetadataVersion,
            boolean notify) throws RequestNotValidException, GenericException, AlreadyExistsException,
            AuthorizationDeniedException, NotFoundException {
        return createDescriptiveMetadata(aipId, null, descriptiveMetadataId, payload, descriptiveMetadataType,
                descriptiveMetadataVersion, notify);
    }

    public DescriptiveMetadata createDescriptiveMetadata(String aipId, String descriptiveMetadataId,
            ContentPayload payload, String descriptiveMetadataType, String descriptiveMetadataVersion)
            throws RequestNotValidException, GenericException, AlreadyExistsException, AuthorizationDeniedException,
            NotFoundException {
        return createDescriptiveMetadata(aipId, null, descriptiveMetadataId, payload, descriptiveMetadataType,
                descriptiveMetadataVersion, true);
    }

    public DescriptiveMetadata createDescriptiveMetadata(String aipId, String representationId,
            String descriptiveMetadataId, ContentPayload payload, String descriptiveMetadataType,
            String descriptiveMetadataVersion) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        return createDescriptiveMetadata(aipId, representationId, descriptiveMetadataId, payload,
                descriptiveMetadataType, descriptiveMetadataVersion, true);
    }

    public DescriptiveMetadata createDescriptiveMetadata(String aipId, String representationId,
            String descriptiveMetadataId, ContentPayload payload, String descriptiveMetadataType,
            String descriptiveMetadataVersion, boolean notify) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {

        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        boolean asReference = false;

        storage.createBinary(binaryPath, payload, asReference);
        DescriptiveMetadata descriptiveMetadata = new DescriptiveMetadata(descriptiveMetadataId, aipId,
                representationId, descriptiveMetadataType, descriptiveMetadataVersion);

        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        aip.addDescriptiveMetadata(descriptiveMetadata);
        updateAIPMetadata(aip);

        if (notify) {
            notifyDescriptiveMetadataCreated(descriptiveMetadata);
        }

        return descriptiveMetadata;
    }

    public DescriptiveMetadata updateDescriptiveMetadata(String aipId, String descriptiveMetadataId,
            ContentPayload descriptiveMetadataPayload, String descriptiveMetadataType,
            String descriptiveMetadataVersion, Map<String, String> properties) throws RequestNotValidException,
            GenericException, NotFoundException, AuthorizationDeniedException, ValidationException {
        return updateDescriptiveMetadata(aipId, null, descriptiveMetadataId, descriptiveMetadataPayload,
                descriptiveMetadataType, descriptiveMetadataVersion, properties);
    }

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

        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        boolean asReference = false;
        boolean createIfNotExists = false;

        // Create version snapshot
        storage.createBinaryVersion(binaryPath, properties);

        // Update
        storage.updateBinaryContent(binaryPath, descriptiveMetadataPayload, asReference, createIfNotExists);

        // set descriptive metadata type
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        ret = updateDescriptiveMetadata(aip, representationId, descriptiveMetadataId, descriptiveMetadataType,
                descriptiveMetadataVersion);

        updateAIPMetadata(aip);
        notifyDescriptiveMetadataUpdated(ret);

        return ret;
    }

    private DescriptiveMetadata updateDescriptiveMetadata(AIP aip, String representationId,
            String descriptiveMetadataId, String descriptiveMetadataType, String descriptiveMetadataVersion) {
        DescriptiveMetadata descriptiveMetadata;

        Optional<DescriptiveMetadata> odm = getDescriptiveMetadata(aip, representationId).stream()
                .filter(dm -> dm.getId().equals(descriptiveMetadataId)).findFirst();
        if (odm.isPresent()) {
            descriptiveMetadata = odm.get();
            descriptiveMetadata.setType(descriptiveMetadataType);
            descriptiveMetadata.setVersion(descriptiveMetadataVersion);
        } else {
            descriptiveMetadata = new DescriptiveMetadata(descriptiveMetadataId, aip.getId(), representationId,
                    descriptiveMetadataType, descriptiveMetadataVersion);
            aip.addDescriptiveMetadata(descriptiveMetadata);
        }

        return descriptiveMetadata;
    }

    public void deleteDescriptiveMetadata(String aipId, String descriptiveMetadataId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        deleteDescriptiveMetadata(aipId, null, descriptiveMetadataId);
    }

    public void deleteDescriptiveMetadata(String aipId, String representationId, String descriptiveMetadataId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);

        storage.deleteResource(binaryPath);

        // update AIP metadata
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        deleteDescriptiveMetadata(aip, representationId, descriptiveMetadataId);

        updateAIPMetadata(aip);
        notifyDescriptiveMetadataDeleted(aipId, representationId, descriptiveMetadataId);

    }

    private void deleteDescriptiveMetadata(AIP aip, String representationId, String descriptiveMetadataId) {

        for (Iterator<DescriptiveMetadata> it = getDescriptiveMetadata(aip, representationId).iterator(); it
                .hasNext();) {
            DescriptiveMetadata descriptiveMetadata = it.next();
            if (descriptiveMetadata.getId().equals(descriptiveMetadataId)) {
                it.remove();
                break;
            }
        }
    }

    private List<DescriptiveMetadata> getDescriptiveMetadata(AIP aip, String representationId) {
        List<DescriptiveMetadata> descriptiveMetadataList = Collections.emptyList();
        if (representationId == null) {
            // AIP descriptive metadata
            descriptiveMetadataList = aip.getDescriptiveMetadata();
        } else {
            // Representation descriptive metadata
            Optional<Representation> oRep = aip.getRepresentations().stream()
                    .filter(rep -> rep.getId().equals(representationId)).findFirst();
            if (oRep.isPresent()) {
                descriptiveMetadataList = oRep.get().getDescriptiveMetadata();
            }
        }
        return descriptiveMetadataList;
    }

    public CloseableIterable<BinaryVersion> listDescriptiveMetadataVersions(String aipId,
            String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return listDescriptiveMetadataVersions(aipId, null, descriptiveMetadataId);
    }

    public CloseableIterable<BinaryVersion> listDescriptiveMetadataVersions(String aipId, String representationId,
            String descriptiveMetadataId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);
        return storage.listBinaryVersions(binaryPath);
    }

    public BinaryVersion revertDescriptiveMetadataVersion(String aipId, String descriptiveMetadataId,
            String versionId, Map<String, String> properties)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return revertDescriptiveMetadataVersion(aipId, null, descriptiveMetadataId, versionId, properties);
    }

    public BinaryVersion revertDescriptiveMetadataVersion(String aipId, String representationId,
            String descriptiveMetadataId, String versionId, Map<String, String> properties)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, representationId,
                descriptiveMetadataId);

        BinaryVersion currentVersion = storage.createBinaryVersion(binaryPath, properties);
        storage.revertBinaryVersion(binaryPath, versionId);

        notifyDescriptiveMetadataUpdated(retrieveDescriptiveMetadata(aipId, descriptiveMetadataId));

        return currentVersion;
    }

    public CloseableIterable<OptionalWithCause<DescriptiveMetadata>> listDescriptiveMetadata()
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        List<CloseableIterable<OptionalWithCause<DescriptiveMetadata>>> list = new ArrayList<>();

        CloseableIterable<OptionalWithCause<AIP>> aips = listAIPs();

        for (OptionalWithCause<AIP> oaip : aips) {
            if (oaip.isPresent()) {
                AIP aip = oaip.get();
                StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aip.getId(), null);

                CloseableIterable<OptionalWithCause<DescriptiveMetadata>> aipDescriptiveMetadata;
                try {
                    boolean recursive = true;
                    CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath,
                            recursive);
                    aipDescriptiveMetadata = ResourceParseUtils.convert(getStorage(), resources,
                            DescriptiveMetadata.class);
                } catch (NotFoundException e) {
                    // check if AIP exists
                    storage.getDirectory(ModelUtils.getAIPStoragePath(aip.getId()));
                    // if no exception was sent by above method, return empty list
                    aipDescriptiveMetadata = new EmptyClosableIterable<>();
                }

                list.add(aipDescriptiveMetadata);

                // list from all representations
                for (Representation representation : aip.getRepresentations()) {
                    CloseableIterable<OptionalWithCause<DescriptiveMetadata>> rpm = listDescriptiveMetadata(
                            aip.getId(), representation.getId());
                    list.add(rpm);
                }
            }
        }

        return CloseableIterables.concat(list);
    }

    public CloseableIterable<OptionalWithCause<DescriptiveMetadata>> listDescriptiveMetadata(String aipId,
            boolean includeRepresentations)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getDescriptiveMetadataStoragePath(aipId, null);

        CloseableIterable<OptionalWithCause<DescriptiveMetadata>> aipDescriptiveMetadata;
        try {
            boolean recursive = true;
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            aipDescriptiveMetadata = ResourceParseUtils.convert(getStorage(), resources, DescriptiveMetadata.class);
        } catch (NotFoundException e) {
            // check if AIP exists
            storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));
            // if no exception was sent by above method, return empty list
            aipDescriptiveMetadata = new EmptyClosableIterable<>();
        }

        if (includeRepresentations) {
            List<CloseableIterable<OptionalWithCause<DescriptiveMetadata>>> list = new ArrayList<>();
            list.add(aipDescriptiveMetadata);

            // list from all representations
            AIP aip = retrieveAIP(aipId);
            for (Representation representation : aip.getRepresentations()) {
                CloseableIterable<OptionalWithCause<DescriptiveMetadata>> rpm = listDescriptiveMetadata(aipId,
                        representation.getId());
                list.add(rpm);
            }
            return CloseableIterables.concat(list);
        } else {
            return aipDescriptiveMetadata;
        }

    }

    public CloseableIterable<OptionalWithCause<DescriptiveMetadata>> listDescriptiveMetadata(String aipId,
            String representationId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getDescriptiveMetadataDirectoryStoragePath(aipId, representationId);

        boolean recursive = true;
        CloseableIterable<OptionalWithCause<DescriptiveMetadata>> ret;
        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            ret = ResourceParseUtils.convert(getStorage(), resources, DescriptiveMetadata.class);
        } catch (NotFoundException e) {
            // check if Representation exists
            storage.getDirectory(ModelUtils.getRepresentationStoragePath(aipId, representationId));
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }

        return ret;
    }

    /***************** Representation related *****************/
    /**********************************************************/

    public Representation retrieveRepresentation(String aipId, String representationId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);

        Representation ret = null;
        for (Representation representation : aip.getRepresentations()) {
            if (representation.getId().equals(representationId)) {
                ret = representation;
                break;
            }
        }

        if (ret == null) {
            throw new NotFoundException("Could not find representation: " + representationId);
        }

        return ret;
    }

    public Representation createRepresentation(String aipId, String representationId, boolean original, String type,
            boolean notify) throws RequestNotValidException, GenericException, NotFoundException,
            AuthorizationDeniedException, AlreadyExistsException {
        Representation representation = new Representation(representationId, aipId, original, type);

        StoragePath directoryPath = ModelUtils.getRepresentationStoragePath(aipId, representationId);
        storage.createDirectory(directoryPath);

        // update AIP metadata
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        aip.getRepresentations().add(representation);
        updateAIPMetadata(aip);

        if (notify) {
            notifyRepresentationCreated(representation);
        }

        return representation;
    }

    public Representation createRepresentation(String aipId, String representationId, boolean original, String type,
            StorageService sourceStorage, StoragePath sourcePath, boolean justData)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
            AlreadyExistsException, ValidationException {
        Representation representation;

        if (justData) {
            StoragePath dataPath = ModelUtils.getRepresentationDataStoragePath(aipId, representationId);
            StoragePath sourceDataPath = DefaultStoragePath.parse(sourcePath, RodaConstants.STORAGE_DIRECTORY_DATA);
            storage.copy(sourceStorage, sourceDataPath, dataPath);
        } else {
            StoragePath directoryPath = ModelUtils.getRepresentationStoragePath(aipId, representationId);

            // verify structure of source representation
            // 20170324 should we validate the representation???
            storage.copy(sourceStorage, sourcePath, directoryPath);
        }

        representation = new Representation(representationId, aipId, original, type);

        // update AIP metadata
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        aip.getRepresentations().add(representation);
        updateAIPMetadata(aip);

        notifyRepresentationCreated(representation);
        return representation;
    }

    public Representation updateRepresentationInfo(Representation representation) {
        notifyRepresentationUpdated(representation);
        return representation;
    }

    public void updateRepresentationType(String aipId, String representationId, String type)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        AIP aip = retrieveAIP(aipId);
        Iterator<Representation> it = aip.getRepresentations().iterator();

        while (it.hasNext()) {
            Representation representation = it.next();
            if (representation.getId().equals(representationId)) {
                representation.setType(type);
                notifyRepresentationUpdated(representation);
                break;
            }
        }

        updateAIPMetadata(aip);
    }

    public Representation updateRepresentation(String aipId, String representationId, boolean original, String type,
            StorageService sourceStorage, StoragePath sourcePath) throws RequestNotValidException,
            NotFoundException, GenericException, AuthorizationDeniedException, ValidationException {
        Representation representation;

        // XXX possible optimization only creating new files, updating
        // changed and removing deleted

        StoragePath representationPath = ModelUtils.getRepresentationStoragePath(aipId, representationId);
        storage.deleteResource(representationPath);
        try {
            storage.copy(sourceStorage, sourcePath, representationPath);
        } catch (AlreadyExistsException e) {
            throw new GenericException("Copying after delete gave an unexpected already exists exception", e);
        }

        // build return object
        representation = new Representation(representationId, aipId, original, type);
        notifyRepresentationUpdated(representation);
        return representation;
    }

    public void deleteRepresentation(String aipId, String representationId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath representationPath = ModelUtils.getRepresentationStoragePath(aipId, representationId);
        storage.deleteResource(representationPath);

        // update AIP metadata
        AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId);
        for (Iterator<Representation> it = aip.getRepresentations().iterator(); it.hasNext();) {
            Representation representation = it.next();
            if (representation.getId().equals(representationId)) {
                it.remove();
                break;
            }
        }
        updateAIPMetadata(aip);
        notifyRepresentationDeleted(aipId, representationId);
    }

    public CloseableIterable<OptionalWithCause<File>> listFilesUnder(String aipId, String representationId,
            boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {

        final StoragePath storagePath = ModelUtils.getRepresentationDataStoragePath(aipId, representationId);
        CloseableIterable<OptionalWithCause<File>> ret;
        try {
            final CloseableIterable<Resource> iterable = storage.listResourcesUnderDirectory(storagePath,
                    recursive);
            ret = ResourceParseUtils.convert(getStorage(), iterable, File.class);
        } catch (NotFoundException e) {
            // check if AIP exists
            storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }

        return ret;

    }

    /***************** File related *****************/
    /************************************************/

    public CloseableIterable<OptionalWithCause<File>> listFilesUnder(File f, boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        return listFilesUnder(f.getAipId(), f.getRepresentationId(), f.getPath(), f.getId(), recursive);
    }

    public CloseableIterable<OptionalWithCause<File>> listFilesUnder(String aipId, String representationId,
            List<String> directoryPath, String fileId, boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        final StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);
        final CloseableIterable<Resource> iterable = storage.listResourcesUnderDirectory(filePath, recursive);
        return ResourceParseUtils.convert(getStorage(), iterable, File.class);
    }

    public File retrieveFile(String aipId, String representationId, List<String> directoryPath, String fileId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        File file;
        StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);
        Class<? extends Entity> entity = storage.getEntity(filePath);

        if (entity.equals(Binary.class) || entity.equals(DefaultBinary.class)) {
            Binary binary = storage.getBinary(filePath);
            file = ResourceParseUtils.convertResourceToFile(binary);
        } else {
            Directory directory = storage.getDirectory(filePath);
            file = ResourceParseUtils.convertResourceToFile(directory);
        }

        return file;
    }

    public File createFile(String aipId, String representationId, List<String> directoryPath, String fileId,
            ContentPayload contentPayload) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        return createFile(aipId, representationId, directoryPath, fileId, contentPayload, true);
    }

    public File createFile(String aipId, String representationId, List<String> directoryPath, String fileId,
            ContentPayload contentPayload, boolean notify) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        boolean asReference = false;
        StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);

        final Binary createdBinary = storage.createBinary(filePath, contentPayload, asReference);
        File file = ResourceParseUtils.convertResourceToFile(createdBinary);

        if (notify) {
            notifyFileCreated(file);
        }

        return file;
    }

    public File createFile(String aipId, String representationId, List<String> directoryPath, String fileId,
            String dirName, boolean notify) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {

        StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);
        final Directory createdDirectory = storage.createDirectory(DefaultStoragePath.parse(filePath, dirName));
        File file = ResourceParseUtils.convertResourceToFile(createdDirectory);

        if (notify) {
            notifyFileCreated(file);
        }

        return file;
    }

    public File updateFile(String aipId, String representationId, List<String> directoryPath, String fileId,
            ContentPayload contentPayload, boolean createIfNotExists, boolean notify)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        boolean asReference = false;
        StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);

        storage.updateBinaryContent(filePath, contentPayload, asReference, createIfNotExists);
        Binary binaryUpdated = storage.getBinary(filePath);
        File file = ResourceParseUtils.convertResourceToFile(binaryUpdated);

        if (notify) {
            notifyFileUpdated(file);
        }

        return file;
    }

    public File updateFile(File file, ContentPayload contentPayload, boolean createIfNotExists, boolean notify)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return updateFile(file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId(), contentPayload,
                createIfNotExists, notify);
    }

    public void deleteFile(String aipId, String representationId, List<String> directoryPath, String fileId,
            boolean notify)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {

        StoragePath filePath = ModelUtils.getFileStoragePath(aipId, representationId, directoryPath, fileId);
        storage.deleteResource(filePath);

        if (notify) {
            notifyFileDeleted(aipId, representationId, directoryPath, fileId);
        }
    }

    public File renameFolder(File folder, String newName, boolean replaceExisting, boolean reindexResources)
            throws AlreadyExistsException, GenericException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException {

        Path basePath = RodaCoreFactory.getStoragePath();
        StoragePath fileStoragePath = ModelUtils.getFileStoragePath(folder);
        Path fullPath = basePath.resolve(FSUtils.getStoragePathAsString(fileStoragePath, false));

        if (FSUtils.exists(fullPath)) {
            FSUtils.move(fullPath, fullPath.getParent().resolve(newName), replaceExisting);

            if (reindexResources) {
                notifyAipUpdated(folder.getAipId());
            }

            return retrieveFile(folder.getAipId(), folder.getRepresentationId(), folder.getPath(), newName);
        } else {
            throw new NotFoundException("Folder was moved or does not exist");
        }
    }

    public File moveFile(File file, String newAipId, String newRepresentationId, List<String> newDirectoryPath,
            String newId, boolean replaceExisting, boolean reindexResources) throws AlreadyExistsException,
            GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {

        Path basePath = RodaCoreFactory.getStoragePath();

        StoragePath fileStoragePath = ModelUtils.getFileStoragePath(file);
        Path fullPath = basePath.resolve(FSUtils.getStoragePathAsString(fileStoragePath, false));

        if (!FSUtils.exists(fullPath)) {
            throw new NotFoundException("Some files/folders were moved or do not exist");
        }

        File newFile = new File(newId, newAipId, newRepresentationId, newDirectoryPath, file.isDirectory());
        StoragePath newFileStoragePath = ModelUtils.getFileStoragePath(newFile);
        Path newFullPath = basePath.resolve(FSUtils.getStoragePathAsString(newFileStoragePath, false));

        FSUtils.move(fullPath, newFullPath, replaceExisting);

        if (reindexResources) {
            notifyRepresentationUpdated(retrieveRepresentation(newAipId, newRepresentationId));
            if (!newAipId.equals(file.getAipId()) || !newRepresentationId.equals(file.getRepresentationId())) {
                notifyRepresentationUpdated(retrieveRepresentation(file.getAipId(), file.getRepresentationId()));
            }
        }

        return newFile;

    }

    /***************** Preservation related *****************/
    /********************************************************/

    public void createRepositoryEvent(PreservationEventType eventType, String eventDescription,
            PluginState outcomeState, String outcomeText, String outcomeDetail, String agentName, boolean notify) {
        createRepositoryEvent(eventType, eventDescription, null, null, outcomeState, outcomeText, outcomeDetail,
                agentName, notify);
    }

    public void createRepositoryEvent(PreservationEventType eventType, String eventDescription,
            List<LinkingIdentifier> sources, List<LinkingIdentifier> targets, PluginState outcomeState,
            String outcomeText, String outcomeDetail, String agentName, boolean notify) {
        createEvent(null, null, null, null, eventType, eventDescription, sources, targets, outcomeState,
                outcomeText, outcomeDetail, agentName, notify);
    }

    public void createUpdateAIPEvent(String aipId, String representationId, List<String> filePath, String fileId,
            PreservationEventType eventType, String eventDescription, PluginState outcomeState, String outcomeText,
            String outcomeDetail, String agentName, boolean notify) {
        createEvent(aipId, representationId, filePath, fileId, eventType, eventDescription, null, null,
                outcomeState, outcomeText, outcomeDetail, agentName, notify);
    }

    public void createEvent(String aipId, String representationId, List<String> filePath, String fileId,
            PreservationEventType eventType, String eventDescription, List<LinkingIdentifier> sources,
            List<LinkingIdentifier> targets, PluginState outcomeState, String outcomeText, String outcomeDetail,
            String agentName, boolean notify) {
        try {
            StringBuilder builder = new StringBuilder(outcomeText);
            if (StringUtils.isNotBlank(outcomeDetail) && outcomeState.equals(PluginState.SUCCESS)) {
                builder.append("\n").append("The following reason has been reported by the user: ")
                        .append(agentName).append("\n").append(outcomeDetail);
            }

            createEvent(aipId, representationId, filePath, fileId, eventType, eventDescription, sources, targets,
                    outcomeState, builder.toString(), "", Arrays.asList(IdUtils.getUserAgentId(agentName)), notify);
        } catch (ValidationException | AlreadyExistsException | GenericException | NotFoundException
                | RequestNotValidException | AuthorizationDeniedException e1) {
            LOGGER.error("Could not create an event for: " + eventDescription, e1);
        }
    }

    public void createEvent(String aipId, String representationId, List<String> filePath, String fileId,
            PreservationEventType eventType, String eventDescription, List<LinkingIdentifier> sources,
            List<LinkingIdentifier> targets, PluginState outcomeState, String outcomeDetail,
            String outcomeExtension, List<String> agentIds, boolean notify)
            throws GenericException, ValidationException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException, AlreadyExistsException {

        String id = IdUtils.createPreservationMetadataId(PreservationMetadataType.EVENT);
        ContentPayload premisEvent = PremisV3Utils.createPremisEventBinary(id, new Date(), eventType.toString(),
                eventDescription, sources, targets, outcomeState.toString(), outcomeDetail, outcomeExtension,
                agentIds);
        createPreservationMetadata(PreservationMetadataType.EVENT, id, aipId, representationId, filePath, fileId,
                premisEvent, notify);
    }

    public PreservationMetadata retrievePreservationMetadata(String aipId, String representationId,
            List<String> fileDirectoryPath, String fileId, PreservationMetadataType type) {
        PreservationMetadata pm = new PreservationMetadata();
        pm.setId(IdUtils.getPreservationId(type, aipId, representationId, fileDirectoryPath, fileId));
        pm.setAipId(aipId);
        pm.setRepresentationId(representationId);
        pm.setFileDirectoryPath(fileDirectoryPath);
        pm.setFileId(fileId);
        pm.setType(type);
        return pm;
    }

    public Binary retrievePreservationRepresentation(String aipId, String representationId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        String urn = IdUtils.getPreservationId(PreservationMetadataType.REPRESENTATION, aipId, representationId,
                null, null);
        StoragePath path = ModelUtils.getPreservationMetadataStoragePath(urn,
                PreservationMetadataType.REPRESENTATION, aipId, representationId);
        return storage.getBinary(path);
    }

    public Binary retrievePreservationRepresentation(String aipId, String representationId, List<String> filePath,
            String fileId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        String urn = IdUtils.getPreservationFileId(fileId);
        StoragePath path = ModelUtils.getPreservationMetadataStoragePath(urn, PreservationMetadataType.FILE, aipId,
                representationId, filePath, fileId);
        return storage.getBinary(path);
    }

    public Binary retrievePreservationFile(String aipId, String representationId, List<String> fileDirectoryPath,
            String fileId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return retrievePreservationFile(aipId, representationId, fileDirectoryPath, fileId,
                PreservationMetadataType.FILE);
    }

    public Binary retrievePreservationFile(String aipId, String representationId, List<String> fileDirectoryPath,
            String fileId, PreservationMetadataType type)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        String identifier = null;
        if (PreservationMetadataType.FILE.equals(type)) {
            identifier = IdUtils.getPreservationFileId(fileId);
        } else {
            identifier = IdUtils.getPreservationId(type, aipId, representationId, fileDirectoryPath, fileId);
        }
        StoragePath filePath = ModelUtils.getPreservationMetadataStoragePath(identifier, type, aipId,
                representationId, fileDirectoryPath, fileId);
        return storage.getBinary(filePath);
    }

    public Binary retrievePreservationFile(File file)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return retrievePreservationFile(file.getAipId(), file.getRepresentationId(), file.getPath(), file.getId());
    }

    public Binary retrievePreservationEvent(String aipId, String representationId, List<String> filePath,
            String fileId, String preservationID)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getPreservationMetadataStoragePath(preservationID,
                PreservationMetadataType.EVENT, aipId, representationId, filePath, fileId);
        return storage.getBinary(storagePath);
    }

    public Binary retrievePreservationAgent(String preservationID)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getPreservationMetadataStoragePath(preservationID,
                PreservationMetadataType.AGENT);
        return storage.getBinary(storagePath);
    }

    public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String aipId,
            String representationId, List<String> fileDirectoryPath, String fileId, ContentPayload payload,
            boolean notify) throws GenericException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException, AlreadyExistsException {
        String identifier = fileId;
        if (!PreservationMetadataType.FILE.equals(type)) {
            identifier = IdUtils.getFileId(aipId, representationId, fileDirectoryPath, fileId);
        }
        String urn = URNUtils.createRodaPreservationURN(type, identifier);
        return createPreservationMetadata(type, urn, aipId, representationId, fileDirectoryPath, fileId, payload,
                notify);
    }

    public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String aipId,
            List<String> fileDirectoryPath, String fileId, ContentPayload payload, boolean notify)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException,
            AlreadyExistsException {
        String id = IdUtils.getPreservationId(type, aipId, null, fileDirectoryPath, fileId);
        return createPreservationMetadata(type, id, aipId, null, fileDirectoryPath, fileId, payload, notify);
    }

    public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String aipId,
            String representationId, ContentPayload payload, boolean notify) throws GenericException,
            NotFoundException, RequestNotValidException, AuthorizationDeniedException, AlreadyExistsException {
        String id = IdUtils.getPreservationId(type, aipId, representationId, null, null);
        return createPreservationMetadata(type, id, aipId, representationId, null, null, payload, notify);
    }

    public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String id,
            ContentPayload payload, boolean notify) throws GenericException, NotFoundException,
            RequestNotValidException, AuthorizationDeniedException, AlreadyExistsException {
        return createPreservationMetadata(type, id, null, null, null, null, payload, notify);
    }

    public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String id, String aipId,
            String representationId, List<String> fileDirectoryPath, String fileId, ContentPayload payload,
            boolean notify) throws GenericException, NotFoundException, RequestNotValidException,
            AuthorizationDeniedException, AlreadyExistsException {
        PreservationMetadata pm = new PreservationMetadata();
        pm.setId(id);
        pm.setAipId(aipId);
        pm.setRepresentationId(representationId);
        pm.setFileDirectoryPath(fileDirectoryPath);
        pm.setFileId(fileId);
        pm.setType(type);
        StoragePath binaryPath = ModelUtils.getPreservationMetadataStoragePath(pm);
        boolean asReference = false;
        storage.createBinary(binaryPath, payload, asReference);

        if (notify) {
            notifyPreservationMetadataCreated(pm);
        }
        return pm;
    }

    public PreservationMetadata updatePreservationMetadata(String id, PreservationMetadataType type, String aipId,
            String representationId, List<String> fileDirectoryPath, String fileId, ContentPayload payload,
            boolean notify)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        PreservationMetadata pm = new PreservationMetadata();
        pm.setId(id);
        pm.setType(type);
        pm.setAipId(aipId);
        pm.setRepresentationId(representationId);
        pm.setFileDirectoryPath(fileDirectoryPath);
        pm.setFileId(fileId);

        StoragePath binaryPath = ModelUtils.getPreservationMetadataStoragePath(pm);
        storage.updateBinaryContent(binaryPath, payload, false, true);

        if (notify) {
            notifyPreservationMetadataUpdated(pm);
        }
        return pm;
    }

    public void deletePreservationMetadata(PreservationMetadataType type, String aipId, String representationId,
            String id, boolean notify)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        PreservationMetadata pm = new PreservationMetadata();
        pm.setAipId(aipId);
        pm.setId(id);
        pm.setRepresentationId(representationId);
        pm.setType(type);

        StoragePath binaryPath = ModelUtils.getPreservationMetadataStoragePath(pm);
        storage.deleteResource(binaryPath);

        if (notify) {
            notifyPreservationMetadataDeleted(pm);
        }
    }

    public CloseableIterable<OptionalWithCause<PreservationMetadata>> listPreservationMetadata()
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        List<CloseableIterable<OptionalWithCause<PreservationMetadata>>> list = new ArrayList<>();

        try {
            StoragePath storagePath = ModelUtils.getPreservationRepositoryEventStoragePath();
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, true);
            CloseableIterable<OptionalWithCause<PreservationMetadata>> pms = ResourceParseUtils
                    .convert(getStorage(), resources, PreservationMetadata.class);
            list.add(pms);
        } catch (NotFoundException e) {
            // do nothing
        }

        try {
            StoragePath storagePath = ModelUtils.getPreservationAgentStoragePath();
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, true);
            CloseableIterable<OptionalWithCause<PreservationMetadata>> pms = ResourceParseUtils
                    .convert(getStorage(), resources, PreservationMetadata.class);
            list.add(pms);
        } catch (NotFoundException e) {
            // do nothing
        }

        CloseableIterable<OptionalWithCause<AIP>> aips = listAIPs();

        for (OptionalWithCause<AIP> oaip : aips) {
            if (oaip.isPresent()) {
                AIP aip = oaip.get();
                StoragePath storagePath = ModelUtils.getAIPPreservationMetadataStoragePath(aip.getId());

                CloseableIterable<OptionalWithCause<PreservationMetadata>> aipPreservationMetadata;
                try {
                    boolean recursive = true;
                    CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath,
                            recursive);
                    aipPreservationMetadata = ResourceParseUtils.convert(getStorage(), resources,
                            PreservationMetadata.class);
                } catch (NotFoundException e) {
                    // check if AIP exists
                    storage.getDirectory(ModelUtils.getAIPStoragePath(aip.getId()));
                    // if no exception was sent by above method, return empty list
                    aipPreservationMetadata = new EmptyClosableIterable<>();
                }

                list.add(aipPreservationMetadata);

                // list from all representations
                for (Representation representation : aip.getRepresentations()) {
                    CloseableIterable<OptionalWithCause<PreservationMetadata>> rpm = listPreservationMetadata(
                            aip.getId(), representation.getId());
                    list.add(rpm);
                }
            }
        }

        return CloseableIterables.concat(list);
    }

    public CloseableIterable<OptionalWithCause<PreservationMetadata>> listPreservationMetadata(String aipId,
            boolean includeRepresentations)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getAIPPreservationMetadataStoragePath(aipId);

        CloseableIterable<OptionalWithCause<PreservationMetadata>> aipPreservationMetadata;
        try {
            boolean recursive = true;
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            aipPreservationMetadata = ResourceParseUtils.convert(getStorage(), resources,
                    PreservationMetadata.class);
        } catch (NotFoundException e) {
            // check if AIP exists
            storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));
            // if no exception was sent by above method, return empty list
            aipPreservationMetadata = new EmptyClosableIterable<>();
        }

        if (includeRepresentations) {
            List<CloseableIterable<OptionalWithCause<PreservationMetadata>>> list = new ArrayList<>();
            list.add(aipPreservationMetadata);
            // list from all representations
            AIP aip = retrieveAIP(aipId);
            for (Representation representation : aip.getRepresentations()) {
                CloseableIterable<OptionalWithCause<PreservationMetadata>> rpm = listPreservationMetadata(aipId,
                        representation.getId());
                list.add(rpm);
            }
            return CloseableIterables.concat(list);
        } else {
            return aipPreservationMetadata;
        }
    }

    public CloseableIterable<OptionalWithCause<PreservationMetadata>> listPreservationMetadata(String aipId,
            String representationId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getRepresentationPreservationMetadataStoragePath(aipId,
                representationId);

        boolean recursive = true;
        CloseableIterable<OptionalWithCause<PreservationMetadata>> ret;
        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            ret = ResourceParseUtils.convert(getStorage(), resources, PreservationMetadata.class);
        } catch (NotFoundException e) {
            // check if Representation exists
            storage.getDirectory(ModelUtils.getRepresentationStoragePath(aipId, representationId));
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }

        return ret;
    }

    public CloseableIterable<OptionalWithCause<PreservationMetadata>> listPreservationAgents()
            throws RequestNotValidException, GenericException, AuthorizationDeniedException {
        CloseableIterable<OptionalWithCause<PreservationMetadata>> ret;
        StoragePath storagePath = ModelUtils.getPreservationAgentStoragePath();

        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, false);
            ret = ResourceParseUtils.convert(getStorage(), resources, PreservationMetadata.class);
        } catch (NotFoundException e) {
            ret = new EmptyClosableIterable<>();
        }

        return ret;
    }

    public CloseableIterable<OptionalWithCause<PreservationMetadata>> listPreservationRepositoryEvents()
            throws RequestNotValidException, GenericException, AuthorizationDeniedException {
        CloseableIterable<OptionalWithCause<PreservationMetadata>> ret;
        StoragePath storagePath = ModelUtils.getPreservationRepositoryEventStoragePath();

        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, false);
            ret = ResourceParseUtils.convert(getStorage(), resources, PreservationMetadata.class);
        } catch (NotFoundException e) {
            ret = new EmptyClosableIterable<>();
        }

        return ret;
    }

    /***************** Other metadata related *****************/
    /**********************************************************/

    public Binary retrieveOtherMetadataBinary(OtherMetadata om)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        return retrieveOtherMetadataBinary(om.getAipId(), om.getRepresentationId(), om.getFileDirectoryPath(),
                om.getFileId(), om.getFileSuffix(), om.getType());
    }

    public Binary retrieveOtherMetadataBinary(String aipId, String representationId, List<String> fileDirectoryPath,
            String fileId, String fileSuffix, String type)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        Binary binary;
        StoragePath binaryPath = ModelUtils.getOtherMetadataStoragePath(aipId, representationId, fileDirectoryPath,
                fileId, fileSuffix, type);
        binary = storage.getBinary(binaryPath);
        return binary;
    }

    public OtherMetadata retrieveOtherMetadata(String aipId, String representationId,
            List<String> fileDirectoryPath, String fileId, String fileSuffix, String type)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            retrieveOtherMetadataBinary(aipId, representationId, fileDirectoryPath, fileId, fileSuffix, type);
            String id = IdUtils.getOtherMetadataId(aipId, representationId, fileDirectoryPath, fileId);
            return new OtherMetadata(id, type, aipId, representationId, fileDirectoryPath, fileId, fileSuffix);
        } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException e) {
            throw e;
        }
    }

    public OtherMetadata createOrUpdateOtherMetadata(String aipId, String representationId,
            List<String> fileDirectoryPath, String fileId, String fileSuffix, String type, ContentPayload payload,
            boolean notify)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getOtherMetadataStoragePath(aipId, representationId, fileDirectoryPath,
                fileId, fileSuffix, type);
        boolean asReference = false;
        boolean createIfNotExists = true;

        try {
            storage.createBinary(binaryPath, payload, asReference);
        } catch (AlreadyExistsException e) {
            storage.updateBinaryContent(binaryPath, payload, asReference, createIfNotExists);
        }

        String id = IdUtils.getOtherMetadataId(aipId, representationId, fileDirectoryPath, fileId);
        OtherMetadata om = new OtherMetadata(id, type, aipId, representationId, fileDirectoryPath, fileId,
                fileSuffix);

        if (notify) {
            notifyOtherMetadataCreated(om);
        }

        return om;
    }

    public void deleteOtherMetadata(String aipId, String representationId, List<String> fileDirectoryPath,
            String fileId, String fileSuffix, String type)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getOtherMetadataStoragePath(aipId, representationId, fileDirectoryPath,
                fileId, fileSuffix, type);
        storage.deleteResource(binaryPath);
    }

    public CloseableIterable<OptionalWithCause<OtherMetadata>> listOtherMetadata(String aipId, String type,
            boolean includeRepresentations)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath storagePath = ModelUtils.getAIPOtherMetadataStoragePath(aipId, type);

        boolean recursive = true;
        CloseableIterable<OptionalWithCause<OtherMetadata>> aipOtherMetadata;
        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            aipOtherMetadata = ResourceParseUtils.convert(getStorage(), resources, OtherMetadata.class);
        } catch (NotFoundException e) {
            // check if AIP exists
            storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));
            // if no exception was sent by above method, return empty list
            aipOtherMetadata = new EmptyClosableIterable<>();
        }

        if (includeRepresentations) {
            List<CloseableIterable<OptionalWithCause<OtherMetadata>>> list = new ArrayList<>();
            list.add(aipOtherMetadata);
            // list from all representations
            AIP aip = retrieveAIP(aipId);
            for (Representation representation : aip.getRepresentations()) {
                CloseableIterable<OptionalWithCause<OtherMetadata>> representationOtherMetadata = listOtherMetadata(
                        aipId, representation.getId(), null, null, type);
                list.add(representationOtherMetadata);
            }
            return CloseableIterables.concat(list);
        } else {
            return aipOtherMetadata;
        }

    }

    public CloseableIterable<OptionalWithCause<OtherMetadata>> listOtherMetadata(String aipId,
            String representationId)
            throws NotFoundException, GenericException, AuthorizationDeniedException, RequestNotValidException {
        StoragePath storagePath = ModelUtils.getRepresentationOtherMetadataFolderStoragePath(aipId,
                representationId);

        boolean recursive = true;
        CloseableIterable<OptionalWithCause<OtherMetadata>> ret;
        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            ret = ResourceParseUtils.convert(getStorage(), resources, OtherMetadata.class);
        } catch (NotFoundException e) {
            // check if Representation exists
            storage.getDirectory(ModelUtils.getRepresentationStoragePath(aipId, representationId));
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }
        return ret;
    }

    public CloseableIterable<OptionalWithCause<OtherMetadata>> listOtherMetadata(String aipId,
            String representationId, List<String> filePath, String fileId, String type)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        List<String> metadataPath = ModelUtils.getOtherMetadataStoragePath(aipId, representationId, filePath,
                fileId, type);
        StoragePath storagePath = DefaultStoragePath.parse(metadataPath);

        boolean recursive = true;
        CloseableIterable<OptionalWithCause<OtherMetadata>> ret;
        try {
            CloseableIterable<Resource> resources = storage.listResourcesUnderDirectory(storagePath, recursive);
            ret = ResourceParseUtils.convert(getStorage(), resources, OtherMetadata.class);
        } catch (NotFoundException e) {
            // check if Representation or AIP exists
            if (representationId != null) {
                storage.getDirectory(ModelUtils.getRepresentationStoragePath(aipId, representationId));
            } else {
                storage.getDirectory(ModelUtils.getAIPStoragePath(aipId));
            }
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }

        return ret;
    }

    /***************** Log entry related *****************/
    /*****************************************************/
    public void addLogEntry(LogEntry logEntry, Path logDirectory, boolean notify)
            throws GenericException, RequestNotValidException, AuthorizationDeniedException, NotFoundException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String datePlusExtension = sdf.format(new Date()) + ".log";
        Path logFile = logDirectory.resolve(datePlusExtension);
        synchronized (logFileLock) {

            // verify if file exists and if not, if older files exist (in that case,
            // move them to storage)
            if (!FSUtils.exists(logFile)) {
                findOldLogsAndMoveThemToStorage(logDirectory, logFile);
                try {
                    Files.createFile(logFile);
                } catch (FileAlreadyExistsException e) {
                    // do nothing (just caused due to concurrency)
                } catch (IOException e) {
                    throw new GenericException("Error creating file to write log into", e);
                }
            }

            // write to log file
            JsonUtils.appendObjectToFile(logEntry, logFile);

            // emit event
            if (notify) {
                notifyLogEntryCreated(logEntry);
            }
        }
    }

    public void addLogEntry(LogEntry logEntry, Path logDirectory)
            throws GenericException, RequestNotValidException, AuthorizationDeniedException, NotFoundException {
        addLogEntry(logEntry, logDirectory, true);
    }

    public synchronized void findOldLogsAndMoveThemToStorage(Path logDirectory, Path currentLogFile)
            throws RequestNotValidException, AuthorizationDeniedException, NotFoundException {
        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(logDirectory)) {

            for (Path path : directoryStream) {
                if (!path.equals(currentLogFile)) {
                    try {
                        StoragePath logPath = ModelUtils.getLogStoragePath(path.getFileName().toString());
                        storage.createBinary(logPath, new FSPathContentPayload(path), false);
                        Files.delete(path);
                    } catch (IOException | GenericException | AlreadyExistsException e) {
                        LOGGER.error("Error archiving log file", e);
                    }
                }
            }
        } catch (IOException e) {
            LOGGER.error("Error listing directory for log files", e);
        }
    }

    /***************** Users/Groups related *****************/
    /********************************************************/

    public User retrieveAuthenticatedUser(String name, String password)
            throws GenericException, AuthenticationDeniedException {
        return UserUtility.getLdapUtility().getAuthenticatedUser(name, password);
    }

    public User retrieveUserByName(String name) throws GenericException {
        return UserUtility.getLdapUtility().getUser(name);
    }

    public User retrieveUserByEmail(String email) throws GenericException {
        return UserUtility.getLdapUtility().getUserWithEmail(email);
    }

    public User registerUser(User user, String password, boolean notify)
            throws GenericException, UserAlreadyExistsException, EmailAlreadyExistsException {
        User registeredUser = UserUtility.getLdapUtility().registerUser(user, password);
        if (notify) {
            notifyUserCreated(registeredUser);
        }
        return registeredUser;
    }

    public User createUser(User user, boolean notify) throws GenericException, EmailAlreadyExistsException,
            UserAlreadyExistsException, IllegalOperationException, NotFoundException {
        return createUser(user, null, notify);
    }

    public User createUser(User user, String password, boolean notify) throws GenericException,
            EmailAlreadyExistsException, UserAlreadyExistsException, IllegalOperationException, NotFoundException {
        User createdUser = UserUtility.getLdapUtility().addUser(user);
        if (password != null) {
            UserUtility.getLdapUtility().setUserPassword(createdUser.getId(), password);
        }
        if (notify) {
            notifyUserCreated(createdUser);
        }
        return createdUser;
    }

    public User updateUser(User user, String password, boolean notify)
            throws GenericException, AlreadyExistsException, NotFoundException, AuthorizationDeniedException {
        try {
            if (password != null) {
                UserUtility.getLdapUtility().setUserPassword(user.getId(), password);
            }

            User updatedUser = UserUtility.getLdapUtility().modifyUser(user);
            if (notify) {
                notifyUserUpdated(updatedUser);
            }
            return updatedUser;
        } catch (IllegalOperationException e) {
            throw new AuthorizationDeniedException("Illegal operation", e);
        }
    }

    public User updateMyUser(User user, String password, boolean notify)
            throws GenericException, AlreadyExistsException, NotFoundException, AuthorizationDeniedException {
        try {
            User updatedUser = UserUtility.getLdapUtility().modifySelfUser(user, password);

            if (notify) {
                notifyUserUpdated(updatedUser);
            }
            return updatedUser;
        } catch (IllegalOperationException e) {
            throw new AuthorizationDeniedException("Illegal operation", e);
        }

    }

    public void deleteUser(String id, boolean notify) throws GenericException, AuthorizationDeniedException {
        try {
            UserUtility.getLdapUtility().removeUser(id);
            if (notify) {
                notifyUserDeleted(id);
            }
        } catch (IllegalOperationException e) {
            throw new AuthorizationDeniedException("Illegal operation", e);
        }
    }

    public List<User> listUsers() throws GenericException {
        return UserUtility.getLdapUtility().getUsers();
    }

    public RODAMember retrieveRODAMember(String name) throws GenericException {
        RODAMember member = null;
        try {
            member = UserUtility.getLdapUtility().getUser(name);
        } catch (GenericException e) {
            try {
                member = UserUtility.getLdapUtility().getGroup(name);
            } catch (GenericException | NotFoundException e1) {
                LOGGER.error("Could not retrieve any user or group with name: {}", name);
                throw e;
            }
        }
        return member;
    }

    public User retrieveUser(String name) throws GenericException {
        return UserUtility.getLdapUtility().getUser(name);
    }

    public Group retrieveGroup(String name) throws GenericException, NotFoundException {
        return UserUtility.getLdapUtility().getGroup(name);
    }

    public Group createGroup(Group group, boolean notify) throws GenericException, AlreadyExistsException {
        Group createdGroup = UserUtility.getLdapUtility().addGroup(group);
        if (notify) {
            notifyGroupCreated(createdGroup);
        }
        return createdGroup;
    }

    public Group updateGroup(final Group group, boolean notify)
            throws GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            Group updatedGroup = UserUtility.getLdapUtility().modifyGroup(group);
            if (notify) {
                notifyGroupUpdated(updatedGroup);
            }
            return updatedGroup;
        } catch (IllegalOperationException e) {
            throw new AuthorizationDeniedException("Illegal operation", e);
        }

    }

    public void deleteGroup(String id, boolean notify) throws GenericException, AuthorizationDeniedException {
        try {
            UserUtility.getLdapUtility().removeGroup(id);
            if (notify) {
                notifyGroupDeleted(id);
            }
        } catch (IllegalOperationException e) {
            throw new AuthorizationDeniedException("Illegal operation", e);
        }
    }

    public List<Group> listGroups() throws GenericException {
        return UserUtility.getLdapUtility().getGroups();
    }

    public User confirmUserEmail(String username, String email, String emailConfirmationToken, boolean useModel,
            boolean notify) throws NotFoundException, InvalidTokenException, GenericException {
        User user = null;
        if (useModel) {
            user = UserUtility.getLdapUtility().confirmUserEmail(username, email, emailConfirmationToken);
        }
        if (user != null && notify) {
            notifyUserUpdated(user);
        }
        return user;
    }

    public User requestPasswordReset(String username, String email, boolean useModel, boolean notify)
            throws IllegalOperationException, NotFoundException, GenericException {
        User user = null;
        if (useModel) {
            user = UserUtility.getLdapUtility().requestPasswordReset(username, email);
        }
        if (user != null && notify) {
            notifyUserUpdated(user);
        }
        return user;
    }

    public User resetUserPassword(String username, String password, String resetPasswordToken, boolean useModel,
            boolean notify)
            throws NotFoundException, InvalidTokenException, IllegalOperationException, GenericException {
        User user = null;
        if (useModel) {
            user = UserUtility.getLdapUtility().resetUserPassword(username, password, resetPasswordToken);
        }
        if (user != null && notify) {
            notifyUserUpdated(user);
        }
        return user;
    }

    /***************** Jobs related *****************/
    /************************************************/
    public void createJob(Job job)
            throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        createOrUpdateJob(job);

        // try to create directory for this job in job report container
        try {
            StoragePath jobReportsPath = ModelUtils.getJobReportsStoragePath(job.getId());
            storage.createDirectory(jobReportsPath);
        } catch (AlreadyExistsException e) {
            // do nothing & carry on
        } catch (RequestNotValidException | AuthorizationDeniedException e) {
            throw new GenericException("Error creating/updating job report", e);
        }
    }

    public void createOrUpdateJob(Job job)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        // create or update job in storage
        String jobAsJson = JsonUtils.getJsonFromObject(job);
        StoragePath jobPath = ModelUtils.getJobStoragePath(job.getId());
        storage.updateBinaryContent(jobPath, new StringContentPayload(jobAsJson), false, true);

        // index it
        notifyJobCreatedOrUpdated(job, false);
    }

    public Job retrieveJob(String jobId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        StoragePath jobPath = ModelUtils.getJobStoragePath(jobId);
        Binary binary = storage.getBinary(jobPath);
        Job ret;
        InputStream inputStream = null;
        try {
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, Job.class);
        } catch (IOException | GenericException e) {
            throw new GenericException("Error reading job: " + jobId, e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    public void deleteJob(String jobId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath jobPath = ModelUtils.getJobStoragePath(jobId);

        // remove it from storage
        storage.deleteResource(jobPath);

        // remove it from index
        notifyJobDeleted(jobId);
    }

    public Report retrieveJobReport(String jobId, String givenId, boolean generateId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        String id = givenId;
        if (generateId) {
            id = IdUtils.getJobReportId(jobId, givenId);
        }
        StoragePath jobReportPath = ModelUtils.getJobReportStoragePath(jobId, id);
        Binary binary = storage.getBinary(jobReportPath);
        Report ret;
        InputStream inputStream = null;
        try {
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, Report.class);
        } catch (IOException e) {
            throw new GenericException("Error reading job report", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    public void createOrUpdateJobReport(Report jobReport, Job job) throws GenericException {
        // create job report in storage
        try {
            String jobReportAsJson = JsonUtils.getJsonFromObject(jobReport);
            StoragePath jobReportPath = ModelUtils.getJobReportStoragePath(jobReport.getJobId(), jobReport.getId());
            storage.updateBinaryContent(jobReportPath, new StringContentPayload(jobReportAsJson), false, true);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) {
            LOGGER.error("Error creating/updating job report in storage", e);
        }

        // index it
        notifyJobReportCreatedOrUpdated(jobReport, job);
    }

    public void deleteJobReport(String jobId, String jobReportId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        StoragePath jobReportPath = ModelUtils.getJobReportStoragePath(jobId, jobReportId);

        // remove it from storage
        storage.deleteResource(jobReportPath);

        // remove it from index
        notifyJobReportDeleted(jobReportId);
    }

    public void updateAIPPermissions(AIP aip, String updatedBy)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        aip.setUpdatedBy(updatedBy);
        aip.setUpdatedOn(new Date());
        updateAIPMetadata(aip);
        notifyAipPermissionsUpdated(aip);
    }

    public void updateDIPPermissions(DIP dip)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        dip.setLastModified(new Date());
        updateDIPMetadata(dip);
        notifyDipPermissionsUpdated(dip);
    }

    public void deleteTransferredResource(TransferredResource transferredResource) {
        FSUtils.deletePathQuietly(Paths.get(transferredResource.getFullPath()));
        notifyTransferredResourceDeleted(transferredResource.getUUID());
    }

    /***************** Risk related *****************/
    /************************************************/

    public Risk createRisk(Risk risk, boolean commit) throws GenericException {
        try {
            if (risk.getId() == null) {
                risk.setId(IdUtils.createUUID());
            }

            risk.setCreatedOn(new Date());
            risk.setUpdatedOn(new Date());

            String riskAsJson = JsonUtils.getJsonFromObject(risk);
            StoragePath riskPath = ModelUtils.getRiskStoragePath(risk.getId());
            storage.createBinary(riskPath, new StringContentPayload(riskAsJson), false);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException
                | AlreadyExistsException e) {
            LOGGER.error("Error creating risk in storage", e);
        }

        notifyRiskCreatedOrUpdated(risk, 0, commit);
        return risk;
    }

    public Risk updateRisk(Risk risk, Map<String, String> properties, boolean commit, int incidences)
            throws GenericException {
        try {
            risk.setUpdatedOn(new Date());
            String riskAsJson = JsonUtils.getJsonFromObject(risk);
            StoragePath riskPath = ModelUtils.getRiskStoragePath(risk.getId());

            // Create version snapshot
            if (properties != null && !properties.isEmpty()) {
                storage.createBinaryVersion(riskPath, properties);
            }

            storage.updateBinaryContent(riskPath, new StringContentPayload(riskAsJson), false, true);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) {
            LOGGER.error("Error updating risk in storage", e);
        }

        notifyRiskCreatedOrUpdated(risk, incidences, commit);
        return risk;
    }

    public void deleteRisk(String riskId, boolean commit)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {
        StoragePath riskPath = ModelUtils.getRiskStoragePath(riskId);
        storage.deleteResource(riskPath);
        notifyRiskDeleted(riskId, commit);
    }

    public Risk retrieveRisk(String riskId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        StoragePath riskPath = ModelUtils.getRiskStoragePath(riskId);
        Binary binary = storage.getBinary(riskPath);
        Risk ret;
        InputStream inputStream = null;
        try {
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, Risk.class);
        } catch (IOException e) {
            throw new GenericException("Error reading risk", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    public CloseableIterable<OptionalWithCause<Risk>> listRisks()
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        final boolean recursive = false;

        final CloseableIterable<Resource> resourcesIterable = storage
                .listResourcesUnderContainer(ModelUtils.getRiskContainerPath(), recursive);

        return ResourceParseUtils.convert(getStorage(), resourcesIterable, Risk.class);
    }

    public BinaryVersion retrieveVersion(String id, String versionId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getRiskStoragePath(id);
        return storage.getBinaryVersion(binaryPath, versionId);
    }

    public BinaryVersion revertRiskVersion(String riskId, String versionId, Map<String, String> properties,
            boolean commit, int incidences)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        StoragePath binaryPath = ModelUtils.getRiskStoragePath(riskId);

        BinaryVersion currentVersion = storage.createBinaryVersion(binaryPath, properties);
        storage.revertBinaryVersion(binaryPath, versionId);

        notifyRiskCreatedOrUpdated(retrieveRisk(riskId), incidences, commit);
        return currentVersion;
    }

    public RiskIncidence createRiskIncidence(RiskIncidence riskIncidence, boolean commit) throws GenericException {
        try {
            riskIncidence.setId(IdUtils.createUUID());
            riskIncidence.setDetectedOn(new Date());

            String riskIncidenceAsJson = JsonUtils.getJsonFromObject(riskIncidence);
            StoragePath riskIncidencePath = ModelUtils.getRiskIncidenceStoragePath(riskIncidence.getId());
            storage.createBinary(riskIncidencePath, new StringContentPayload(riskIncidenceAsJson), false);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException
                | AlreadyExistsException e) {
            LOGGER.error("Error creating risk incidence in storage", e);
        }

        notifyRiskIncidenceCreatedOrUpdated(riskIncidence, commit);
        return riskIncidence;
    }

    public RiskIncidence updateRiskIncidence(RiskIncidence riskIncidence, boolean commit) throws GenericException {
        try {
            riskIncidence.setRiskId(riskIncidence.getRiskId());
            String riskIncidenceAsJson = JsonUtils.getJsonFromObject(riskIncidence);
            StoragePath riskIncidencePath = ModelUtils.getRiskIncidenceStoragePath(riskIncidence.getId());
            storage.updateBinaryContent(riskIncidencePath, new StringContentPayload(riskIncidenceAsJson), false,
                    true);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) {
            LOGGER.error("Error updating risk incidence in storage", e);
        }

        notifyRiskIncidenceCreatedOrUpdated(riskIncidence, commit);
        return riskIncidence;
    }

    public void deleteRiskIncidence(String riskIncidenceId, boolean commit)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {
        StoragePath riskIncidencePath = ModelUtils.getRiskIncidenceStoragePath(riskIncidenceId);
        storage.deleteResource(riskIncidencePath);
        notifyRiskIncidenceDeleted(riskIncidenceId, commit);
    }

    public RiskIncidence retrieveRiskIncidence(String incidenceId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        StoragePath riskIncidencePath = ModelUtils.getRiskIncidenceStoragePath(incidenceId);
        Binary binary = storage.getBinary(riskIncidencePath);
        RiskIncidence ret;
        InputStream inputStream = null;
        try {
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, RiskIncidence.class);
        } catch (IOException e) {
            throw new GenericException("Error reading risk incidence", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    /***************** Format related *****************/
    /************************************************/

    public Format createFormat(Format format, boolean commit) throws GenericException {
        try {
            format.setId(IdUtils.createUUID());
            String formatAsJson = JsonUtils.getJsonFromObject(format);
            StoragePath formatPath = ModelUtils.getFormatStoragePath(format.getId());
            storage.createBinary(formatPath, new StringContentPayload(formatAsJson), false);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException
                | AlreadyExistsException e) {
            LOGGER.error("Error creating format in storage", e);
        }

        notifyFormatCreatedOrUpdated(format, commit);
        return format;
    }

    public Format updateFormat(Format format, boolean commit) throws GenericException {
        try {
            String formatAsJson = JsonUtils.getJsonFromObject(format);
            StoragePath formatPath = ModelUtils.getFormatStoragePath(format.getId());
            storage.updateBinaryContent(formatPath, new StringContentPayload(formatAsJson), false, true);
        } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) {
            LOGGER.error("Error updating format in storage", e);
        }

        notifyFormatCreatedOrUpdated(format, commit);
        return format;
    }

    public void deleteFormat(String formatId, boolean commit)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {

        StoragePath formatPath = ModelUtils.getFormatStoragePath(formatId);
        storage.deleteResource(formatPath);
        notifyFormatDeleted(formatId, commit);
    }

    public Format retrieveFormat(String formatId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {

        StoragePath formatPath = ModelUtils.getFormatStoragePath(formatId);
        Binary binary = storage.getBinary(formatPath);
        Format ret;
        InputStream inputStream = null;
        try {
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, Format.class);
        } catch (IOException e) {
            throw new GenericException("Error reading format", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    /***************** Notification related *****************/
    /**********************************************************/

    public Notification createNotification(final Notification notification, final NotificationProcessor processor)
            throws GenericException, AuthorizationDeniedException {

        notification.setId(IdUtils.createUUID());
        notification.setAcknowledgeToken(IdUtils.createUUID());
        Notification processedNotification = processor.processNotification(this, notification);

        try {
            String notificationAsJson = JsonUtils.getJsonFromObject(processedNotification);
            StoragePath notificationPath = ModelUtils.getNotificationStoragePath(processedNotification.getId());
            storage.createBinary(notificationPath, new StringContentPayload(notificationAsJson), false);
            notifyNotificationCreatedOrUpdated(processedNotification);
        } catch (NotFoundException | RequestNotValidException | AlreadyExistsException e) {
            LOGGER.error("Error creating notification in storage", e);
            throw new GenericException(e);
        }
        return processedNotification;
    }

    public Notification updateNotification(Notification notification)
            throws GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            String notificationAsJson = JsonUtils.getJsonFromObject(notification);
            StoragePath notificationPath = ModelUtils.getNotificationStoragePath(notification.getId());
            storage.updateBinaryContent(notificationPath, new StringContentPayload(notificationAsJson), false,
                    true);
        } catch (GenericException | RequestNotValidException e) {
            LOGGER.error("Error updating notification in storage", e);
            throw new GenericException(e);
        }

        notifyNotificationCreatedOrUpdated(notification);
        return notification;
    }

    public void deleteNotification(String notificationId)
            throws GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            StoragePath notificationPath = ModelUtils.getNotificationStoragePath(notificationId);
            storage.deleteResource(notificationPath);
            notifyNotificationDeleted(notificationId);
        } catch (RequestNotValidException e) {
            LOGGER.error("Error deleting notification", e);
            throw new GenericException(e);
        }
    }

    public Notification retrieveNotification(String notificationId)
            throws GenericException, NotFoundException, AuthorizationDeniedException {
        InputStream inputStream = null;
        Notification ret;
        try {
            StoragePath notificationPath = ModelUtils.getNotificationStoragePath(notificationId);
            Binary binary = storage.getBinary(notificationPath);
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, Notification.class);
        } catch (IOException | RequestNotValidException e) {
            throw new GenericException("Error reading notification", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    public void acknowledgeNotification(String notificationId, String token)
            throws GenericException, NotFoundException, AuthorizationDeniedException {

        Notification notification = this.retrieveNotification(notificationId);
        String ackToken = token.substring(0, 36);
        String emailToken = token.substring(36);

        if (notification.getAcknowledgeToken().equals(ackToken)) {
            for (String recipient : notification.getRecipientUsers()) {
                String recipientUUID = IdUtils.createUUID(recipient);
                if (recipientUUID.equals(emailToken)) {
                    DateFormat df = DateFormat.getDateTimeInstance();
                    String ackDate = df.format(new Date());
                    notification.addAcknowledgedUser(recipient, ackDate);
                    notification.setAcknowledged(true);
                    this.updateNotification(notification);
                }
            }
        }
    }

    /***************** DIP related *****************/
    /**********************************************************/

    public CloseableIterable<OptionalWithCause<DIPFile>> listDIPFilesUnder(DIPFile f, boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        return listDIPFilesUnder(f.getDipId(), f.getPath(), f.getId(), recursive);
    }

    public CloseableIterable<OptionalWithCause<DIPFile>> listDIPFilesUnder(String dipId, List<String> directoryPath,
            String fileId, boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {
        final StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        final CloseableIterable<Resource> iterable = storage.listResourcesUnderDirectory(filePath, recursive);
        return ResourceParseUtils.convert(getStorage(), iterable, DIPFile.class);
    }

    public CloseableIterable<OptionalWithCause<DIPFile>> listDIPFilesUnder(String dipId, boolean recursive)
            throws NotFoundException, GenericException, RequestNotValidException, AuthorizationDeniedException {

        final StoragePath storagePath = ModelUtils.getDIPDataStoragePath(dipId);
        CloseableIterable<OptionalWithCause<DIPFile>> ret;
        try {
            final CloseableIterable<Resource> iterable = storage.listResourcesUnderDirectory(storagePath,
                    recursive);
            ret = ResourceParseUtils.convert(getStorage(), iterable, DIPFile.class);
        } catch (NotFoundException e) {
            // check if AIP exists
            storage.getDirectory(ModelUtils.getDIPStoragePath(dipId));
            // if no exception was sent by above method, return empty list
            ret = new EmptyClosableIterable<>();
        }

        return ret;

    }

    private void createDIPMetadata(DIP dip, StoragePath storagePath) throws RequestNotValidException,
            GenericException, AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        String json = JsonUtils.getJsonFromObject(dip);
        DefaultStoragePath metadataStoragePath = DefaultStoragePath.parse(storagePath,
                RodaConstants.STORAGE_DIP_METADATA_FILENAME);
        boolean asReference = false;
        storage.createBinary(metadataStoragePath, new StringContentPayload(json), asReference);
    }

    private void updateDIPMetadata(DIP dip, StoragePath storagePath)
            throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
        String json = JsonUtils.getJsonFromObject(dip);
        DefaultStoragePath metadataStoragePath = DefaultStoragePath.parse(storagePath,
                RodaConstants.STORAGE_DIP_METADATA_FILENAME);
        boolean asReference = false;
        boolean createIfNotExists = true;
        storage.updateBinaryContent(metadataStoragePath, new StringContentPayload(json), asReference,
                createIfNotExists);
    }

    public DIP createDIP(DIP dip, boolean notify) throws GenericException, AuthorizationDeniedException {
        try {
            Directory directory;

            if (StringUtils.isNotBlank(dip.getId())) {
                try {
                    directory = storage.createDirectory(
                            DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_DIP, dip.getId()));
                } catch (AlreadyExistsException | GenericException | AuthorizationDeniedException e) {
                    directory = storage
                            .createRandomDirectory(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_DIP));
                    dip.setId(directory.getStoragePath().getName());
                }
            } else {
                directory = storage
                        .createRandomDirectory(DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_DIP));
                dip.setId(directory.getStoragePath().getName());
            }

            dip.setDateCreated(new Date());
            dip.setLastModified(new Date());
            createDIPMetadata(dip, directory.getStoragePath());

            if (notify) {
                notifyDIPCreated(dip, false);
            }

            return dip;
        } catch (NotFoundException | RequestNotValidException | AlreadyExistsException e) {
            LOGGER.error("Error creating DIP in storage", e);
            throw new GenericException(e);
        }
    }

    public DIP updateDIP(DIP dip) throws GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            dip.setLastModified(new Date());
            updateDIPMetadata(dip, DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_DIP, dip.getId()));
        } catch (GenericException | RequestNotValidException e) {
            LOGGER.error("Error updating DIP in storage", e);
            throw new GenericException(e);
        }

        notifyDIPUpdated(dip, false);
        return dip;
    }

    public void deleteDIP(String dipId) throws GenericException, NotFoundException, AuthorizationDeniedException {
        try {
            // deleting external service if existing
            DIP dip = retrieveDIP(dipId);
            OptionalWithCause<String> deleteURL = DIPUtils.getCompleteDeleteExternalURL(dip);
            Optional<String> httpMethod = DIPUtils.getDeleteMethod(dip);
            if (deleteURL.isPresent() && httpMethod.isPresent()) {
                String url = deleteURL.get();
                Optional<Pair<String, String>> credentials = DIPUtils.getDeleteCredentials(dip);
                String method = httpMethod.get();
                HTTPUtility.doMethod(url, method, credentials);
            }

            StoragePath dipPath = ModelUtils.getDIPStoragePath(dipId);
            storage.deleteResource(dipPath);
            notifyDIPDeleted(dipId, false);
        } catch (RequestNotValidException e) {
            LOGGER.error("Error deleting DIP", e);
            throw new GenericException(e);
        }
    }

    public DIP retrieveDIP(String dipId) throws GenericException, NotFoundException, AuthorizationDeniedException {
        InputStream inputStream = null;
        DIP ret;
        try {
            StoragePath dipPath = ModelUtils.getDIPMetadataStoragePath(dipId);
            Binary binary = storage.getBinary(dipPath);
            inputStream = binary.getContent().createInputStream();
            ret = JsonUtils.getObjectFromJson(inputStream, DIP.class);
        } catch (IOException | RequestNotValidException e) {
            throw new GenericException("Error reading DIP", e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return ret;
    }

    public DIPFile createDIPFile(String dipId, List<String> directoryPath, String fileId, long size,
            ContentPayload contentPayload, boolean notify) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        boolean asReference = false;

        StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        final Binary createdBinary = storage.createBinary(filePath, contentPayload, asReference);
        DIPFile file = ResourceParseUtils.convertResourceToDIPFile(createdBinary);
        file.setUUID(IdUtils.getDIPFileId(file));
        file.setSize(size);

        if (notify) {
            notifyDIPFileCreated(file);
        }

        return file;
    }

    public DIPFile createDIPFile(String dipId, List<String> directoryPath, String fileId, String dirName,
            boolean notify) throws RequestNotValidException, GenericException, AlreadyExistsException,
            AuthorizationDeniedException, NotFoundException {

        StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        final Directory createdDirectory = storage.createDirectory(DefaultStoragePath.parse(filePath, dirName));
        DIPFile file = ResourceParseUtils.convertResourceToDIPFile(createdDirectory);

        if (notify) {
            notifyDIPFileCreated(file);
        }

        return file;
    }

    public DIPFile updateDIPFile(String dipId, List<String> directoryPath, String oldFileId, String fileId,
            long size, ContentPayload contentPayload, boolean createIfNotExists, boolean notify)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
            AlreadyExistsException {
        DIPFile file;
        boolean asReference = false;

        StoragePath oldFilePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, oldFileId);
        storage.deleteResource(oldFilePath);

        StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        final Binary binary = storage.createBinary(filePath, contentPayload, asReference);
        file = ResourceParseUtils.convertResourceToDIPFile(binary);

        if (notify) {
            notifyDIPFileDeleted(dipId, directoryPath, oldFileId);
            notifyDIPFileCreated(file);
        }

        return file;
    }

    public void deleteDIPFile(String dipId, List<String> directoryPath, String fileId, boolean notify)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {

        StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        storage.deleteResource(filePath);
        if (notify) {
            notifyDIPFileDeleted(dipId, directoryPath, fileId);
        }
    }

    public DIPFile retrieveDIPFile(String dipId, List<String> directoryPath, String fileId)
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        DIPFile file;
        StoragePath filePath = ModelUtils.getDIPFileStoragePath(dipId, directoryPath, fileId);
        Class<? extends Entity> entity = storage.getEntity(filePath);

        if (entity.equals(Binary.class) || entity.equals(DefaultBinary.class)) {
            Binary binary = storage.getBinary(filePath);
            file = ResourceParseUtils.convertResourceToDIPFile(binary);
        } else {
            Directory directory = storage.getDirectory(filePath);
            file = ResourceParseUtils.convertResourceToDIPFile(directory);
        }

        return file;
    }

    /****************************************************************
     * 
     * OTHER DIRECTORIES (submission, documentation, schemas)
     * 
     *********************************************************/

    /**
     * 
     */

    public Directory getSubmissionDirectory(String aipId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return storage.getDirectory(ModelUtils.getSubmissionStoragePath(aipId));
    }

    public void createSubmission(StorageService submissionStorage, StoragePath submissionStoragePath, String aipId)
            throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException,
            AuthorizationDeniedException {
        storage.copy(submissionStorage, submissionStoragePath, ModelUtils.getSubmissionStoragePath(aipId));
    }

    public void createSubmission(Path submissionPath, String aipId) throws AlreadyExistsException, GenericException,
            RequestNotValidException, NotFoundException, AuthorizationDeniedException {
        StoragePath submissionStoragePath = DefaultStoragePath.parse(ModelUtils.getSubmissionStoragePath(aipId),
                submissionPath.getFileName().toString());
        storage.createBinary(submissionStoragePath, new FSPathContentPayload(submissionPath), false);
    }

    public Directory getDocumentationDirectory(String aipId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return storage.getDirectory(ModelUtils.getDocumentationStoragePath(aipId));
    }

    public Directory getDocumentationDirectory(String aipId, String representationId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return storage.getDirectory(ModelUtils.getDocumentationStoragePath(aipId, representationId));
    }

    public File createDocumentation(String aipId, String representationId, List<String> directoryPath,
            String fileId, ContentPayload contentPayload) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        boolean asReference = false;
        StoragePath filePath = ModelUtils.getDocumentationStoragePath(aipId, representationId, directoryPath,
                fileId);
        final Binary createdBinary = storage.createBinary(filePath, contentPayload, asReference);
        return ResourceParseUtils.convertResourceToFile(createdBinary);
    }

    public Directory getSchemasDirectory(String aipId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return storage.getDirectory(ModelUtils.getSchemasStoragePath(aipId));
    }

    public Directory getSchemasDirectory(String aipId, String representationId)
            throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
        return storage.getDirectory(ModelUtils.getSchemasStoragePath(aipId, representationId));
    }

    public File createSchema(String aipId, String representationId, List<String> directoryPath, String fileId,
            ContentPayload contentPayload) throws RequestNotValidException, GenericException,
            AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
        boolean asReference = false;
        StoragePath filePath = ModelUtils.getSchemaStoragePath(aipId, representationId, directoryPath, fileId);
        final Binary createdBinary = storage.createBinary(filePath, contentPayload, asReference);
        return ResourceParseUtils.convertResourceToFile(createdBinary);
    }

    private CloseableIterable<OptionalWithCause<Representation>> listRepresentations()
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        CloseableIterable<OptionalWithCause<AIP>> aips = listAIPs();

        return CloseableIterables.concat(aips, aip -> {
            if (aip.isPresent()) {
                List<Representation> representations = aip.get().getRepresentations();
                return CloseableIterables.fromList(representations.stream().map(rep -> OptionalWithCause.of(rep))
                        .collect(Collectors.toList()));
            } else {
                return CloseableIterables.empty();
            }
        });
    }

    private CloseableIterable<OptionalWithCause<File>> listFiles()
            throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
        CloseableIterable<OptionalWithCause<Representation>> representations = listRepresentations();

        return CloseableIterables.concat(representations, rep -> {
            if (rep.isPresent()) {
                Representation representation = rep.get();
                try {
                    return listFilesUnder(representation.getAipId(), representation.getId(), true);
                } catch (RODAException e) {
                    LOGGER.error("Error listing files under representation: {}", representation.getId(), e);
                    return CloseableIterables.empty();
                }
            } else {
                return CloseableIterables.empty();
            }
        });

    }

    private CloseableIterable<OptionalWithCause<DIPFile>> listDIPFiles() throws RODAException {
        CloseableIterable<OptionalWithCause<DIP>> dips = list(DIP.class);

        return CloseableIterables.concat(dips, odip -> {
            CloseableIterable<?> dipFiles = CloseableIterables.empty();

            if (odip.isPresent()) {
                DIP dip = odip.get();
                try {
                    dipFiles = listDIPFilesUnder(dip.getId(), true);
                } catch (RODAException e) {
                    LOGGER.error("Error getting DIP files under a DIP " + dip.getId());
                }
            }

            return (CloseableIterable<OptionalWithCause<DIPFile>>) dipFiles;
        });

    }

    public <T extends IsRODAObject> Optional<LiteRODAObject> retrieveLiteFromObject(T object) {
        return LiteRODAObjectFactory.get(object);
    }

    public <T extends IsModelObject> OptionalWithCause<T> retrieveObjectFromLite(LiteRODAObject liteRODAObject) {
        return LiteRODAObjectFactory.get(this, liteRODAObject);
    }

    public TransferredResource retrieveTransferredResource(String fullPath) {
        TransferredResourcesScanner transferredResourcesScanner = RodaCoreFactory.getTransferredResourcesScanner();
        return transferredResourcesScanner.instantiateTransferredResource(Paths.get(fullPath),
                transferredResourcesScanner.getBasePath());
    }

    @SuppressWarnings("unchecked")
    public <T extends IsRODAObject> CloseableIterable<OptionalWithCause<T>> list(Class<T> objectClass)
            throws RODAException {
        CloseableIterable<? extends OptionalWithCause<?>> ret;

        if (Representation.class.equals(objectClass)) {
            ret = listRepresentations();
        } else if (File.class.equals(objectClass)) {
            ret = listFiles();
        } else if (TransferredResource.class.equals(objectClass)) {
            ret = LiteRODAObjectFactory.transformFromLite(this,
                    RodaCoreFactory.getTransferredResourcesScanner().listTransferredResources());
        } else if (RODAMember.class.equals(objectClass)) {
            ret = listMembers();
        } else if (LogEntry.class.equals(objectClass)) {
            ret = listLogEntries();
        } else if (DIPFile.class.equals(objectClass)) {
            ret = listDIPFiles();
        } else if (PreservationMetadata.class.equals(objectClass)) {
            ret = listPreservationMetadata();
        } else if (DescriptiveMetadata.class.equals(objectClass)) {
            ret = listDescriptiveMetadata();
        } else if (Report.class.equals(objectClass)) {
            ret = ResourceParseUtils.convert(getStorage(), listReportResources(), objectClass);
        } else {
            StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
            final CloseableIterable<Resource> resourcesIterable = storage.listResourcesUnderContainer(containerPath,
                    false);
            ret = ResourceParseUtils.convert(getStorage(), resourcesIterable, objectClass);
        }

        return (CloseableIterable<OptionalWithCause<T>>) ret;
    }

    public <T extends IsRODAObject> CloseableIterable<OptionalWithCause<LiteRODAObject>> listLite(
            Class<T> objectClass) throws RODAException {
        CloseableIterable<OptionalWithCause<LiteRODAObject>> ret;

        if (Representation.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(),
                    ResourceListUtils.listRepresentationResources(storage), objectClass);
        } else if (File.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(), ResourceListUtils.listFileResources(storage),
                    objectClass);
        } else if (TransferredResource.class.equals(objectClass)) {
            ret = RodaCoreFactory.getTransferredResourcesScanner().listTransferredResources();
        } else if (RODAMember.class.equals(objectClass)) {
            ret = LiteRODAObjectFactory.transformIntoLite(listMembers());
        } else if (LogEntry.class.equals(objectClass)) {
            ret = LiteRODAObjectFactory.transformIntoLite(listLogEntries());
        } else if (DIPFile.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(), ResourceListUtils.listDIPFileResources(storage),
                    objectClass);
        } else if (PreservationMetadata.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(),
                    ResourceListUtils.listPreservationMetadataResources(storage), objectClass);
        } else if (DescriptiveMetadata.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(),
                    ResourceListUtils.listDescriptiveMetadataResources(storage), objectClass);
        } else if (Report.class.equals(objectClass)) {
            ret = ResourceParseUtils.convertLite(getStorage(), listReportResources(), objectClass);
        } else {
            StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
            final CloseableIterable<Resource> resourcesIterable = storage.listResourcesUnderContainer(containerPath,
                    false);
            ret = ResourceParseUtils.convertLite(getStorage(), resourcesIterable, objectClass);
        }

        return ret;
    }

    private CloseableIterable<Resource> listReportResources() throws RODAException {
        CloseableIterable<Resource> resources = storage
                .listResourcesUnderContainer(ModelUtils.getContainerPath(Report.class), true);
        return CloseableIterables.filter(resources, resource -> !(resource instanceof Directory));
    }

    private CloseableIterable<OptionalWithCause<RODAMember>> listMembers() {
        List<OptionalWithCause<RODAMember>> members = new ArrayList<>();

        try {
            List<OptionalWithCause<RODAMember>> users = listUsers().stream()
                    .map(user -> OptionalWithCause.of((RODAMember) user)).collect(Collectors.toList());
            members.addAll(users);

            List<OptionalWithCause<RODAMember>> groups = listGroups().stream()
                    .map(group -> OptionalWithCause.of((RODAMember) group)).collect(Collectors.toList());
            members.addAll(groups);
        } catch (GenericException e) {
            LOGGER.error("Error getting user and/or groups list");
        }

        return CloseableIterables.fromList(members);
    }

    public CloseableIterable<OptionalWithCause<LogEntry>> listLogEntries() {
        return listLogEntries(0);
    }

    public CloseableIterable<OptionalWithCause<LogEntry>> listLogEntries(int daysToIndex) {
        boolean recursive = false;
        CloseableIterable<OptionalWithCause<LogEntry>> inStorage = null;
        CloseableIterable<OptionalWithCause<LogEntry>> notStorage = null;

        try {
            final CloseableIterable<Resource> actionLogs = getStorage().listResourcesUnderContainer(
                    DefaultStoragePath.parse(RodaConstants.STORAGE_CONTAINER_ACTIONLOG), recursive);

            if (daysToIndex > 0) {
                inStorage = new LogEntryStorageIterable(CloseableIterables.filter(actionLogs,
                        r -> isToIndex(r.getStoragePath().getName(), daysToIndex)));
            } else {
                inStorage = new LogEntryStorageIterable(actionLogs);
            }
        } catch (NotFoundException | GenericException | AuthorizationDeniedException | RequestNotValidException e) {
            LOGGER.error("Error getting action log from storage", e);
        }

        try {
            if (daysToIndex > 0) {
                notStorage = new LogEntryFileSystemIterable(RodaCoreFactory.getLogPath(),
                        p -> isToIndex(p.getFileName().toString(), daysToIndex));
            } else {
                notStorage = new LogEntryFileSystemIterable(RodaCoreFactory.getLogPath());
            }

        } catch (IOException e) {
            LOGGER.error("Error getting action log from storage", e);
        }

        return CloseableIterables.concat(inStorage, notStorage);
    }

    private boolean isToIndex(String fileName, int daysToIndex) {
        boolean isToIndex = false;
        String fileNameWithoutExtension = fileName.replaceFirst(".log$", "");

        try {
            DateTime dt = LOG_NAME_DATE_FORMAT.parseDateTime(fileNameWithoutExtension);

            if (dt.plusDays(daysToIndex + 1).isAfterNow()) {
                isToIndex = true;
            }

        } catch (IllegalArgumentException | UnsupportedOperationException e) {
            LOGGER.error("Could not parse log file name", e);
        }
        return isToIndex;
    }

    public boolean hasObjects(Class<? extends IsRODAObject> objectClass) {
        try {
            if (LogEntry.class.equals(objectClass) || RODAMember.class.equals(objectClass)
                    || TransferredResource.class.equals(objectClass)
                    || IndexedPreservationAgent.class.equals(objectClass)) {
                return true;
            } else {
                StoragePath storagePath = ModelUtils.getContainerPath(objectClass);
                try {
                    return RodaCoreFactory.getStorageService().countResourcesUnderContainer(storagePath, false)
                            .intValue() > 0;
                } catch (NotFoundException e) {
                    // TODO 20160913 hsilva: we want to handle the non-existence of a
                    // container
                }
            }

            return false;
        } catch (RODAException e) {
            return false;
        }
    }

    public boolean checkObjectPermission(String username, String permissionType, String objectClass, String id)
            throws GenericException, NotFoundException, AuthorizationDeniedException, RequestNotValidException {
        if (UserUtility.isAdministrator(username)) {
            return true;
        }

        boolean hasPermission = false;
        Set<String> groups = this.retrieveUser(username).getGroups();

        try {
            if (DIP.class.getName().equals(objectClass)) {
                DIP dip = this.retrieveDIP(id);
                Permissions permissions = dip.getPermissions();
                Set<PermissionType> userPermissions = permissions.getUserPermissions(username);

                for (String group : groups) {
                    userPermissions.addAll(permissions.getGroupPermissions(group));
                }

                PermissionType type = PermissionType.valueOf(permissionType.toUpperCase());
                hasPermission = userPermissions.contains(type);
            } else if (AIP.class.getName().equals(objectClass)) {
                AIP aip = this.retrieveAIP(id);
                Permissions permissions = aip.getPermissions();
                Set<PermissionType> userPermissions = permissions.getUserPermissions(username);

                for (String group : groups) {
                    userPermissions.addAll(permissions.getGroupPermissions(group));
                }

                PermissionType type = PermissionType.valueOf(permissionType.toUpperCase());
                hasPermission = userPermissions.contains(type);
            } else {
                throw new RequestNotValidException(objectClass + " permission verification is not supported");
            }
        } catch (IllegalArgumentException e) {
            throw new RequestNotValidException(e);
        }

        return hasPermission;
    }

}