fr.ortolang.diffusion.core.CoreServiceBean.java Source code

Java tutorial

Introduction

Here is the source code for fr.ortolang.diffusion.core.CoreServiceBean.java

Source

package fr.ortolang.diffusion.core;

import com.fasterxml.jackson.core.type.TypeReference;

/*
 * #%L
 * ORTOLANG
 * A online network structure for hosting language resources and tools.
 * 
 * Jean-Marie Pierrel / ATILF UMR 7118 - CNRS / Universit de Lorraine
 * Etienne Petitjean / ATILF UMR 7118 - CNRS
 * Jrme Blanchard / ATILF UMR 7118 - CNRS
 * Bertrand Gaiffe / ATILF UMR 7118 - CNRS
 * Cyril Pestel / ATILF UMR 7118 - CNRS
 * Marie Tonnelier / ATILF UMR 7118 - CNRS
 * Ulrike Fleury / ATILF UMR 7118 - CNRS
 * Frdric Pierre / ATILF UMR 7118 - CNRS
 * Cline Moro / ATILF UMR 7118 - CNRS
 *  
 * This work is based on work done in the equipex ORTOLANG (http://www.ortolang.fr/), by several Ortolang contributors (mainly CNRTL and SLDR)
 * ORTOLANG is funded by the French State program "Investissements d'Avenir" ANR-11-EQPX-0032
 * %%
 * Copyright (C) 2013 - 2015 Ortolang Team
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jackson.JsonLoader;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import fr.ortolang.diffusion.*;
import fr.ortolang.diffusion.OrtolangEvent.ArgumentsBuilder;
import fr.ortolang.diffusion.OrtolangObjectState.Status;
import fr.ortolang.diffusion.core.entity.Collection;
import fr.ortolang.diffusion.core.entity.*;
import fr.ortolang.diffusion.core.indexing.*;
import fr.ortolang.diffusion.core.wrapper.CollectionWrapper;
import fr.ortolang.diffusion.core.wrapper.OrtolangObjectWrapper;
import fr.ortolang.diffusion.core.xml.*;
import fr.ortolang.diffusion.event.EventService;
import fr.ortolang.diffusion.extraction.ExtractionService;
import fr.ortolang.diffusion.extraction.ExtractionServiceException;
import fr.ortolang.diffusion.indexing.IndexingService;
import fr.ortolang.diffusion.indexing.IndexingServiceException;
import fr.ortolang.diffusion.indexing.OrtolangIndexableContent;
import fr.ortolang.diffusion.membership.MembershipService;
import fr.ortolang.diffusion.membership.MembershipServiceException;
import fr.ortolang.diffusion.membership.entity.Group;
import fr.ortolang.diffusion.notification.NotificationService;
import fr.ortolang.diffusion.notification.NotificationServiceException;
import fr.ortolang.diffusion.registry.*;
import fr.ortolang.diffusion.security.SecurityService;
import fr.ortolang.diffusion.security.SecurityServiceException;
import fr.ortolang.diffusion.security.authorisation.AccessDeniedException;
import fr.ortolang.diffusion.security.authorisation.AuthorisationService;
import fr.ortolang.diffusion.security.authorisation.AuthorisationServiceException;
import fr.ortolang.diffusion.security.authorisation.entity.AuthorisationPolicyTemplate;
import fr.ortolang.diffusion.store.binary.BinaryStoreService;
import fr.ortolang.diffusion.store.binary.BinaryStoreServiceException;
import fr.ortolang.diffusion.store.binary.DataCollisionException;
import fr.ortolang.diffusion.store.binary.DataNotFoundException;

import org.javers.core.Javers;
import org.javers.core.JaversBuilder;
import org.javers.core.diff.Change;
import org.javers.core.diff.Diff;
import org.jboss.ejb3.annotation.SecurityDomain;

import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ejb.*;
import javax.json.*;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

@Local(CoreService.class)
@Stateless(name = CoreService.SERVICE_NAME)
@SecurityDomain("ortolang")
@PermitAll
public class CoreServiceBean implements CoreService {

    private static final Logger LOGGER = Logger.getLogger(CoreServiceBean.class.getName());

    private static final String[] OBJECT_TYPE_LIST = new String[] { Workspace.OBJECT_TYPE, DataObject.OBJECT_TYPE,
            Collection.OBJECT_TYPE, Link.OBJECT_TYPE, MetadataObject.OBJECT_TYPE };
    private static final String[][] OBJECT_PERMISSIONS_LIST = new String[][] {
            { Workspace.OBJECT_TYPE, "read,update,delete,snapshot" },
            { DataObject.OBJECT_TYPE, "read,update,delete,download" },
            { Collection.OBJECT_TYPE, "read,update,delete,download" }, { Link.OBJECT_TYPE, "read,update,delete" },
            { MetadataObject.OBJECT_TYPE, "read,update,delete,download" } };

    private static final String[] RESERVED_ALIASES = new String[] { "key", "auth", "export" };
    private static final String[] RESERVED_TAG_NAMES = new String[] { Workspace.HEAD, Workspace.LATEST };

    @EJB
    private RegistryService registry;
    @EJB
    private BinaryStoreService binarystore;
    @EJB
    private MembershipService membership;
    @EJB
    private EventService events;
    @EJB
    private AuthorisationService authorisation;
    @EJB
    private IndexingService indexing;
    @EJB
    private NotificationService notification;
    @EJB
    private ExtractionService extraction;
    @EJB
    private SecurityService security;
    @PersistenceContext(unitName = "ortolangPU")
    private EntityManager em;
    @Resource
    private SessionContext ctx;

    public CoreServiceBean() {
    }

    public RegistryService getRegistryService() {
        return registry;
    }

    public void setRegistryService(RegistryService registryService) {
        this.registry = registryService;
    }

    public BinaryStoreService getBinaryStoreService() {
        return binarystore;
    }

    public void setBinaryStoreService(BinaryStoreService binaryStoreService) {
        this.binarystore = binaryStoreService;
    }

    public NotificationService getNotificationService() {
        return notification;
    }

    public void setNotificationService(NotificationService notificationService) {
        this.notification = notificationService;
    }

    public MembershipService getMembershipService() {
        return membership;
    }

    public void setMembershipService(MembershipService membership) {
        this.membership = membership;
    }

    public AuthorisationService getAuthorisationService() {
        return authorisation;
    }

    public void setAuthorisationService(AuthorisationService authorisation) {
        this.authorisation = authorisation;
    }

    public IndexingService getIndexingService() {
        return indexing;
    }

    public void setIndexingService(IndexingService indexing) {
        this.indexing = indexing;
    }

    public void setEntityManager(EntityManager em) {
        this.em = em;
    }

    public EntityManager getEntityManager() {
        return this.em;
    }

    public void setSessionContext(SessionContext ctx) {
        this.ctx = ctx;
    }

    public SessionContext getSessionContext() {
        return this.ctx;
    }

    /* Workspace */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Workspace createWorkspace(String wskey, String name, String type) throws CoreServiceException,
            KeyAlreadyExistsException, AccessDeniedException, AliasAlreadyExistsException {
        WorkspaceAlias alias = new WorkspaceAlias();
        em.persist(alias);
        return createWorkspace(wskey, alias.getValue(), name, type);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Workspace createWorkspace(String wskey, String alias, String name, String type)
            throws CoreServiceException, KeyAlreadyExistsException, AccessDeniedException,
            AliasAlreadyExistsException {
        LOGGER.log(Level.FINE, "creating workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);

            String members = UUID.randomUUID().toString();
            membership.createGroup(members, name + "'s Members",
                    "Members of a workspace have all permissions on workspace content");
            membership.addMemberInGroup(members, caller);
            Map<String, List<String>> membersrules = authorisation.getPolicyRules(members);
            membersrules.put(MembershipService.MODERATORS_GROUP_KEY, Arrays.asList("read", "update"));
            membersrules.put(MembershipService.PUBLISHERS_GROUP_KEY, Arrays.asList("read"));
            membersrules.put(MembershipService.REVIEWERS_GROUP_KEY, Arrays.asList("read"));
            authorisation.setPolicyRules(members, membersrules);

            String head = UUID.randomUUID().toString();
            Collection collection = new Collection();
            collection.setId(UUID.randomUUID().toString());
            collection.setName("root");
            collection.setRoot(true);
            collection.setClock(1);
            em.persist(collection);

            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(head, collection.getObjectIdentifier(), caller, properties);
            indexing.index(head);

            Map<String, List<String>> rules = new HashMap<String, List<String>>();
            rules.put(members, Arrays.asList("read", "create", "update", "delete", "download"));
            rules.put(MembershipService.MODERATORS_GROUP_KEY, Arrays.asList("read", "update", "download"));
            rules.put(MembershipService.PUBLISHERS_GROUP_KEY, Arrays.asList("read", "download"));
            rules.put(MembershipService.REVIEWERS_GROUP_KEY, Arrays.asList("read", "download"));
            authorisation.createPolicy(head, members);
            authorisation.setPolicyRules(head, rules);

            if (Arrays.asList(RESERVED_ALIASES).contains(alias)) {
                throw new CoreServiceException(alias + " is reserved and cannot be used as an alias");
            }

            List<Workspace> results = em.createNamedQuery("findWorkspaceByAlias", Workspace.class)
                    .setParameter("alias", alias).getResultList();
            if (!results.isEmpty()) {
                ctx.setRollbackOnly();
                throw new AliasAlreadyExistsException(
                        "a workspace with alias [" + alias + "] already exists in storage");
            }
            PathBuilder palias;
            try {
                palias = PathBuilder.fromPath(alias);
                if (palias.isRoot() || palias.depth() > 1) {
                    throw new InvalidPathException("incorrect depth for an alias path");
                }
            } catch (InvalidPathException e) {
                LOGGER.log(Level.SEVERE, "invalid alias for workspace", e);
                throw new CoreServiceException("alias is invalid", e);
            }
            String id = UUID.randomUUID().toString();
            Workspace workspace = new Workspace();
            workspace.setId(id);
            workspace.setKey(wskey);
            workspace.setAlias(palias.part());
            workspace.setName(name);
            workspace.setType(type);
            workspace.setHead(head);
            workspace.setChanged(true);
            workspace.setMembers(members);
            em.persist(workspace);

            registry.register(wskey, workspace.getObjectIdentifier(), caller);

            Map<String, List<String>> wsrules = new HashMap<String, List<String>>();
            wsrules.put(members, Collections.singletonList("read"));
            wsrules.put(MembershipService.UNAUTHENTIFIED_IDENTIFIER, Collections.singletonList("read"));
            wsrules.put(MembershipService.MODERATORS_GROUP_KEY,
                    Arrays.asList("read", "create", "update", "delete"));
            wsrules.put(MembershipService.PUBLISHERS_GROUP_KEY, Arrays.asList("read"));
            wsrules.put(MembershipService.REVIEWERS_GROUP_KEY, Arrays.asList("read"));
            authorisation.createPolicy(wskey, caller);
            authorisation.setPolicyRules(wskey, wsrules);

            indexing.index(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(1).addArgument("ws-alias", alias);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "create"),
                    argsBuilder.build());
            argsBuilder.addArgument("key", head).addArgument("path", "/");
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE, "create"),
                    argsBuilder.build());

            return workspace;
        } catch (KeyAlreadyExistsException e) {
            ctx.setRollbackOnly();
            throw e;
        } catch (KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | IdentifierAlreadyRegisteredException | AuthorisationServiceException | MembershipServiceException
                | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while creating workspace", e);
            throw new CoreServiceException("unable to create workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Workspace readWorkspace(String wskey)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "reading workspace [" + wskey + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setKey(wskey);
            return workspace;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while reading workspace", e);
            throw new CoreServiceException("unable to read workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> findWorkspacesForProfile(String profile)
            throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "finding workspaces for profile");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);

            List<String> groups = membership.getProfileGroups(profile);
            return findWorkspacesForGroups(groups);
        } catch (MembershipServiceException | AuthorisationServiceException | RegistryServiceException
                | KeyNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding workspaces for profile", e);
            throw new CoreServiceException("unable to find workspaces for profile", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> systemFindWorkspacesForProfile(String profile) throws CoreServiceException {
        LOGGER.log(Level.FINE, "#SYSTEM# finding workspaces for profile");
        try {
            List<String> groups = Arrays.asList(membership.systemReadProfile(profile).getGroups());
            return findWorkspacesForGroups(groups);
        } catch (MembershipServiceException | RegistryServiceException | KeyNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding workspaces for profile", e);
            throw new CoreServiceException("unable to find workspaces for profile", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private List<String> findWorkspacesForGroups(List<String> groups) throws RegistryServiceException {
        if (groups.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> keys = new ArrayList<>();
        TypedQuery<Workspace> query = em.createNamedQuery("findWorkspaceByMember", Workspace.class)
                .setParameter("groups", groups);
        List<Workspace> workspaces = query.getResultList();
        for (Workspace workspace : workspaces) {
            OrtolangObjectIdentifier identifier = workspace.getObjectIdentifier();
            try {
                keys.add(registry.lookup(identifier));
            } catch (IdentifierNotRegisteredException e) {
                LOGGER.log(Level.SEVERE,
                        "a workspace with an unregistered identifier has be found : " + identifier);
            }
        }
        return keys;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> findWorkspacesAliasForProfile(String profile)
            throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "finding workspace alias for profile");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);

            List<String> groups = membership.getProfileGroups(profile);
            if (groups.isEmpty()) {
                return Collections.emptyList();
            }

            List<String> aliases = new ArrayList<String>();
            TypedQuery<Workspace> query = em.createNamedQuery("findWorkspaceByMember", Workspace.class)
                    .setParameter("groups", groups);
            List<Workspace> workspaces = query.getResultList();
            for (Workspace workspace : workspaces) {
                OrtolangObjectIdentifier identifier = workspace.getObjectIdentifier();
                try {
                    if (workspace.getAlias() != null && workspace.getAlias().length() > 0) {
                        registry.lookup(identifier);
                        aliases.add(workspace.getAlias());
                    }
                } catch (IdentifierNotRegisteredException e) {
                    LOGGER.log(Level.SEVERE,
                            "a workspace with an unregistered identifier has be found : " + identifier);
                }
            }

            return aliases;
        } catch (MembershipServiceException | AuthorisationServiceException | RegistryServiceException
                | KeyNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding workspaces for profile", e);
            throw new CoreServiceException("unable to find workspaces for profile", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> listAllWorkspaceAlias() throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "listing all workspaces alias");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            authorisation.checkSuperUser(caller);
            TypedQuery<String> query = em.createNamedQuery("listAllWorkspaceAlias", String.class);
            return query.getResultList();
        } catch (AuthorisationServiceException e) {
            LOGGER.log(Level.SEVERE, "unable to list all workspace aliases", e);
            throw new CoreServiceException("unable to list all workspace aliases", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public String snapshotWorkspace(String wskey) throws CoreServiceException, KeyNotFoundException,
            AccessDeniedException, WorkspaceReadOnlyException, WorkspaceUnchangedException {
        LOGGER.log(Level.FINE, "snapshoting workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "update");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, workspace)) {
                throw new WorkspaceReadOnlyException(
                        "unable to snapshot workspace with key [" + wskey + "] because it is read only");
            }

            String name = String.valueOf(workspace.getClock());

            if (!workspace.hasChanged()) {
                throw new WorkspaceUnchangedException(
                        "unable to snapshot because workspace has no pending modifications since last snapshot");
            }

            try {
                JsonObjectBuilder builder = Json.createObjectBuilder();
                builder.add("wskey", wskey);
                builder.add("snapshotName", name);
                builder.add("wsalias", workspace.getAlias());

                JsonObject jsonObject = builder.build();
                String hash = binarystore.put(new ByteArrayInputStream(jsonObject.toString().getBytes()));

                List<String> mds = findMetadataObjectsForTargetAndName(workspace.getHead(),
                        MetadataFormat.WORKSPACE);

                if (mds.isEmpty()) {
                    LOGGER.log(Level.INFO, "creating workspace metadata for root collection");
                    createMetadataObject(wskey, "/", MetadataFormat.WORKSPACE, hash, null, false);
                } else {
                    LOGGER.log(Level.INFO, "updating workspace metadata for root collection");
                    updateMetadataObject(wskey, "/", MetadataFormat.WORKSPACE, hash, null, false);
                }
            } catch (BinaryStoreServiceException | DataCollisionException | CoreServiceException
                    | KeyNotFoundException | InvalidPathException | AccessDeniedException | MetadataFormatException
                    | PathNotFoundException | KeyAlreadyExistsException e) {
                throw new CoreServiceException(
                        "cannot create workspace metadata for collection root : " + e.getMessage());
            }

            workspace.setKey(wskey);
            workspace.incrementClock();

            OrtolangObjectIdentifier hidentifier = registry.lookup(workspace.getHead());
            checkObjectType(hidentifier, Collection.OBJECT_TYPE);
            Collection collection = em.find(Collection.class, hidentifier.getId());
            if (collection == null) {
                throw new CoreServiceException(
                        "unable to load head collection with id [" + hidentifier.getId() + "] from storage");
            }
            collection.setKey(workspace.getHead());

            Collection clone = cloneCollection(workspace.getHead(), collection, workspace.getClock());

            workspace.addSnapshot(new SnapshotElement(name, collection.getKey()));
            workspace.setHead(clone.getKey());
            workspace.setChanged(false);
            em.merge(workspace);

            registry.update(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("ws-alias", workspace.getAlias())
                    .addArgument("snapshot-name", name);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "snapshot"),
                    argsBuilder.build());
            return name;
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | CloneException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while snapshoting workspace", e);
            throw new CoreServiceException("unable to snapshot workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String getLatestSnapshot(String wskey)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }

            return String.valueOf(workspace.getClock() - 1);
        } catch (MembershipServiceException | AuthorisationServiceException | RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while getting latest workspace snapshot", e);
            throw new CoreServiceException("unable to get latest snapshot for workspace with key [" + wskey + "]",
                    e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void systemTagWorkspace(String wskey, String tag, String snapshot)
            throws CoreServiceException, KeyNotFoundException {
        LOGGER.log(Level.FINE, "#SYSTEM# tagging workspace [" + wskey + "] and snapshot [" + snapshot + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setKey(wskey);

            try {
                PathBuilder pname = PathBuilder.newInstance().path(tag);
                if (pname.depth() > 1) {
                    throw new CoreServiceException("tag name is invalid: " + tag);
                }
                if (Arrays.asList(RESERVED_TAG_NAMES).contains(pname.part())) {
                    throw new CoreServiceException(pname.part() + " is reserved and cannot be used as tag name");
                }
                tag = pname.part();
                TagElement tagElement = workspace.findTagByName(tag);
                if (tagElement != null) {
                    workspace.removeTag(tagElement);
                    String oldRoot = workspace.findSnapshotByName(tagElement.getSnapshot()).getKey();
                    indexing.remove(oldRoot);
                }
                if (!workspace.containsSnapshotName(snapshot)) {
                    throw new CoreServiceException(
                            "the snapshot with name '" + snapshot + "' does not exists in this workspace");
                }

                workspace.addTag(new TagElement(tag, snapshot));
                em.merge(workspace);
                registry.update(wskey);
                indexing.index(wskey);

                ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("ws-alias", workspace.getAlias())
                        .addArgument("tag-name", tag);
                notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                        OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "tag"),
                        argsBuilder.build());

            } catch (InvalidPathException e) {
                throw new CoreServiceException("tag name is invalid: " + tag);
            }

        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while tagging workspace snapshot", e);
            throw new CoreServiceException("unable to tag workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void updateWorkspace(String wskey, String name)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "updating workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "update");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, workspace)) {
                throw new WorkspaceReadOnlyException(
                        "unable to update workspace with key [" + wskey + "] because it is read only");
            }
            workspace.setName(name);
            em.merge(workspace);

            registry.update(wskey);
            indexing.index(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("ws-alias", workspace.getAlias())
                    .addArgument("name", name);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "update"),
                    argsBuilder.build());
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while updating workspace", e);
            throw new CoreServiceException("unable to update workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Workspace archiveWorkspace(String wskey, Boolean archive)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "archiving workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            if (!MembershipService.SUPERUSER_IDENTIFIER.equals(caller)) {
                throw new CoreServiceException(
                        "only " + MembershipService.SUPERUSER_IDENTIFIER + " can archive workspace");
            }

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "update");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setArchive(archive);
            workspace.setKey(wskey);
            em.merge(workspace);

            registry.update(wskey);
            indexing.index(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("ws-alias", workspace.getAlias())
                    .addArgument("archive", archive.toString());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "archive"),
                    argsBuilder.build());

            return workspace;
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while archiving workspace", e);
            throw new CoreServiceException("unable to archive workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteWorkspace(String wskey)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException, WorkspaceReadOnlyException {
        deleteWorkspace(wskey, false);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteWorkspace(String wskey, boolean force)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "deleting workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            if (force && !MembershipService.SUPERUSER_IDENTIFIER.equals(caller)) {
                throw new CoreServiceException(
                        "only " + MembershipService.SUPERUSER_IDENTIFIER + " can force workspace delete");
            }

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "delete");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (!force) {
                if (applyReadOnly(caller, subjects, workspace)) {
                    throw new WorkspaceReadOnlyException(
                            "unable to delete workspace with key [" + wskey + "] because it is read only");
                }
                if (workspace.getType().equals(WorkspaceType.SYSTEM.name())) {
                    throw new CoreServiceException("unable to delete with key [" + wskey
                            + "] because it is of type: " + WorkspaceType.SYSTEM.name());
                }
                String current = workspace.getHead();
                while (current != null) {
                    String parent = registry.getParent(current);
                    if (parent != null && registry.getPublicationStatus(parent)
                            .equals(OrtolangObjectState.Status.PUBLISHED.value())) {
                        throw new CoreServiceException("unable to delete workspace with key [" + wskey
                                + "] because it has a published version");
                    }
                    current = parent;
                }
            }
            workspace.setAlias(null);
            em.merge(workspace);

            membership.deleteGroup(workspace.getMembers());
            registry.delete(wskey);
            indexing.remove(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(1).addArgument("ws-alias", workspace.getAlias());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "delete"),
                    argsBuilder.build());
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while deleting workspace", e);
            throw new CoreServiceException("unable to delete workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void changeWorkspaceOwner(String wskey, String newOwner) throws CoreServiceException {
        LOGGER.log(Level.FINE, "changing workspace [" + wskey + "] owner to: " + newOwner);
        try {
            Workspace workspace = readWorkspace(wskey);
            security.changeOwner(wskey, newOwner);
            membership.addMemberInGroup(workspace.getMembers(), newOwner);
            security.changeOwner(workspace.getMembers(), newOwner);
        } catch (SecurityServiceException | AccessDeniedException | KeyNotFoundException
                | MembershipServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while changing workspace owner", e);
            throw new CoreServiceException("unable to change owner of workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public void notifyWorkspaceOwner(String wskey, String email, String message) throws CoreServiceException {
        LOGGER.log(Level.FINE, "notify owner of workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("email", email)
                    .addArgument("message", message);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "notify-owner"),
                    argsBuilder.build());
        } catch (RegistryServiceException | KeyNotFoundException | NotificationServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while trying to notify workspace owner", e);
            throw new CoreServiceException("unable to notify owner of workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public void notifyWorkspaceMembers(String wskey, String email, String message) throws CoreServiceException {
        LOGGER.log(Level.FINE, "notify members of workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(2).addArgument("email", email)
                    .addArgument("message", message);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "notify-members"),
                    argsBuilder.build());
        } catch (RegistryServiceException | KeyNotFoundException | NotificationServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while trying to notify workspace members", e);
            throw new CoreServiceException("unable to notify members of workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String resolveWorkspaceAlias(String alias) throws CoreServiceException, AliasNotFoundException {
        LOGGER.log(Level.FINE, "finding workspace for alias: " + alias);
        try {
            TypedQuery<Workspace> query = em.createNamedQuery("findWorkspaceByAlias", Workspace.class)
                    .setParameter("alias", alias);
            try {
                Workspace workspace = query.getSingleResult();
                return registry.lookup(workspace.getObjectIdentifier());
            } catch (NoResultException e) {
                throw new AliasNotFoundException(alias);
            }
        } catch (RegistryServiceException | IdentifierNotRegisteredException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during resolving workspace alias: " + alias, e);
            throw new CoreServiceException("unable to resolve workspace alias: " + alias, e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String resolveWorkspacePath(String wskey, String root, String path)
            throws CoreServiceException, InvalidPathException, PathNotFoundException {
        LOGGER.log(Level.FINE,
                "resolving into workspace [" + wskey + "] and root [" + root + "] path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            PathBuilder ppath = npath.clone().parent();

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            String rroot = ws.getHead();
            if (root != null && root.length() > 0 && !root.equals(Workspace.HEAD)) {
                String snapshot = root;
                TagElement tag = ws.findTagByName(root);
                if (tag != null) {
                    LOGGER.log(Level.FINEST, "root is a tag, resolving tag snapshot");
                    snapshot = tag.getSnapshot();
                }
                SnapshotElement element = ws.findSnapshotByName(snapshot);
                if (element == null) {
                    throw new RootNotFoundException(root);
                } else {
                    rroot = element.getKey();
                }
            }

            if (npath.isRoot()) {
                return rroot;
            }

            Collection parent = readCollectionAtPath(rroot, ppath);
            LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());

            CollectionElement element = parent.findElementByName(npath.part());
            if (element == null) {
                throw new PathNotFoundException(npath.build());
            }
            return element.getKey();
        } catch (KeyNotFoundException | RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during resolving path", e);
            throw new CoreServiceException("unable to resolve into workspace [" + wskey + "] path [" + path + "]",
                    e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String resolveWorkspaceMetadata(String wskey, String root, String path, String name)
            throws CoreServiceException, InvalidPathException, AccessDeniedException, PathNotFoundException {
        LOGGER.log(Level.FINE, "resolving into workspace [" + wskey + "] and root [" + root
                + "] metadata with name [" + name + "] at path [" + path + "]");
        try {
            String key = resolveWorkspacePath(wskey, root, path);
            MetadataElement cmdelement = loadMetadataElement(name, key);
            if (cmdelement == null) {
                throw new CoreServiceException(
                        "a metadata object with name [" + name + "] does not exists for at path [" + path + "]");
            }
            return cmdelement.getKey();
        } catch (KeyNotFoundException | RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during resolving metadata", e);
            throw new CoreServiceException("unable to resolve into workspace [" + wskey + "] metadata name [" + name
                    + "] at [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Set<String> buildWorkspaceReviewList(String wskey, String snapshot)
            throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "building review list for workspace [" + wskey + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (!workspace.containsSnapshotName(snapshot)) {
                throw new CoreServiceException("the workspace with key: " + wskey
                        + " does not containt a snapshot with name: " + snapshot);
            }
            String root = workspace.findSnapshotByName(snapshot).getKey();

            Set<String> keys = new HashSet<String>();
            systemListCollectionKeys(root, keys);
            return keys;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during building workspace review list", e);
            throw new CoreServiceException("unexpected error while trying to build workspace review list", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Map<String, Map<String, List<String>>> buildWorkspacePublicationMap(String wskey, String snapshot)
            throws CoreServiceException {
        LOGGER.log(Level.FINE, "building publication map for workspace [" + wskey + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (!workspace.containsSnapshotName(snapshot)) {
                throw new CoreServiceException(
                        "the workspace with key: " + wskey + " does not contain a snapshot with name: " + snapshot);
            }
            String root = workspace.findSnapshotByName(snapshot).getKey();

            Map<String, Map<String, List<String>>> map = new HashMap<String, Map<String, List<String>>>();

            AuthorisationPolicyTemplate defaultTemplate = authorisation
                    .getPolicyTemplate(AuthorisationPolicyTemplate.DEFAULT);
            Map<String, String> aclParams = new HashMap<String, String>();
            aclParams.put("${workspace.members}", workspace.getMembers());
            if (workspace.getPrivileged() != "") {
                aclParams.put("${workspace.privileged}", workspace.getPrivileged());
            }
            builtPublicationMap(root, map, authorisation.getPolicyRules(defaultTemplate.getTemplate()), aclParams);
            return map;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | OrtolangException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during building workspace publication map", e);
            throw new CoreServiceException("unexpected error while trying to build workspace publication map", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private void builtPublicationMap(String key, Map<String, Map<String, List<String>>> map,
            Map<String, List<String>> current, Map<String, String> params)
            throws KeyNotFoundException, CoreServiceException, OrtolangException {
        Object object = readObject(key);
        if (object instanceof MetadataSource) {
            MetadataElement mde = ((MetadataSource) object).findMetadataByName(MetadataFormat.ACL);
            if (mde != null) {
                LOGGER.log(Level.FINE, "ACL metadata found, load json, find policy template and render it...");
                MetadataObject md = (MetadataObject) readObject(mde.getKey());
                try {
                    JsonReader reader = Json.createReader(binarystore.get(md.getStream()));
                    JsonObject json = reader.readObject();
                    String template = json.getString("template");
                    reader.close();
                    AuthorisationPolicyTemplate policy = authorisation.getPolicyTemplate(template);
                    Map<String, List<String>> rules = authorisation.getPolicyRules(policy.getTemplate());
                    Map<String, List<String>> filtered = new HashMap<String, List<String>>();
                    for (Entry<String, List<String>> entry : rules.entrySet()) {
                        if (params.containsKey(entry.getKey())) {
                            filtered.put(params.get(entry.getKey()), entry.getValue());
                        } else {
                            filtered.put(entry.getKey(), entry.getValue());
                        }
                    }
                    current = filtered;
                } catch (AuthorisationServiceException | BinaryStoreServiceException | DataNotFoundException e) {
                    LOGGER.log(Level.SEVERE, "unable to read acl metadata", e);
                }
            }
        }
        map.put(key, current);
        if (object instanceof MetadataSource) {
            for (MetadataElement element : ((MetadataSource) object).getMetadatas()) {
                map.put(element.getKey(), current);
            }
        }
        if (object instanceof Collection) {
            for (CollectionElement element : ((Collection) object).getElements()) {
                builtPublicationMap(element.getKey(), map, current, params);
            }
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Set<OrtolangObjectPid> buildWorkspacePidList(String wskey, String tag) throws CoreServiceException {
        LOGGER.log(Level.FINE, "building pid list for workspace [" + wskey + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            if (!workspace.containsTagName(tag)) {
                throw new CoreServiceException(
                        "the workspace with key: " + wskey + " does not containt a tag with name: " + tag);
            }
            String snapshot = workspace.findTagByName(tag).getSnapshot();
            String root = workspace.findSnapshotByName(snapshot).getKey();

            Set<OrtolangObjectPid> pids = new HashSet<OrtolangObjectPid>();
            String apiUrlBase = OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_URL_SSL)
                    + OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.API_PATH_CONTENT);
            String marketUrlBase = OrtolangConfig.getInstance()
                    .getProperty(OrtolangConfig.Property.MARKET_SERVER_URL) + "market/item";
            buildHandleList(workspace.getAlias(), tag, root, pids, PathBuilder.newInstance(), apiUrlBase,
                    marketUrlBase);
            return pids;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | OrtolangException | InvalidPathException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during building workspace pid list", e);
            throw new CoreServiceException("unexpected error while trying to build workspace pid list", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private void buildHandleList(String wsalias, String tag, String key, Set<OrtolangObjectPid> pids,
            PathBuilder path, String apiUrlBase, String marketUrlBase)
            throws CoreServiceException, KeyNotFoundException, OrtolangException, InvalidPathException {
        try {
            OrtolangObject object = findObject(key);
            LOGGER.log(Level.FINE, "Generating pid for key: " + key);
            String target = ((path.isRoot()) ? marketUrlBase : apiUrlBase) + "/" + wsalias + "/" + tag
                    + ((path.isRoot()) ? "" : path.build());
            String dynHandle = OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.HANDLE_PREFIX) + "/"
                    + wsalias + ((path.isRoot()) ? "" : path.build());
            String staticHandle = OrtolangConfig.getInstance().getProperty(OrtolangConfig.Property.HANDLE_PREFIX)
                    + "/" + wsalias + "/" + tag + ((path.isRoot()) ? "" : path.build());
            OrtolangObjectPid dpid = new OrtolangObjectPid(OrtolangObjectPid.Type.HANDLE, dynHandle, key, target,
                    false);
            boolean adddpid = true;
            for (OrtolangObjectPid pid : pids) {
                if (pid.getName().equals(dpid.getName()) && pid.isUserbased()) {
                    adddpid = false;
                    break;
                }
            }
            if (adddpid) {
                pids.add(dpid);
            }
            OrtolangObjectPid spid = new OrtolangObjectPid(OrtolangObjectPid.Type.HANDLE, staticHandle, key, target,
                    false);
            boolean addspid = true;
            for (OrtolangObjectPid pid : pids) {
                if (pid.getName().equals(spid.getName()) && pid.isUserbased()) {
                    addspid = false;
                    break;
                }
            }
            if (addspid) {
                pids.add(spid);
            }
            if (object instanceof MetadataSource) {
                MetadataElement mde = ((MetadataSource) object).findMetadataByName(MetadataFormat.PID);
                if (mde != null) {
                    LOGGER.log(Level.FINE, "PID metadata found, load json and generate corresponding pids");
                    MetadataObject md = readMetadataObject(mde.getKey());
                    try {
                        JsonReader reader = Json.createReader(binarystore.get(md.getStream()));
                        JsonObject json = reader.readObject();
                        if (json.containsKey("pids")) {
                            JsonArray jpids = json.getJsonArray("pids");
                            for (int i = 0; i < jpids.size(); i++) {
                                JsonObject jpid = jpids.getJsonObject(i);
                                LOGGER.log(Level.FINE, "Generating metadata based pid for key: " + key);
                                String ctarget = ((path.isRoot()) ? marketUrlBase : apiUrlBase) + "/" + wsalias
                                        + "/" + tag + ((path.isRoot()) ? "" : path.build());
                                OrtolangObjectPid upid = new OrtolangObjectPid(OrtolangObjectPid.Type.HANDLE,
                                        jpid.getString("value"), key, ctarget, true);
                                Iterator<OrtolangObjectPid> iter = pids.iterator();
                                while (iter.hasNext()) {
                                    OrtolangObjectPid pid = iter.next();
                                    if (pid.getName().equals(upid.getName())) {
                                        iter.remove();
                                    }
                                }
                                pids.add(upid);
                            }
                        }
                        reader.close();
                    } catch (BinaryStoreServiceException | DataNotFoundException e) {
                        LOGGER.log(Level.SEVERE, "unable to read pid metadata", e);
                    }
                }
            }
            if (object instanceof Collection) {
                for (CollectionElement element : ((Collection) object).getElements()) {
                    buildHandleList(wsalias, tag, element.getKey(), pids, path.clone().path(element.getName()),
                            apiUrlBase, marketUrlBase);
                }
            }
        } catch (AccessDeniedException e) {
            LOGGER.log(Level.INFO, "Unable to generate a PID for an object that has been set private.");
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Map<String, String> listWorkspaceContent(String wskey, String snapshot) throws CoreServiceException {
        LOGGER.log(Level.FINE, "listing content of workspace [" + wskey + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            String root;
            if (Workspace.HEAD.equals(snapshot)) {
                root = workspace.getHead();
            } else {
                if (!workspace.containsSnapshotName(snapshot)) {
                    throw new CoreServiceException("the workspace with key: " + wskey
                            + " does not contain a snapshot with name: " + snapshot);
                }
                root = workspace.findSnapshotByName(snapshot).getKey();
            }

            return listCollectionContent(root).getPathKeyMap();
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | OrtolangException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during listing workspace content", e);
            throw new CoreServiceException("unexpected error while trying to list workspace content", e);
        }
    }

    private CollectionContent listCollectionContent(String key) throws CoreServiceException {
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            OrtolangObjectIdentifier identifier = registry.lookup(key);
            checkObjectType(identifier, Collection.OBJECT_TYPE);
            authorisation.checkPermission(key, subjects, "read");
            CollectionContent collectionContent = new CollectionContent();
            listContent(key, PathBuilder.newInstance(), collectionContent);
            return collectionContent;
        } catch (OrtolangException | MembershipServiceException | KeyNotFoundException | RegistryServiceException
                | AuthorisationServiceException | InvalidPathException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during listing collection [" + key + "] content",
                    e);
            throw new CoreServiceException("unexpected error while trying to list collection [" + key + "] content",
                    e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private void listContent(String key, PathBuilder path, CollectionContent collectionContent)
            throws OrtolangException, InvalidPathException {
        OrtolangObject object = findObject(key);
        collectionContent.add(object, path.build());
        if (object instanceof Collection) {
            for (CollectionElement element : ((Collection) object).getElements()) {
                listContent(element.getKey(), path.clone().path(element.getName()), collectionContent);
            }
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private CollectionWrapper getWorkspaceContentTree(String key, PathBuilder path, CollectionWrapper parent)
            throws KeyNotFoundException, InvalidPathException, RegistryServiceException {
        OrtolangObjectIdentifier identifier = registry.lookup(key);
        OrtolangObjectWrapper ortolangObjectWrapper;
        switch (identifier.getType()) {
        case Collection.OBJECT_TYPE:
            Collection collection = em.find(Collection.class, identifier.getId());
            ortolangObjectWrapper = OrtolangObjectWrapper.fromOrtolangObject(collection, path.build());
            for (CollectionElement element : collection.getElements()) {
                getWorkspaceContentTree(element.getKey(), path.clone().path(element.getName()),
                        (CollectionWrapper) ortolangObjectWrapper);
            }
            if (parent != null) {
                parent.addChild(ortolangObjectWrapper);
            }
            return (CollectionWrapper) ortolangObjectWrapper;
        case DataObject.OBJECT_TYPE:
            DataObject dataObject = em.find(DataObject.class, identifier.getId());
            ortolangObjectWrapper = OrtolangObjectWrapper.fromOrtolangObject(dataObject, path.build());
            parent.addChild(ortolangObjectWrapper);
            break;
        case Link.OBJECT_TYPE:
            Link link = em.find(Link.class, identifier.getId());
            ortolangObjectWrapper = OrtolangObjectWrapper.fromOrtolangObject(link, path.build());
            parent.addChild(ortolangObjectWrapper);
            break;
        }
        return null;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Change> diffWorkspaceContent(String wskey, String lsnapshot, String rsnapshot)
            throws CoreServiceException {
        LOGGER.log(Level.FINE, "diff content of workspace [" + wskey + "] between snapshots [" + lsnapshot
                + "] and [" + rsnapshot + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            authorisation.checkPermission(wskey, subjects, "read");

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            String lroot;
            if (lsnapshot.equals(Workspace.HEAD)) {
                lroot = workspace.getHead();
            } else {
                if (!workspace.containsSnapshotName(lsnapshot)) {
                    throw new CoreServiceException("the workspace with key: " + wskey
                            + " does not containt a snapshot with name: " + lsnapshot);
                }
                lroot = workspace.findSnapshotByName(lsnapshot).getKey();
            }
            String rroot;
            if (rsnapshot.equals(Workspace.HEAD)) {
                rroot = workspace.getHead();
            } else {
                if (!workspace.containsSnapshotName(rsnapshot)) {
                    throw new CoreServiceException("the workspace with key: " + wskey
                            + " does not containt a snapshot with name: " + rsnapshot);
                }
                rroot = workspace.findSnapshotByName(rsnapshot).getKey();
            }

            CollectionWrapper lcontent = getWorkspaceContentTree(lroot, PathBuilder.newInstance(), null);
            CollectionWrapper rcontent = getWorkspaceContentTree(rroot, PathBuilder.newInstance(), null);

            Javers javers = JaversBuilder.javers().build();
            Diff diff = javers.compare(lcontent, rcontent);

            return diff.getChanges();

        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | OrtolangException | InvalidPathException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during diff workspace content", e);
            throw new CoreServiceException("unexpected error while trying to diff workspace content", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String findWorkspaceLatestPublishedSnapshot(String wskey)
            throws CoreServiceException, KeyNotFoundException {
        LOGGER.log(Level.FINE, "find workspace [" + wskey + "] latest published snapshot");
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setKey(wskey);

            String current = workspace.getHead();
            boolean found = false;
            while (!found && current != null) {
                String parent = registry.getParent(current);
                if (parent != null && registry.getPublicationStatus(parent)
                        .equals(OrtolangObjectState.Status.PUBLISHED.value())) {
                    found = true;
                }
                current = parent;
            }

            if (current != null) {
                SnapshotElement snapshot = workspace.findSnapshotByKey(current);
                if (snapshot != null) {
                    return snapshot.getName();
                }
            }

            return null;
        } catch (RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while finding workspace latest published snapshot",
                    e);
            throw new CoreServiceException(
                    "unable to find latest published snapshot for workspace with key [" + wskey + "]", e);
        }
    }

    /* Collections */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Collection createCollection(String wskey, String path) throws CoreServiceException, KeyNotFoundException,
            InvalidPathException, AccessDeniedException, PathNotFoundException, PathAlreadyExistsException,
            WorkspaceReadOnlyException, KeyAlreadyExistsException {
        String key = UUID.randomUUID().toString();
        try {
            return createCollection(wskey, key, path);
        } catch (KeyAlreadyExistsException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.WARNING, "the generated key already exists : " + key);
            throw e;
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Collection createCollection(String wskey, String key, String path)
            throws CoreServiceException, KeyAlreadyExistsException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE,
                "creating collection with key [" + key + "] into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("forbidden to create the root collection");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to create collection in workspace with key [" + wskey
                        + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "create");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'create' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());

            if (parent.containsElementName(npath.part())) {
                throw new PathAlreadyExistsException(npath.build());
            }

            String id = UUID.randomUUID().toString();
            Collection collection = new Collection();
            collection.setId(id);
            collection.setKey(key);
            collection.setName(npath.part());
            collection.setRoot(false);
            collection.setClock(ws.getClock());
            em.persist(collection);
            LOGGER.log(Level.FINEST, "collection [" + key + "] created");

            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(key, collection.getObjectIdentifier(), caller, properties);
            indexing.index(key);

            authorisation.clonePolicy(key, ws.getHead());
            LOGGER.log(Level.FINEST, "security policy cloned from head collection to key [" + key + "]");

            parent.addElement(new CollectionElement(Collection.OBJECT_TYPE, collection.getName(),
                    System.currentTimeMillis(), 0, Collection.MIME_TYPE, key));
            em.merge(parent);
            registry.update(parent.getKey());
            LOGGER.log(Level.FINEST, "collection [" + key + "] added to parent [" + parent.getKey() + "]");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(3).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", key).addArgument("path", path);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE, "create"),
                    argsBuilder.build());

            return collection;
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | IdentifierAlreadyRegisteredException | AuthorisationServiceException | MembershipServiceException
                | IndexingServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during collection creation", e);
            ctx.setRollbackOnly();
            throw new CoreServiceException(
                    "unable to create collection into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Collection readCollection(String key)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "reading collection with key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier cidentifier = registry.lookup(key);
            checkObjectType(cidentifier, Collection.OBJECT_TYPE);
            authorisation.checkPermission(key, subjects, "read");

            Collection collection = em.find(Collection.class, cidentifier.getId());
            if (collection == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + cidentifier.getId() + "] from storage");
            }
            collection.setKey(key);

            return collection;
        } catch (MembershipServiceException | AuthorisationServiceException | RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error while reading collection", e);
            throw new CoreServiceException("unable to read collection with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public String resolvePathFromCollection(String key, String path) throws KeyNotFoundException,
            CoreServiceException, AccessDeniedException, InvalidPathException, PathNotFoundException {
        LOGGER.log(Level.FINE, "reading collection with key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier cidentifier = registry.lookup(key);
            checkObjectType(cidentifier, Collection.OBJECT_TYPE);

            Collection collection = em.find(Collection.class, cidentifier.getId());
            if (collection == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + cidentifier.getId() + "] from storage");
            }
            collection.setKey(key);

            PathBuilder pathTarget = PathBuilder.fromPath(path);
            PathBuilder parentTarget = pathTarget.clone().parent();

            Collection parent = readCollectionAtPath(key, parentTarget);

            String partTarget = pathTarget.part();

            CollectionElement element = parent.findElementByName(partTarget);
            if (element == null) {
                throw new PathNotFoundException(path);
            }
            authorisation.checkPermission(element.getKey(), subjects, "read");

            return element.getKey();
        } catch (MembershipServiceException | AuthorisationServiceException | RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error while reading collection", e);
            throw new CoreServiceException(
                    "unable to resolve path " + path + " from collection with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void moveElements(String wskey, List<String> sources, String destination)
            throws PathAlreadyExistsException, PathNotFoundException, RegistryServiceException,
            InvalidPathException, AccessDeniedException, WorkspaceReadOnlyException, CoreServiceException,
            KeyNotFoundException {
        if (!sources.isEmpty()) {
            try {
                String ppath = PathBuilder.fromPath(sources.get(0)).parent().build();
                for (String source : sources) {
                    String sppath = PathBuilder.fromPath(source).parent().build();
                    if (!ppath.equals(sppath)) {
                        throw new InvalidPathException("unable to move elements from different collections");
                    }
                }
                String parentKey = resolveWorkspacePath(wskey, "head", ppath);
                OrtolangObjectIdentifier identifier = registry.lookup(parentKey);
                checkObjectType(identifier, Collection.OBJECT_TYPE);
                Collection collection = readCollection(parentKey);

                for (String source : sources) {
                    CollectionElement collectionElement = collection
                            .findElementByName(PathBuilder.fromPath(source).part());
                    switch (collectionElement.getType()) {
                    case DataObject.OBJECT_TYPE:
                        moveDataObject(wskey, source,
                                destination + PathBuilder.PATH_SEPARATOR + collectionElement.getName());
                        break;
                    case Collection.OBJECT_TYPE:
                        moveCollection(wskey, source,
                                destination + PathBuilder.PATH_SEPARATOR + collectionElement.getName());
                        break;
                    case Link.OBJECT_TYPE:
                        moveLink(wskey, source,
                                destination + PathBuilder.PATH_SEPARATOR + collectionElement.getName());
                    }
                }
            } catch (AccessDeniedException | CoreServiceException | RegistryServiceException
                    | WorkspaceReadOnlyException | InvalidPathException | PathNotFoundException
                    | KeyNotFoundException e) {
                ctx.setRollbackOnly();
                LOGGER.log(Level.SEVERE, "unexpected error while moving workspace elements", e);
                throw e;
            } catch (PathAlreadyExistsException e) {
                ctx.setRollbackOnly();
                throw e;
            }
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteElements(String wskey, List<String> sources, boolean force) throws InvalidPathException,
            CoreServiceException, PathNotFoundException, AccessDeniedException, KeyNotFoundException,
            WorkspaceReadOnlyException, CollectionNotEmptyException, RegistryServiceException {
        if (!sources.isEmpty()) {
            try {
                String ppath = PathBuilder.fromPath(sources.get(0)).parent().build();
                for (String source : sources) {
                    String sppath = PathBuilder.fromPath(source).parent().build();
                    if (!ppath.equals(sppath)) {
                        throw new InvalidPathException("unable to delete elements from different collections");
                    }
                }
                String parentKey = resolveWorkspacePath(wskey, "head", ppath);
                OrtolangObjectIdentifier identifier = registry.lookup(parentKey);
                checkObjectType(identifier, Collection.OBJECT_TYPE);
                Collection collection = readCollection(parentKey);

                for (String source : sources) {
                    CollectionElement collectionElement = collection
                            .findElementByName(PathBuilder.fromPath(source).part());
                    switch (collectionElement.getType()) {
                    case DataObject.OBJECT_TYPE:
                        deleteDataObject(wskey, source);
                        break;
                    case Collection.OBJECT_TYPE:
                        deleteCollection(wskey, source, force);
                    }
                }
            } catch (InvalidPathException | CoreServiceException | PathNotFoundException | AccessDeniedException
                    | KeyNotFoundException | WorkspaceReadOnlyException | RegistryServiceException e) {
                ctx.setRollbackOnly();
                LOGGER.log(Level.SEVERE, "unexpected error while moving workspace elements", e);
                throw e;
            } catch (CollectionNotEmptyException e) {
                ctx.setRollbackOnly();
                throw e;
            }
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Group addMember(String wskey, String member) throws CoreServiceException, AccessDeniedException,
            KeyNotFoundException, MembershipServiceException, NotificationServiceException {
        String caller = membership.getProfileKeyForConnectedIdentifier();
        Workspace workspace = readWorkspace(wskey);
        Group group = membership.addMemberInGroup(workspace.getMembers(), member);
        ArgumentsBuilder argsBuilder = new ArgumentsBuilder().addArgument("ws-alias", workspace.getAlias())
                .addArgument("member", member);
        notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE, OrtolangEvent.buildEventType(
                CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "notify-added-member"), argsBuilder.build());
        return group;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Collection moveCollection(String wskey, String source, String destination)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "moving collection into workspace [" + wskey + "] from path [" + source
                + "] to path [" + destination + "]");
        try {
            PathBuilder spath = PathBuilder.fromPath(source);
            if (spath.isRoot()) {
                throw new InvalidPathException("unable to move the root collection");
            }
            PathBuilder sppath = spath.clone().parent();

            PathBuilder dpath = PathBuilder.fromPath(destination);
            if (dpath.isRoot()) {
                throw new InvalidPathException("unable to move to the root collection");
            }
            PathBuilder dppath = dpath.clone().parent();

            if (dpath.equals(spath)) {
                throw new InvalidPathException("unable to move into the same path");
            }
            if (spath.isParent(dpath)) {
                throw new InvalidPathException("unable to move into a children of this path");
            }

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to move collection in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            Collection sparent = loadCollectionAtPath(ws.getHead(), sppath, ws.getClock());
            CollectionElement selement = sparent.findElementByName(spath.part());
            if (selement == null) {
                throw new PathNotFoundException(spath.build());
            }
            LOGGER.log(Level.FINEST,
                    "source collection element found for path " + spath.build() + ", key: " + selement.getKey());

            OrtolangObjectIdentifier scidentifier = registry.lookup(selement.getKey());
            checkObjectType(scidentifier, Collection.OBJECT_TYPE);
            Collection scollection = em.find(Collection.class, scidentifier.getId());
            if (scollection == null) {
                throw new CoreServiceException(
                        "unable to load source collection with id [" + scidentifier.getId() + "] from storage");
            }
            scollection.setKey(selement.getKey());
            LOGGER.log(Level.FINEST, "source collection exists and loaded from storage");

            Collection dparent = loadCollectionAtPath(ws.getHead(), dppath, ws.getClock());
            if (dparent.containsElementName(dpath.part())) {
                throw new PathAlreadyExistsException(dpath.build());
            }

            sparent.removeElement(selement);
            em.merge(sparent);
            registry.update(sparent.getKey());
            LOGGER.log(Level.FINEST, "parent [" + sparent.getKey() + "] has been updated");

            LOGGER.log(Level.FINEST, "destination element does not exists, ok for creating it");
            if (!dpath.part().equals(spath.part())) {
                if (scollection.getClock() < ws.getClock()) {
                    scollection = cloneCollection(ws.getHead(), scollection, ws.getClock());
                }
                scollection.setName(dpath.part());
                em.merge(scollection);
                registry.update(scollection.getKey());
            }
            dparent.addElement(new CollectionElement(Collection.OBJECT_TYPE, scollection.getName(),
                    System.currentTimeMillis(), 0, Collection.MIME_TYPE, scollection.getKey()));
            em.merge(dparent);
            registry.update(dparent.getKey());
            LOGGER.log(Level.FINEST, "collection [" + scollection.getKey() + "] added to destination parent ["
                    + dparent.getKey() + "]");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", scollection.getKey()).addArgument("okey", selement.getKey())
                    .addArgument("src-path", spath.build()).addArgument("dest-path", dpath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE, "move"),
                    argsBuilder.build());

            return scollection;
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | CloneException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while moving collection", e);
            throw new CoreServiceException("unable to move collection into workspace [" + wskey + "] from path ["
                    + source + "] to path [" + destination + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteCollection(String workspace, String path)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            CollectionNotEmptyException, PathNotFoundException, WorkspaceReadOnlyException {
        deleteCollection(workspace, path, false);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteCollection(String wskey, String path, boolean force)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            CollectionNotEmptyException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "deleting collection into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("unable to delete the root collection");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to delete collection in workspace with key [" + wskey
                        + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "delete");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'delete' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            CollectionElement element = parent.findElementByName(npath.part());
            if (element == null) {
                throw new PathNotFoundException(npath.build());
            }
            LOGGER.log(Level.FINEST,
                    "collection element found for path " + npath.build() + ", key: " + element.getKey());

            OrtolangObjectIdentifier cidentifier = registry.lookup(element.getKey());
            checkObjectType(cidentifier, Collection.OBJECT_TYPE);
            Collection leaf = em.find(Collection.class, cidentifier.getId());
            if (leaf == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + cidentifier.getId() + "] from storage");
            }
            leaf.setKey(element.getKey());
            LOGGER.log(Level.FINEST, "collection exists and loaded from storage");

            if (!leaf.isEmpty() && !force) {
                throw new CollectionNotEmptyException(path);
            }

            parent.removeElement(element);
            em.merge(parent);
            registry.update(parent.getKey());

            LOGGER.log(Level.FINEST, "parent [" + parent.getKey() + "] has been updated");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            if (leaf.getClock() == ws.getClock()) {
                LOGGER.log(Level.FINEST, "leaf clock [" + leaf.getClock()
                        + "] is the same than workspace, key can be deleted and unindexed");
                for (MetadataElement mde : leaf.getMetadatas()) {
                    registry.delete(mde.getKey());
                }
                registry.delete(leaf.getKey());
                indexing.remove(leaf.getKey());
            }
            deleteCollectionContent(leaf, ws.getClock());

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(3).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", leaf.getKey()).addArgument("path", npath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE, "delete"),
                    argsBuilder.build());
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while deleting collection", e);
            throw new CoreServiceException(
                    "unable to delete collection into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    /* Data Objects */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public DataObject createDataObject(String workspace, String path, String hash) throws CoreServiceException,
            KeyNotFoundException, InvalidPathException, AccessDeniedException, PathNotFoundException,
            PathAlreadyExistsException, WorkspaceReadOnlyException, KeyAlreadyExistsException {
        String key = UUID.randomUUID().toString();
        try {
            return createDataObject(workspace, key, path, hash);
        } catch (KeyAlreadyExistsException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.WARNING, "the generated key already exists : " + key);
            throw e;
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public DataObject createDataObject(String wskey, String key, String path, String hash)
            throws CoreServiceException, KeyAlreadyExistsException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE,
                "create data object with key [" + key + "] into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("forbidden to create an object at root level");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to create data object in workspace with key [" + wskey
                        + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "create");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'create' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());

            if (parent.containsElementName(npath.part())) {
                throw new PathAlreadyExistsException(npath.build());
            }

            DataObject object = new DataObject();
            object.setId(UUID.randomUUID().toString());
            object.setName(npath.part());
            if (hash != null && hash.length() > 0) {
                object.setSize(binarystore.size(hash));
                object.setMimeType(binarystore.type(hash, npath.part()));
                object.setStream(hash);
            } else {
                object.setSize(0);
                object.setMimeType("application/octet-stream");
                object.setStream("");
            }
            object.setClock(ws.getClock());
            object.setKey(key);
            em.persist(object);
            LOGGER.log(Level.FINEST, "object [" + key + "] created");

            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(key, object.getObjectIdentifier(), caller, properties);
            indexing.index(key);

            authorisation.clonePolicy(key, ws.getHead());
            LOGGER.log(Level.FINEST, "security policy cloned from head collection to key [" + key + "]");

            parent.addElement(new CollectionElement(DataObject.OBJECT_TYPE, object.getName(),
                    System.currentTimeMillis(), object.getSize(), object.getMimeType(), key));
            em.merge(parent);
            registry.update(parent.getKey());

            LOGGER.log(Level.FINEST, "object [" + key + "] added to parent [" + parent.getKey() + "]");

            try {
                extraction.extract(object.getKey());
            } catch (ExtractionServiceException e) {
                LOGGER.log(Level.SEVERE, e.getMessage(), e);
            }

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", key).addArgument("path", npath.build())
                    .addArgument("hash", object.getStream()).addArgument("mimetype", object.getMimeType());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE, "create"),
                    argsBuilder.build());

            return object;
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | IdentifierAlreadyRegisteredException | AuthorisationServiceException | MembershipServiceException
                | BinaryStoreServiceException | DataNotFoundException | IndexingServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during object creation", e);
            ctx.setRollbackOnly();
            throw new CoreServiceException(
                    "unable to create object into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public DataObject readDataObject(String key)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "reading object with key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(key);
            checkObjectType(identifier, DataObject.OBJECT_TYPE);
            authorisation.checkPermission(key, subjects, "read");

            DataObject object = em.find(DataObject.class, identifier.getId());
            if (object == null) {
                throw new CoreServiceException(
                        "unable to load object with id [" + identifier.getId() + "] from storage");
            }
            object.setKey(key);

            return object;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while reading object", e);
            throw new CoreServiceException("unable to read object with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public DataObject updateDataObject(String wskey, String path, String hash) throws CoreServiceException,
            InvalidPathException, AccessDeniedException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "updating object into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to update data object in workspace with key [" + wskey
                        + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            String current = resolveWorkspacePath(wskey, Workspace.HEAD, npath.build());
            OrtolangObjectIdentifier cidentifier = registry.lookup(current);
            checkObjectType(cidentifier, DataObject.OBJECT_TYPE);
            DataObject cobject = em.find(DataObject.class, cidentifier.getId());
            if (cobject == null) {
                throw new CoreServiceException(
                        "unable to load object with id [" + cidentifier.getId() + "] from storage");
            }
            cobject.setKey(current);
            LOGGER.log(Level.FINEST, "current object loaded");

            if (!hash.equals(cobject.getStream())) {
                Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
                LOGGER.log(Level.FINEST, "parent collection loaded for path " + npath.build());

                CollectionElement element = parent.findElementByName(npath.part());
                if (element == null) {
                    throw new PathNotFoundException(npath.build());
                }
                LOGGER.log(Level.FINEST, "object element found for name " + npath.part());
                if (!element.getType().equals(DataObject.OBJECT_TYPE)) {
                    throw new InvalidPathException("path [" + npath.build() + "] is not a data object");
                }

                OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                checkObjectType(identifier, DataObject.OBJECT_TYPE);
                DataObject object = em.find(DataObject.class, identifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + identifier.getId() + "] from storage");
                }
                object.setKey(element.getKey());
                if (object.getClock() < ws.getClock()) {
                    DataObject clone = cloneDataObject(ws.getHead(), object, ws.getClock());
                    object = clone;
                }
                if (hash.length() > 0) {
                    object.setSize(binarystore.size(hash));
                    object.setMimeType(binarystore.type(hash, object.getName()));
                    object.setStream(hash);
                } else {
                    object.setSize(0);
                    object.setMimeType("application/octet-stream");
                    object.setStream("");
                }
                parent.removeElement(element);
                CollectionElement celement = new CollectionElement(DataObject.OBJECT_TYPE, object.getName(),
                        System.currentTimeMillis(), object.getSize(), object.getMimeType(), object.getKey());
                parent.addElement(celement);
                em.merge(parent);
                em.merge(object);
                registry.update(parent.getKey());
                registry.update(object.getKey());
                indexing.index(object.getKey());
                LOGGER.log(Level.FINEST, "object updated");

                try {
                    extraction.extract(object.getKey());
                } catch (ExtractionServiceException e) {
                    LOGGER.log(Level.SEVERE, e.getMessage(), e);
                }

                ws.setChanged(true);
                em.merge(ws);
                registry.update(ws.getKey());
                LOGGER.log(Level.FINEST, "workspace set changed");

                ArgumentsBuilder argsBuilder = new ArgumentsBuilder(6).addArgument("ws-alias", ws.getAlias())
                        .addArgument("key", object.getKey()).addArgument("okey", element.getKey())
                        .addArgument("path", npath.build()).addArgument("hash", object.getStream())
                        .addArgument("mimetype", object.getMimeType());
                notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                        OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE, "update"),
                        argsBuilder.build());

                return object;
            } else {
                LOGGER.log(Level.FINEST, "no changes detected with current object, nothing to do");
                return cobject;
            }
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | AuthorisationServiceException | MembershipServiceException | BinaryStoreServiceException
                | DataNotFoundException | CloneException | IndexingServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while reading object", e);
            throw new CoreServiceException(
                    "unable to read object into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public DataObject moveDataObject(String wskey, String source, String destination)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "moving object into workspace [" + wskey + "] from path [" + source + "] to path ["
                + destination + "]");
        try {
            PathBuilder spath = PathBuilder.fromPath(source);
            if (spath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder sppath = spath.clone().parent();

            PathBuilder dpath = PathBuilder.fromPath(destination);
            if (dpath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder dppath = dpath.clone().parent();

            if (dpath.equals(spath)) {
                throw new InvalidPathException("unable to move into the same path");
            }
            if (spath.isParent(dpath)) {
                throw new InvalidPathException("unable to move into a children of this path");
            }

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to move data object in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            Collection sparent = loadCollectionAtPath(ws.getHead(), sppath, ws.getClock());
            CollectionElement selement = sparent.findElementByName(spath.part());
            if (selement == null) {
                throw new PathNotFoundException(spath.build());
            }
            LOGGER.log(Level.FINEST, "source object element found for name " + spath.part());

            OrtolangObjectIdentifier soidentifier = registry.lookup(selement.getKey());
            checkObjectType(soidentifier, DataObject.OBJECT_TYPE);
            DataObject sobject = em.find(DataObject.class, soidentifier.getId());
            if (sobject == null) {
                throw new CoreServiceException(
                        "unable to load source object with id [" + soidentifier.getId() + "] from storage");
            }
            sobject.setKey(selement.getKey());
            LOGGER.log(Level.FINEST, "source object exists and loaded from storage");

            Collection dparent = loadCollectionAtPath(ws.getHead(), dppath, ws.getClock());
            if (dparent.containsElementName(dpath.part())) {
                throw new PathAlreadyExistsException(dpath.build());
            }

            sparent.removeElement(selement);
            em.merge(sparent);
            registry.update(sparent.getKey());
            LOGGER.log(Level.FINEST, "parent [" + sparent.getKey() + "] has been updated");

            LOGGER.log(Level.FINEST, "destination element does not exists, creating it");
            if (!dpath.part().equals(spath.part())) {
                if (sobject.getClock() < ws.getClock()) {
                    sobject = cloneDataObject(ws.getHead(), sobject, ws.getClock());
                }
                sobject.setName(dpath.part());
                em.merge(sobject);
                registry.update(sobject.getKey());
            }
            dparent.addElement(new CollectionElement(DataObject.OBJECT_TYPE, sobject.getName(),
                    System.currentTimeMillis(), sobject.getSize(), sobject.getMimeType(), sobject.getKey()));
            em.merge(dparent);
            registry.update(dparent.getKey());

            LOGGER.log(Level.FINEST,
                    "object [" + sobject.getKey() + "] added to destination parent [" + dparent.getKey() + "]");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", sobject.getKey()).addArgument("okey", selement.getKey())
                    .addArgument("src-path", spath.build()).addArgument("dest-path", dpath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE, "move"),
                    argsBuilder.build());

            return sobject;
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | CloneException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while moving object", e);
            throw new CoreServiceException("unable to move object into workspace [" + wskey + "] from path ["
                    + source + "] to path [" + destination + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteDataObject(String wskey, String path) throws CoreServiceException, KeyNotFoundException,
            InvalidPathException, AccessDeniedException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "deleting object into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to delete data object in workspace with key [" + wskey
                        + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "delete");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'delete' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            CollectionElement element = parent.findElementByName(npath.part());
            if (element == null) {
                throw new PathNotFoundException(npath.build());
            }
            LOGGER.log(Level.FINEST,
                    "object element found for path " + npath.build() + ", key: " + element.getKey());

            OrtolangObjectIdentifier oidentifier = registry.lookup(element.getKey());
            checkObjectType(oidentifier, DataObject.OBJECT_TYPE);
            DataObject leaf = em.find(DataObject.class, oidentifier.getId());
            if (leaf == null) {
                throw new CoreServiceException(
                        "unable to load object with id [" + oidentifier.getId() + "] from storage");
            }
            leaf.setKey(element.getKey());
            LOGGER.log(Level.FINEST, "object exists and loaded from storage");

            parent.removeElement(element);
            em.merge(parent);
            registry.update(parent.getKey());

            LOGGER.log(Level.FINEST, "parent [" + parent.getKey() + "] has been updated");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            if (leaf.getClock() == ws.getClock()) {
                LOGGER.log(Level.FINEST, "leaf clock [" + leaf.getClock()
                        + "] is the same than workspace, key can be deleted and unindexed");
                for (MetadataElement mde : leaf.getMetadatas()) {
                    registry.delete(mde.getKey());
                }
                registry.delete(leaf.getKey());
                indexing.remove(leaf.getKey());
            }

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(3).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", leaf.getKey()).addArgument("path", npath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE, "delete"),
                    argsBuilder.build());
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while deleting object", e);
            throw new CoreServiceException(
                    "unable to delete object into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    /* Link */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Link createLink(String workspace, String path, String target) throws CoreServiceException,
            KeyNotFoundException, InvalidPathException, AccessDeniedException, PathNotFoundException,
            PathAlreadyExistsException, WorkspaceReadOnlyException, KeyAlreadyExistsException {
        String key = UUID.randomUUID().toString();
        try {
            return createLink(workspace, key, path, target);
        } catch (KeyAlreadyExistsException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.WARNING, "the generated key already exists : " + key);
            throw e;
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Link createLink(String wskey, String key, String path, String target)
            throws CoreServiceException, KeyAlreadyExistsException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE,
                "create link with key [" + key + "] into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            authorisation.checkPermission(target, subjects, "read");
            LOGGER.log(Level.FINEST, "user [" + caller + "] has 'read' permissions on the target");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to create link in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "create");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'create' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());

            if (parent.containsElementName(npath.part())) {
                throw new PathAlreadyExistsException(npath.build());
            }

            String ntarget = PathBuilder.fromPath(target).build();

            Link link = new Link();
            link.setId(UUID.randomUUID().toString());
            link.setKey(key);
            link.setName(npath.part());
            link.setClock(ws.getClock());
            link.setTarget(ntarget);
            em.persist(link);

            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(key, link.getObjectIdentifier(), caller, properties);
            indexing.index(key);

            authorisation.clonePolicy(key, ws.getHead());
            LOGGER.log(Level.FINEST, "security policy cloned from head collection to key [" + key + "]");

            parent.addElement(new CollectionElement(Link.OBJECT_TYPE, link.getName(), System.currentTimeMillis(), 0,
                    Link.MIME_TYPE, key));
            em.merge(parent);
            registry.update(parent.getKey());
            LOGGER.log(Level.FINEST, "link [" + key + "] added to parent [" + parent.getKey() + "]");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(3).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", key).addArgument("path", npath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Link.OBJECT_TYPE, "create"),
                    argsBuilder.build());

            return link;
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | IdentifierAlreadyRegisteredException | AuthorisationServiceException | MembershipServiceException
                | IndexingServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during link creation", e);
            ctx.setRollbackOnly();
            throw new CoreServiceException(
                    "unable to create link into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Link readLink(String key) throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "reading link with key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(key);
            checkObjectType(identifier, Link.OBJECT_TYPE);
            authorisation.checkPermission(key, subjects, "read");

            Link link = em.find(Link.class, identifier.getId());
            if (link == null) {
                throw new CoreServiceException(
                        "unable to load link with id [" + identifier.getId() + "] from storage");
            }
            link.setKey(key);

            return link;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while reading link", e);
            throw new CoreServiceException("unable to read link with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Link updateLink(String wskey, String path, String target) throws CoreServiceException,
            InvalidPathException, AccessDeniedException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "updating link into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to update link in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            String current = resolveWorkspacePath(wskey, Workspace.HEAD, npath.build());
            OrtolangObjectIdentifier cidentifier = registry.lookup(current);
            checkObjectType(cidentifier, Link.OBJECT_TYPE);
            Link clink = em.find(Link.class, cidentifier.getId());
            if (clink == null) {
                throw new CoreServiceException(
                        "unable to load link with id [" + cidentifier.getId() + "] from storage");
            }
            clink.setKey(current);
            LOGGER.log(Level.FINEST, "current link loaded");

            String ntarget = PathBuilder.fromPath(target).build();

            if (!ntarget.equals(clink.getTarget())) {
                Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
                LOGGER.log(Level.FINEST, "parent collection loaded for path " + npath.build());

                CollectionElement element = parent.findElementByName(npath.part());
                if (element == null) {
                    throw new PathNotFoundException(npath.build());
                }
                LOGGER.log(Level.FINEST, "link element found for name " + npath.part());
                if (!element.getType().equals(Link.OBJECT_TYPE)) {
                    throw new InvalidPathException("path [" + npath.build() + "] is not a link");
                }

                OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                checkObjectType(identifier, Link.OBJECT_TYPE);
                Link link = em.find(Link.class, identifier.getId());
                if (link == null) {
                    throw new CoreServiceException(
                            "unable to load link with id [" + identifier.getId() + "] from storage");
                }
                link.setKey(element.getKey());
                link.setTarget(ntarget);
                if (link.getClock() < ws.getClock()) {
                    Link clone = cloneLink(ws.getHead(), link, ws.getClock());
                    parent.removeElement(element);
                    CollectionElement celement = new CollectionElement(Link.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), 0, Link.MIME_TYPE, clone.getKey());
                    parent.addElement(celement);
                    link = clone;
                }
                em.merge(parent);
                em.merge(link);
                registry.update(parent.getKey());
                registry.update(link.getKey());
                indexing.index(link.getKey());
                LOGGER.log(Level.FINEST, "link updated");

                ws.setChanged(true);
                em.merge(ws);
                registry.update(ws.getKey());
                LOGGER.log(Level.FINEST, "workspace set changed");

                ArgumentsBuilder argsBuilder = new ArgumentsBuilder(4).addArgument("ws-alias", ws.getAlias())
                        .addArgument("key", link.getKey()).addArgument("okey", element.getKey())
                        .addArgument("path", npath.build());
                notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                        OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Link.OBJECT_TYPE, "update"),
                        argsBuilder.build());

                return link;
            } else {
                LOGGER.log(Level.FINEST, "no changes detected with current link, nothing to do");
                return clink;
            }
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | AuthorisationServiceException | MembershipServiceException | CloneException
                | IndexingServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while updating link", e);
            throw new CoreServiceException(
                    "unable to update link into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Link moveLink(String wskey, String source, String destination)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            PathNotFoundException, PathAlreadyExistsException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "moving link into workspace [" + wskey + "] from path [" + source + "] to path ["
                + destination + "]");
        try {
            PathBuilder spath = PathBuilder.fromPath(source);
            if (spath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder sppath = spath.clone().parent();

            PathBuilder dpath = PathBuilder.fromPath(destination);
            if (dpath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder dppath = dpath.clone().parent();

            if (dpath.equals(spath)) {
                throw new InvalidPathException("unable to move into the same path");
            }
            if (spath.isParent(dpath)) {
                throw new InvalidPathException("unable to move into a children of this path");
            }

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to move link in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            Collection sparent = loadCollectionAtPath(ws.getHead(), sppath, ws.getClock());
            CollectionElement selement = sparent.findElementByName(spath.part());
            if (selement == null) {
                throw new PathNotFoundException(spath.build());
            }
            LOGGER.log(Level.FINEST, "source link element found for name " + spath.part());

            OrtolangObjectIdentifier slidentifier = registry.lookup(selement.getKey());
            checkObjectType(slidentifier, Link.OBJECT_TYPE);
            Link slink = em.find(Link.class, slidentifier.getId());
            if (slink == null) {
                throw new CoreServiceException(
                        "unable to load source link with id [" + slidentifier.getId() + "] from storage");
            }
            slink.setKey(selement.getKey());
            LOGGER.log(Level.FINEST, "source link exists and loaded from storage");

            Collection dparent = loadCollectionAtPath(ws.getHead(), dppath, ws.getClock());
            if (dparent.containsElementName(dpath.part())) {
                throw new PathAlreadyExistsException(dpath.build());
            }

            sparent.removeElement(selement);
            em.merge(sparent);
            registry.update(sparent.getKey());
            indexing.index(sparent.getKey());
            LOGGER.log(Level.FINEST, "parent [" + sparent.getKey() + "] has been updated");

            if (!dpath.part().equals(spath.part())) {
                if (slink.getClock() < ws.getClock()) {
                    slink = cloneLink(ws.getHead(), slink, ws.getClock());
                }
                slink.setName(dpath.part());
                em.merge(slink);
                registry.update(slink.getKey());
                indexing.index(slink.getKey());
            }

            dparent.addElement(new CollectionElement(Link.OBJECT_TYPE, slink.getName(), System.currentTimeMillis(),
                    0, Link.MIME_TYPE, slink.getKey()));
            em.merge(dparent);
            registry.update(dparent.getKey());
            indexing.index(dparent.getKey());
            LOGGER.log(Level.FINEST,
                    "link [" + slink.getKey() + "] added to destination parent [" + dparent.getKey() + "]");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", slink.getKey()).addArgument("okey", selement.getKey())
                    .addArgument("src-path", spath.build()).addArgument("dest-path", dpath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Link.OBJECT_TYPE, "move"),
                    argsBuilder.build());

            return slink;
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | CloneException
                | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while moving link", e);
            throw new CoreServiceException("unable to move link into workspace [" + wskey + "] from path [" + source
                    + "] to path [" + destination + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteLink(String wskey, String path) throws CoreServiceException, KeyNotFoundException,
            InvalidPathException, AccessDeniedException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "deleting link into workspace [" + wskey + "] at path [" + path + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            if (npath.isRoot()) {
                throw new InvalidPathException("path is empty");
            }
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException(
                        "unable to delete link in workspace with key [" + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "delete");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'delete' permission on the head collection of this workspace");

            Collection parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
            CollectionElement element = parent.findElementByName(npath.part());
            if (element == null) {
                throw new PathNotFoundException(npath.build());
            }
            LOGGER.log(Level.FINEST, "link element found for name " + npath.part());

            OrtolangObjectIdentifier lidentifier = registry.lookup(element.getKey());
            checkObjectType(lidentifier, Link.OBJECT_TYPE);
            Link leaf = em.find(Link.class, lidentifier.getId());
            if (leaf == null) {
                throw new CoreServiceException(
                        "unable to load link with id [" + lidentifier.getId() + "] from storage");
            }
            leaf.setKey(element.getKey());
            LOGGER.log(Level.FINEST, "link exists and loaded from storage");

            parent.removeElement(element);
            em.merge(parent);
            registry.update(parent.getKey());
            indexing.index(parent.getKey());
            LOGGER.log(Level.FINEST, "parent [" + parent.getKey() + "] has been updated");

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            if (leaf.getClock() == ws.getClock()) {
                LOGGER.log(Level.FINEST, "leaf clock [" + leaf.getClock()
                        + "] is the same than workspace, key can be deleted and unindexed");
                for (MetadataElement mde : leaf.getMetadatas()) {
                    registry.delete(mde.getKey());
                }
                registry.delete(leaf.getKey());
                indexing.remove(leaf.getKey());
            }

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(3).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", leaf.getKey()).addArgument("path", npath.build());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Link.OBJECT_TYPE, "delete"),
                    argsBuilder.build());
        } catch (KeyLockedException | NotificationServiceException | RegistryServiceException
                | MembershipServiceException | AuthorisationServiceException | IndexingServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error while deleting link", e);
            throw new CoreServiceException(
                    "unable to delete link into workspace [" + wskey + "] at path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> findLinksForTarget(String target) throws CoreServiceException {
        LOGGER.log(Level.FINE, "finding links for target [" + target + "]");
        try {
            TypedQuery<Link> query = em.createNamedQuery("findLinksForTarget", Link.class).setParameter("target",
                    target);
            List<Link> links = query.getResultList();
            List<String> results = new ArrayList<String>();
            for (Link link : links) {
                String key = registry.lookup(link.getObjectIdentifier());
                results.add(key);
            }
            return results;
        } catch (RegistryServiceException | IdentifierNotRegisteredException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding links for target", e);
            throw new CoreServiceException("unable to find link for target [" + target + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String resolveLinkTarget(String target) throws CoreServiceException, InvalidPathException,
            PathNotFoundException, AccessDeniedException, AliasNotFoundException {
        LOGGER.log(Level.FINE, "resolving link target [" + target + "]");
        PathBuilder tpath = PathBuilder.fromPath(target);
        String[] tparts = tpath.buildParts();
        if (tparts.length < 2) {
            throw new CoreServiceException(
                    "unable to resolve target, path must contains at least an alias and a version");
        }
        String wskey = resolveWorkspaceAlias(tparts[0]);
        String root = tparts[1];
        String path = tpath.relativize(2).build();
        if (root.equals(Workspace.HEAD) || root.equals(Workspace.LATEST)) {
            throw new CoreServiceException("unable to resolve target due to " + Workspace.HEAD + " or "
                    + Workspace.LATEST + " version reference");
        }
        return resolveWorkspacePath(wskey, root, path);
    }

    /* Metadatas */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public MetadataObject createMetadataObject(String workspace, String path, String name, String hash,
            String filename, boolean purgeChildren)
            throws CoreServiceException, KeyNotFoundException, InvalidPathException, AccessDeniedException,
            MetadataFormatException, PathNotFoundException, WorkspaceReadOnlyException, KeyAlreadyExistsException {
        String key = UUID.randomUUID().toString();
        try {
            return createMetadataObject(workspace, key, path, name, hash, filename, purgeChildren);
        } catch (KeyAlreadyExistsException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.WARNING, "the generated key already exists : " + key);
            throw e;
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public MetadataObject createMetadataObject(String wskey, String key, String path, String name, String hash,
            String filename, boolean purgeChildren) throws CoreServiceException, KeyAlreadyExistsException,
            InvalidPathException, MetadataFormatException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "create metadataobject with key [" + key + "] into workspace [" + wskey
                + "] for path [" + path + "] with name [" + name + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to create metadata object in workspace with key ["
                        + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "create");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'create' permission on the head collection of this workspace");

            String tkey;
            Collection parent = null;
            CollectionElement element = null;
            if (npath.isRoot()) {
                tkey = ws.getHead();
            } else {
                parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
                LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());
                element = parent.findElementByName(npath.part());
                if (element == null) {
                    throw new PathNotFoundException(npath.build());
                }
                LOGGER.log(Level.FINEST, "collection element found for name " + npath.part());
                tkey = element.getKey();
            }

            OrtolangObjectIdentifier tidentifier = registry.lookup(tkey);
            if (!tidentifier.getType().equals(Link.OBJECT_TYPE)
                    && !tidentifier.getType().equals(Collection.OBJECT_TYPE)
                    && !tidentifier.getType().equals(DataObject.OBJECT_TYPE)) {
                throw new CoreServiceException("metadata target can only be a Link, a DataObject or a Collection.");
            }

            MetadataObject meta = new MetadataObject();
            meta.setId(UUID.randomUUID().toString());
            meta.setName(name);

            MetadataFormat format = getMetadataFormat(name);
            if (format == null) {
                LOGGER.log(Level.SEVERE, "Unable to find a metadata format for name: " + name);
                throw new CoreServiceException("unknown metadata format for name: " + name);
            }

            if (hash != null && hash.length() > 0) {
                if (format.isValidationNeeded()) {
                    validateMetadata(hash, format);
                }
                meta.setSize(binarystore.size(hash));
                if (filename != null) {
                    meta.setContentType(binarystore.type(hash, filename));
                } else {
                    meta.setContentType(binarystore.type(hash));
                }
                meta.setStream(hash);
            } else {
                meta.setSize(0);
                meta.setContentType("application/octet-stream");
                meta.setStream("");
            }

            meta.setFormat(format.getId());
            meta.setTarget(tkey);
            meta.setKey(key);
            em.persist(meta);

            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(key, meta.getObjectIdentifier(), caller, properties);

            authorisation.clonePolicy(key, ws.getHead());

            switch (tidentifier.getType()) {
            case Collection.OBJECT_TYPE:
                Collection collection = em.find(Collection.class, tidentifier.getId());
                if (collection == null) {
                    ctx.setRollbackOnly();
                    throw new CoreServiceException(
                            "unable to load collection with id [" + tidentifier.getId() + "] from storage");
                }
                collection.setKey(tkey);
                for (MetadataElement mde : collection.getMetadatas()) {
                    if (mde.getName().equals(name)) {
                        ctx.setRollbackOnly();
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] already exists for collection at path [" + npath.build() + "]");
                    }
                }
                if (collection.getClock() < ws.getClock()) {
                    Collection clone = cloneCollection(ws.getHead(), collection, ws.getClock());
                    tkey = clone.getKey();
                    meta.setTarget(tkey);
                    if (parent != null) {
                        parent.removeElement(element);
                        parent.addElement(new CollectionElement(Collection.OBJECT_TYPE, clone.getName(),
                                System.currentTimeMillis(), 0, Collection.MIME_TYPE, clone.getKey()));
                        em.merge(parent);
                        registry.update(parent.getKey());
                    }
                    collection = clone;
                }
                collection.addMetadata(new MetadataElement(name, key));
                em.merge(collection);
                if (purgeChildren) {
                    LOGGER.log(Level.FINE, "Purging children metadata");
                    purgeChildrenMetadata(collection, wskey, path, name);
                }
                break;
            case DataObject.OBJECT_TYPE:
                DataObject object = em.find(DataObject.class, tidentifier.getId());
                if (object == null) {
                    ctx.setRollbackOnly();
                    throw new CoreServiceException(
                            "unable to load object with id [" + tidentifier.getId() + "] from storage");
                }
                object.setKey(tkey);
                for (MetadataElement mde : object.getMetadatas()) {
                    if (mde.getName().equals(name)) {
                        ctx.setRollbackOnly();
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] already exists for object at path [" + npath.build() + "]");
                    }
                }
                if (object.getClock() < ws.getClock()) {
                    DataObject clone = cloneDataObject(ws.getHead(), object, ws.getClock());
                    tkey = clone.getKey();
                    meta.setTarget(tkey);
                    if (parent == null) {
                        throw new CoreServiceException("An object should have a parent");
                    }
                    parent.removeElement(element);
                    parent.addElement(new CollectionElement(DataObject.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), clone.getSize(), clone.getMimeType(), clone.getKey()));
                    em.merge(parent);
                    registry.update(parent.getKey());
                    object = clone;
                }
                object.addMetadata(new MetadataElement(name, key));
                em.merge(object);
                break;
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, tidentifier.getId());
                if (link == null) {
                    ctx.setRollbackOnly();
                    throw new CoreServiceException(
                            "unable to load link with id [" + tidentifier.getId() + "] from storage");
                }
                link.setKey(tkey);
                for (MetadataElement mde : link.getMetadatas()) {
                    if (mde.getName().equals(name)) {
                        ctx.setRollbackOnly();
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] already exists for link at path [" + npath.build() + "]");
                    }
                }
                if (link.getClock() < ws.getClock()) {
                    Link clone = cloneLink(ws.getHead(), link, ws.getClock());
                    tkey = clone.getKey();
                    meta.setTarget(tkey);
                    if (parent == null) {
                        throw new CoreServiceException("A link should have a parent");
                    }
                    parent.removeElement(element);
                    parent.addElement(new CollectionElement(Link.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), 0, Link.MIME_TYPE, clone.getKey()));
                    em.merge(parent);
                    registry.update(parent.getKey());
                    link = clone;
                }
                link.addMetadata(new MetadataElement(name, key));
                em.merge(link);
                break;
            }
            em.merge(meta);

            indexing.index(tkey);

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", key).addArgument("tkey", tkey).addArgument("path", npath.build())
                    .addArgument("name", name);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, MetadataObject.OBJECT_TYPE, "create"),
                    argsBuilder.build());

            return meta;
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | IdentifierAlreadyRegisteredException | AuthorisationServiceException | MembershipServiceException
                | BinaryStoreServiceException | DataNotFoundException | CloneException | IndexingServiceException
                | OrtolangException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred during metadata creation", e);
            throw new CoreServiceException(
                    "unable to create metadata into workspace [" + wskey + "] for path [" + path + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public MetadataObject readMetadataObject(String key)
            throws CoreServiceException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "reading metadata for key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkPermission(key, subjects, "read");

            OrtolangObjectIdentifier identifier = registry.lookup(key);
            checkObjectType(identifier, MetadataObject.OBJECT_TYPE);

            MetadataObject meta = em.find(MetadataObject.class, identifier.getId());
            if (meta == null) {
                throw new CoreServiceException(
                        "unable to load metadata with id [" + identifier.getId() + "] from storage");
            }
            meta.setKey(key);

            return meta;
        } catch (RegistryServiceException | AuthorisationServiceException | MembershipServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during reading metadata", e);
            throw new CoreServiceException("unable to read metadata with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public MetadataObject updateMetadataObject(String wskey, String path, String name, String hash, String filename,
            boolean purgeChildren) throws CoreServiceException, KeyNotFoundException, InvalidPathException,
            AccessDeniedException, MetadataFormatException, PathNotFoundException, WorkspaceReadOnlyException {
        return updateMetadataObject(wskey, path, name, hash, filename, purgeChildren, null);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public MetadataObject updateMetadataObject(String wskey, String path, String name, String hash, String filename,
            boolean purgeChildren, String format) throws CoreServiceException, InvalidPathException,
            MetadataFormatException, PathNotFoundException, WorkspaceReadOnlyException {
        LOGGER.log(Level.FINE, "updating metadata content into workspace [" + wskey + "] for path [" + path
                + "] and name [" + name + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to update metadata object in workspace with key ["
                        + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "update");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'update' permission on the head collection of this workspace");

            String current = resolveWorkspacePath(wskey, Workspace.HEAD, npath.build());
            OrtolangObjectIdentifier ctidentifier = registry.lookup(current);
            if (!ctidentifier.getType().equals(Link.OBJECT_TYPE)
                    && !ctidentifier.getType().equals(Collection.OBJECT_TYPE)
                    && !ctidentifier.getType().equals(DataObject.OBJECT_TYPE)) {
                throw new CoreServiceException("metadata target can only be a Link, a DataObject or a Collection.");
            }
            MetadataElement cmdelement = null;
            switch (ctidentifier.getType()) {
            case Collection.OBJECT_TYPE:
                Collection collection = em.find(Collection.class, ctidentifier.getId());
                if (collection == null) {
                    throw new CoreServiceException(
                            "unable to load collection with id [" + ctidentifier.getId() + "] from storage");
                }
                if (collection.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for collection at path [" + npath.build() + "]");
                }
                cmdelement = collection.findMetadataByName(name);
                break;
            case DataObject.OBJECT_TYPE:
                DataObject object = em.find(DataObject.class, ctidentifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + ctidentifier.getId() + "] from storage");
                }
                if (object.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for object at path [" + npath.build() + "]");
                }
                cmdelement = object.findMetadataByName(name);
                break;
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, ctidentifier.getId());
                if (link == null) {
                    throw new CoreServiceException(
                            "unable to load link with id [" + ctidentifier.getId() + "] from storage");
                }
                if (link.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for link at path [" + npath.build() + "]");
                }
                cmdelement = link.findMetadataByName(name);
                break;
            }
            if (cmdelement == null) {
                throw new CoreServiceException("unable to find current metadata target into workspace [" + wskey
                        + "] for path [" + npath.build() + "] and name [" + name + "]");
            }
            OrtolangObjectIdentifier cidentifier = registry.lookup(cmdelement.getKey());
            checkObjectType(cidentifier, MetadataObject.OBJECT_TYPE);
            MetadataObject cmeta = em.find(MetadataObject.class, cidentifier.getId());
            if (cmeta == null) {
                throw new CoreServiceException(
                        "unable to load metadata with id [" + cidentifier.getId() + "] from storage");
            }
            if (!cmeta.getStream().equals(hash)) {
                String tkey = ws.getHead();
                Collection parent = null;
                CollectionElement element = null;
                if (!npath.isRoot()) {
                    parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
                    LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());
                    element = parent.findElementByName(npath.part());
                    if (element == null) {
                        throw new PathNotFoundException(npath.build());
                    }
                    LOGGER.log(Level.FINEST, "collection element found for name " + npath.part());
                    tkey = element.getKey();
                }

                OrtolangObjectIdentifier tidentifier = registry.lookup(tkey);
                if (!tidentifier.getType().equals(Link.OBJECT_TYPE)
                        && !tidentifier.getType().equals(Collection.OBJECT_TYPE)
                        && !tidentifier.getType().equals(DataObject.OBJECT_TYPE)) {
                    throw new CoreServiceException(
                            "metadata target can only be a Link, a DataObject or a Collection.");
                }

                MetadataElement mdelement = null;
                Collection collection = null;
                switch (tidentifier.getType()) {
                case Collection.OBJECT_TYPE:
                    collection = em.find(Collection.class, tidentifier.getId());
                    if (collection == null) {
                        throw new CoreServiceException(
                                "unable to load collection with id [" + tidentifier.getId() + "] from storage");
                    }
                    collection.setKey(tkey);
                    if (collection.findMetadataByName(name) == null) {
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] does not exists for collection at path [" + npath.build() + "]");
                    }
                    if (collection.getClock() < ws.getClock()) {
                        Collection clone = cloneCollection(ws.getHead(), collection, ws.getClock());
                        tkey = clone.getKey();
                        if (parent != null) {
                            parent.removeElement(element);
                            parent.addElement(new CollectionElement(Collection.OBJECT_TYPE, clone.getName(),
                                    System.currentTimeMillis(), 0, Collection.MIME_TYPE, clone.getKey()));
                        }
                        collection = clone;
                    }
                    mdelement = collection.findMetadataByName(name);
                    break;
                case DataObject.OBJECT_TYPE:
                    DataObject object = em.find(DataObject.class, tidentifier.getId());
                    if (object == null) {
                        throw new CoreServiceException(
                                "unable to load object with id [" + tidentifier.getId() + "] from storage");
                    }
                    object.setKey(tkey);
                    if (object.findMetadataByName(name) == null) {
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] does not exists for object at path [" + npath.build() + "]");
                    }
                    if (object.getClock() < ws.getClock()) {
                        DataObject clone = cloneDataObject(ws.getHead(), object, ws.getClock());
                        tkey = clone.getKey();
                        if (parent == null) {
                            throw new CoreServiceException("An object should have a parent");
                        }
                        parent.removeElement(element);
                        parent.addElement(new CollectionElement(DataObject.OBJECT_TYPE, clone.getName(),
                                System.currentTimeMillis(), clone.getSize(), clone.getMimeType(), clone.getKey()));
                        object = clone;
                    }
                    mdelement = object.findMetadataByName(name);
                    break;
                case Link.OBJECT_TYPE:
                    Link link = em.find(Link.class, tidentifier.getId());
                    if (link == null) {
                        throw new CoreServiceException(
                                "unable to load link with id [" + tidentifier.getId() + "] from storage");
                    }
                    link.setKey(tkey);
                    if (link.findMetadataByName(name) == null) {
                        throw new CoreServiceException("a metadata object with name [" + name
                                + "] does not exists for link at path [" + npath.build() + "]");
                    }
                    if (link.getClock() < ws.getClock()) {
                        Link clone = cloneLink(ws.getHead(), link, ws.getClock());
                        tkey = clone.getKey();
                        if (parent == null) {
                            throw new CoreServiceException("A link should have a parent");
                        }
                        parent.removeElement(element);
                        parent.addElement(new CollectionElement(Link.OBJECT_TYPE, clone.getName(),
                                System.currentTimeMillis(), 0, Link.MIME_TYPE, clone.getKey()));
                        link = clone;
                    }
                    mdelement = link.findMetadataByName(name);
                    break;
                }

                if (mdelement == null) {
                    throw new CoreServiceException("unable to find metadata object into workspace [" + wskey
                            + "] for path [" + npath.build() + "] and name [" + name + "]");
                }
                OrtolangObjectIdentifier identifier = registry.lookup(mdelement.getKey());
                checkObjectType(identifier, MetadataObject.OBJECT_TYPE);
                MetadataObject meta = em.find(MetadataObject.class, identifier.getId());
                if (meta == null) {
                    throw new CoreServiceException(
                            "unable to load metadata with id [" + identifier.getId() + "] from storage");
                }
                meta.setKey(mdelement.getKey());

                if (hash != null && hash.length() > 0) {
                    if (filename != null) {
                        meta.setContentType(binarystore.type(hash, filename));
                    } else {
                        meta.setContentType(binarystore.type(hash));
                    }

                    if (format != null) {
                        meta.setFormat(format);
                    }
                    MetadataFormat metadataFormat = findMetadataFormatById(meta.getFormat());
                    if (metadataFormat == null) {
                        LOGGER.log(Level.SEVERE, "Unable to find a metadata format for name: " + name);
                        throw new CoreServiceException("unknown metadata format for name: " + name);
                    }
                    if (metadataFormat.isValidationNeeded()) {
                        validateMetadata(hash, metadataFormat);
                    }
                    meta.setSize(binarystore.size(hash));
                    meta.setStream(hash);
                    meta.setTarget(tkey);
                } else {
                    throw new CoreServiceException(
                            "unable to update a metadata with an empty content (hash is null)");
                }
                em.merge(meta);

                registry.update(mdelement.getKey());
                indexing.index(tkey);

                if (collection != null && purgeChildren) {
                    LOGGER.log(Level.FINE, "Purging children metadata");
                    purgeChildrenMetadata(collection, wskey, path, name);
                }

                ws.setChanged(true);
                em.merge(ws);
                registry.update(ws.getKey());
                LOGGER.log(Level.FINEST, "workspace set changed");

                ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                        .addArgument("key", mdelement.getKey()).addArgument("tkey", tkey)
                        .addArgument("path", npath.build()).addArgument("name", name);
                notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE, OrtolangEvent.buildEventType(
                        CoreService.SERVICE_NAME, MetadataObject.OBJECT_TYPE, "update"), argsBuilder.build());

                return meta;
            } else {
                LOGGER.log(Level.FINEST, "no changes detected with current metadata object, nothing to do");
                return cmeta;
            }
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | AuthorisationServiceException | MembershipServiceException | BinaryStoreServiceException
                | DataNotFoundException | CloneException | IndexingServiceException | OrtolangException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred during metadata creation", e);
            throw new CoreServiceException("unable to create metadata into workspace [" + wskey + "] for path ["
                    + path + "] and name [" + name + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteMetadataObject(String wskey, String path, String name, boolean recursive)
            throws CoreServiceException, InvalidPathException, WorkspaceReadOnlyException, PathNotFoundException {
        LOGGER.log(Level.FINE, "deleting metadataobject into workspace [" + wskey + "] for path [" + path
                + "] with name [" + name + "]");
        try {
            PathBuilder npath = PathBuilder.fromPath(path);
            PathBuilder ppath = npath.clone().parent();

            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkAuthentified(subjects);
            LOGGER.log(Level.FINEST, "user [" + caller + "] is authentified");

            OrtolangObjectIdentifier wsidentifier = registry.lookup(wskey);
            checkObjectType(wsidentifier, Workspace.OBJECT_TYPE);
            LOGGER.log(Level.FINEST, "workspace with key [" + wskey + "] exists");

            Workspace ws = em.find(Workspace.class, wsidentifier.getId());
            if (ws == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + wsidentifier.getId() + "] from storage");
            }
            if (applyReadOnly(caller, subjects, ws)) {
                throw new WorkspaceReadOnlyException("unable to delete meta data object in workspace with key ["
                        + wskey + "] because it is read only");
            }
            ws.setKey(wskey);
            LOGGER.log(Level.FINEST, "workspace loaded");

            authorisation.checkPermission(ws.getHead(), subjects, "delete");
            LOGGER.log(Level.FINEST,
                    "user [" + caller + "] has 'create' permission on the head collection of this workspace");

            String current = resolveWorkspacePath(wskey, Workspace.HEAD, npath.build());
            MetadataElement cmdelement = loadMetadataElement(name, current);
            if (cmdelement == null) {
                throw new CoreServiceException("a metadata object with name [" + name
                        + "] does not exists for at path [" + npath.build() + "]");
            }
            OrtolangObjectIdentifier cidentifier = registry.lookup(cmdelement.getKey());
            checkObjectType(cidentifier, MetadataObject.OBJECT_TYPE);
            MetadataObject cmeta = em.find(MetadataObject.class, cidentifier.getId());
            if (cmeta == null) {
                throw new CoreServiceException(
                        "unable to load metadata with id [" + cidentifier.getId() + "] from storage");
            }

            Collection parent = null;
            CollectionElement element = null;
            String tkey;
            if (npath.isRoot()) {
                tkey = ws.getHead();
            } else {
                parent = loadCollectionAtPath(ws.getHead(), ppath, ws.getClock());
                LOGGER.log(Level.FINEST, "parent collection loaded for path " + ppath.build());
                element = parent.findElementByName(npath.part());
                if (element == null) {
                    throw new PathNotFoundException(npath.build());
                }
                LOGGER.log(Level.FINEST, "collection element found for name " + npath.part());
                tkey = element.getKey();
            }

            OrtolangObjectIdentifier tidentifier = registry.lookup(tkey);
            if (!tidentifier.getType().equals(Link.OBJECT_TYPE)
                    && !tidentifier.getType().equals(Collection.OBJECT_TYPE)
                    && !tidentifier.getType().equals(DataObject.OBJECT_TYPE)) {
                throw new CoreServiceException("metadata target can only be a Link, a DataObject or a Collection.");
            }

            MetadataElement mdelement;
            switch (tidentifier.getType()) {
            case Collection.OBJECT_TYPE:
                Collection collection = em.find(Collection.class, tidentifier.getId());
                if (collection == null) {
                    throw new CoreServiceException(
                            "unable to load collection with id [" + tidentifier.getId() + "] from storage");
                }
                collection.setKey(tkey);
                if (collection.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for collection at path [" + npath.build() + "]");
                }
                if (collection.getClock() < ws.getClock()) {
                    Collection clone = cloneCollection(ws.getHead(), collection, ws.getClock());
                    tkey = clone.getKey();
                    if (parent != null) {
                        parent.removeElement(element);
                        parent.addElement(new CollectionElement(Collection.OBJECT_TYPE, clone.getName(),
                                System.currentTimeMillis(), 0, Collection.MIME_TYPE, clone.getKey()));
                    }
                    collection = clone;
                }
                mdelement = collection.findMetadataByName(name);
                if (recursive) {
                    LOGGER.log(Level.FINE, "Removing children metadata");
                    purgeChildrenMetadata(collection, wskey, path, name);
                }
                collection.removeMetadata(mdelement);
                em.merge(collection);
                break;
            case DataObject.OBJECT_TYPE:
                DataObject object = em.find(DataObject.class, tidentifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + tidentifier.getId() + "] from storage");
                }
                object.setKey(tkey);
                if (object.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for object at path [" + npath.build() + "]");
                }
                if (object.getClock() < ws.getClock()) {
                    DataObject clone = cloneDataObject(ws.getHead(), object, ws.getClock());
                    tkey = clone.getKey();
                    if (parent == null) {
                        throw new CoreServiceException("An object should have a parent");
                    }
                    parent.removeElement(element);
                    parent.addElement(new CollectionElement(DataObject.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), clone.getSize(), clone.getMimeType(), clone.getKey()));
                    object = clone;
                }
                mdelement = object.findMetadataByName(name);
                object.removeMetadata(mdelement);
                em.merge(object);
                break;
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, tidentifier.getId());
                if (link == null) {
                    throw new CoreServiceException(
                            "unable to load link with id [" + tidentifier.getId() + "] from storage");
                }
                link.setKey(tkey);
                if (link.findMetadataByName(name) == null) {
                    throw new CoreServiceException("a metadata object with name [" + name
                            + "] does not exists for link at path [" + npath.build() + "]");
                }
                if (link.getClock() < ws.getClock()) {
                    Link clone = cloneLink(ws.getHead(), link, ws.getClock());
                    tkey = clone.getKey();
                    if (parent == null) {
                        throw new CoreServiceException("A link should have a parent");
                    }
                    parent.removeElement(element);
                    parent.addElement(new CollectionElement(Link.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), 0, Link.MIME_TYPE, clone.getKey()));
                    link = clone;
                }
                mdelement = link.findMetadataByName(name);
                link.removeMetadata(mdelement);
                em.merge(link);
                break;
            default:
                throw new CoreServiceException(
                        "Metadata target should be a Metadata Source not a " + tidentifier.getType());
            }

            if (mdelement == null) {
                throw new CoreServiceException("unable to find metadata object into workspace [" + wskey
                        + "] for path [" + npath.build() + "] and name [" + name + "]");
            }

            registry.delete(mdelement.getKey());
            indexing.remove(mdelement.getKey());

            indexing.index(tkey);

            ws.setChanged(true);
            em.merge(ws);
            registry.update(ws.getKey());
            LOGGER.log(Level.FINEST, "workspace set changed");

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(5).addArgument("ws-alias", ws.getAlias())
                    .addArgument("key", mdelement.getKey()).addArgument("tkey", tkey)
                    .addArgument("path", npath.build()).addArgument("name", name);
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, MetadataObject.OBJECT_TYPE, "delete"),
                    argsBuilder.build());
        } catch (KeyLockedException | KeyNotFoundException | RegistryServiceException | NotificationServiceException
                | AuthorisationServiceException | MembershipServiceException | CloneException
                | IndexingServiceException | OrtolangException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred during metadata deletion", e);
            throw new CoreServiceException(
                    "unable to delete metadata into workspace [" + wskey + "] for path [" + path + "]", e);
        }
    }

    private MetadataElement loadMetadataElement(String name, String target)
            throws CoreServiceException, KeyNotFoundException, RegistryServiceException {
        OrtolangObjectIdentifier targetIdentifier = registry.lookup(target);
        switch (targetIdentifier.getType()) {
        case Collection.OBJECT_TYPE:
            Collection collection = em.find(Collection.class, targetIdentifier.getId());
            if (collection == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + targetIdentifier.getId() + "] from storage");
            }
            return collection.findMetadataByName(name);
        case DataObject.OBJECT_TYPE:
            DataObject object = em.find(DataObject.class, targetIdentifier.getId());
            if (object == null) {
                throw new CoreServiceException(
                        "unable to load object with id [" + targetIdentifier.getId() + "] from storage");
            }
            return object.findMetadataByName(name);
        case Link.OBJECT_TYPE:
            Link link = em.find(Link.class, targetIdentifier.getId());
            if (link == null) {
                throw new CoreServiceException(
                        "unable to load link with id [" + targetIdentifier.getId() + "] from storage");
            }
            return link.findMetadataByName(name);
        default:
            throw new CoreServiceException(
                    "Metadata target should be a Metadata Source not a " + targetIdentifier.getType());
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public String readPublicationPolicy(String key) throws KeyNotFoundException, RegistryServiceException,
            DataNotFoundException, BinaryStoreServiceException, IOException, CoreServiceException {
        OrtolangObjectIdentifier identifier = registry.lookup(key);
        OrtolangObject object;
        switch (identifier.getType()) {
        case Collection.OBJECT_TYPE:
            object = em.find(Collection.class, identifier.getId());
            break;
        case DataObject.OBJECT_TYPE:
            object = em.find(DataObject.class, identifier.getId());
            break;
        case Link.OBJECT_TYPE:
            object = em.find(Link.class, identifier.getId());
            break;
        default:
            throw new CoreServiceException(
                    "Cannot read publication policy of an object of type: " + identifier.getType());
        }
        MetadataElement metadataElement = null;
        if (object != null) {
            metadataElement = ((MetadataSource) object).findMetadataByName(MetadataFormat.ACL);
        }
        if (metadataElement != null) {
            OrtolangObjectIdentifier mdIdentifier = registry.lookup(metadataElement.getKey());
            MetadataObject metadataObject = em.find(MetadataObject.class, mdIdentifier.getId());
            ObjectMapper mapper = new ObjectMapper();
            PublicationPolicy publicationPolicy = mapper.readValue(binarystore.getFile(metadataObject.getStream()),
                    PublicationPolicy.class);
            return publicationPolicy.getTemplate();
        }
        return null;
    }

    @Override
    public String readPublicationPolicy(String wskey, String root, String path) throws KeyNotFoundException,
            RegistryServiceException, DataNotFoundException, BinaryStoreServiceException, IOException,
            CoreServiceException, AccessDeniedException, InvalidPathException, PathNotFoundException {
        String key = resolveWorkspacePath(wskey, root, path);
        String publicationPolicy = readPublicationPolicy(key);
        if (publicationPolicy == null) {
            PathBuilder pathBuilder = PathBuilder.fromPath(path);
            if (pathBuilder.isRoot()) {
                return AuthorisationPolicyTemplate.FORALL;
            }
            return readPublicationPolicy(wskey, root, pathBuilder.parent().build());
        }
        return publicationPolicy;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> findMetadataObjectsForTarget(String target)
            throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "finding metadata for target [" + target + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkPermission(target, subjects, "read");

            TypedQuery<MetadataObject> query = em
                    .createNamedQuery("findMetadataObjectsForTarget", MetadataObject.class)
                    .setParameter("target", target);
            List<MetadataObject> mdos = query.getResultList();
            List<String> results = new ArrayList<String>();
            for (MetadataObject mdo : mdos) {
                String key = registry.lookup(mdo.getObjectIdentifier());
                results.add(key);
            }
            return results;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | IdentifierNotRegisteredException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding metadata", e);
            throw new CoreServiceException("unable to find metadata for target [" + target + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<String> findMetadataObjectsForTargetAndName(String target, String name)
            throws CoreServiceException, AccessDeniedException {
        LOGGER.log(Level.FINE, "finding metadata for target [" + target + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            authorisation.checkPermission(target, subjects, "read");

            TypedQuery<MetadataObject> query = em
                    .createNamedQuery("findMetadataObjectsForTargetAndName", MetadataObject.class)
                    .setParameter("target", target).setParameter("name", name);
            List<MetadataObject> mdos = query.getResultList();
            List<String> results = new ArrayList<String>();
            for (MetadataObject mdo : mdos) {
                String key = registry.lookup(mdo.getObjectIdentifier());
                results.add(key);
            }
            return results;
        } catch (RegistryServiceException | MembershipServiceException | AuthorisationServiceException
                | KeyNotFoundException | IdentifierNotRegisteredException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during finding metadata", e);
            throw new CoreServiceException("unable to find metadata for target [" + target + "]", e);
        }
    }

    /* MetadataFormat */

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public String createMetadataFormat(String name, String description, String schema, String form,
            boolean validationNeeded, boolean indexable) throws CoreServiceException {
        LOGGER.log(Level.FINE, "creating metadataformat with name [" + name + "]");
        try {
            MetadataFormat newmdf = new MetadataFormat();
            newmdf.setName(name);
            newmdf.setDescription(description);
            newmdf.setForm(form);
            newmdf.setValidationNeeded(validationNeeded);
            newmdf.setIndexable(indexable);
            if (schema != null && schema.length() > 0) {
                newmdf.setSize(binarystore.size(schema));
                newmdf.setMimeType(binarystore.type(schema));
                newmdf.setSchema(schema);
            } else {
                newmdf.setSize(0);
                newmdf.setMimeType("application/octet-stream");
                newmdf.setSchema("");
            }
            MetadataFormat mdf = getMetadataFormat(name);
            if (mdf != null) {
                if (mdf.equals(newmdf)) {
                    LOGGER.log(Level.INFO, "Already imported metadata format: " + mdf.getId());
                    return mdf.getId();
                }
                LOGGER.log(Level.FINE, "metadata format version already exists, creating new version");
                newmdf.setSerial(mdf.getSerial() + 1);
            }
            newmdf.setId(name + ":" + newmdf.getSerial());
            em.persist(newmdf);
            return newmdf.getId();
        } catch (BinaryStoreServiceException | DataNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error during create metadata format", e);
            throw new CoreServiceException("unexpected error during create metadata format", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<MetadataFormat> listMetadataFormat() throws CoreServiceException {
        List<MetadataFormat> formats = listAllMetadataFormat();
        Map<String, MetadataFormat> latest = new HashMap<String, MetadataFormat>();
        for (MetadataFormat format : formats) {
            if (!latest.containsKey(format.getName())) {
                latest.put(format.getName(), format);
            } else if (latest.get(format.getName()).getSerial() < format.getSerial()) {
                latest.put(format.getName(), format);
            }
        }
        List<MetadataFormat> filteredformats = new ArrayList<MetadataFormat>();
        filteredformats.addAll(latest.values());
        return filteredformats;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<MetadataFormat> listAllMetadataFormat() {
        TypedQuery<MetadataFormat> query = em.createNamedQuery("listMetadataFormat", MetadataFormat.class);
        return query.getResultList();
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public MetadataFormat getMetadataFormat(String name) {
        LOGGER.log(Level.FINE, "reading metadata format for name [" + name + "]");
        TypedQuery<MetadataFormat> query = em.createNamedQuery("findMetadataFormatForName", MetadataFormat.class)
                .setParameter("name", name);
        List<MetadataFormat> formats = query.getResultList();
        if (formats.isEmpty()) {
            return null;
        }
        return formats.get(0);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public MetadataFormat findMetadataFormatById(String id) throws CoreServiceException {
        MetadataFormat format = em.find(MetadataFormat.class, id);
        if (format == null) {
            throw new CoreServiceException("unable to find a metadata format for id " + id + " in the storage");
        }
        return format;
    }

    private void validateMetadata(String metadata, MetadataFormat format) throws MetadataFormatException {
        try {
            if (format.getSchema() != null && format.getSchema().length() > 0) {
                JsonNode jsonSchema = JsonLoader
                        .fromReader(new InputStreamReader(binarystore.get(format.getSchema())));
                JsonNode jsonFile = JsonLoader.fromReader(new InputStreamReader(binarystore.get(metadata)));
                JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
                JsonSchema schema = factory.getJsonSchema(jsonSchema);

                ProcessingReport report = schema.validate(jsonFile);
                LOGGER.log(Level.FINE, report.toString());

                if (!report.isSuccess()) {
                    LOGGER.log(Level.WARNING, "error during validating metadata format " + format.getName() + ": "
                            + report.toString());
                    throw new MetadataFormatException(report.toString());
                }
            } else {
                LOGGER.log(Level.SEVERE,
                        "unexpected error occurred during validating metadata with metadata format [" + format
                                + "] : schema not found");
                throw new MetadataFormatException(
                        "unable to validate metadata with metadata format [" + format + "] : schema not found");
            }
        } catch (IOException | ProcessingException | DataNotFoundException | BinaryStoreServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during validating metadata with metadata format ["
                    + format + "] : " + e.getMessage());
            throw new MetadataFormatException(
                    "unable to validate metadata with metadata format [" + format + "] : " + e.getMessage());
        }
    }

    /* BinaryContent */

    @Override
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public String put(InputStream data) throws CoreServiceException, DataCollisionException {
        LOGGER.log(Level.FINE, "putting binary content in store");
        try {
            return binarystore.put(data);
        } catch (BinaryStoreServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during putting binary content", e);
            throw new CoreServiceException("unable to put binary content", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public InputStream download(String key)
            throws CoreServiceException, DataNotFoundException, KeyNotFoundException, AccessDeniedException {
        LOGGER.log(Level.FINE, "download content from store for object with key [" + key + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            List<String> subjects = membership.getConnectedIdentifierSubjects();

            OrtolangObjectIdentifier identifier = registry.lookup(key);
            String hash;
            switch (identifier.getType()) {
            case DataObject.OBJECT_TYPE: {
                authorisation.checkPermission(key, subjects, "download");
                DataObject object = em.find(DataObject.class, identifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + identifier.getId() + "] from storage");
                }
                hash = object.getStream();
                break;
            }
            case MetadataObject.OBJECT_TYPE: {
                authorisation.checkPermission(key, subjects, "read");
                MetadataObject object = em.find(MetadataObject.class, identifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load metadata with id [" + identifier.getId() + "] from storage");
                }
                hash = object.getStream();
                break;
            }
            default:
                throw new CoreServiceException("unable to find downloadable content for key [" + key + "]");
            }
            if (hash != null && hash.length() > 0) {
                InputStream stream = binarystore.get(hash);
                ArgumentsBuilder argumentsBuilder = new ArgumentsBuilder("hash", hash);
                notification.throwEvent(key, caller, identifier.getType(),
                        OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, identifier.getType(), "download"),
                        argumentsBuilder.build());
                return stream;
            } else {
                throw new DataNotFoundException("there is no preview available for this data object");
            }
        } catch (NotificationServiceException | BinaryStoreServiceException | MembershipServiceException
                | RegistryServiceException | AuthorisationServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred during getting preview content", e);
            throw new CoreServiceException("unable to get preview content", e);
        }
    }

    /* Service */

    @Override
    public String getServiceName() {
        return SERVICE_NAME;
    }

    @Override
    public Map<String, String> getServiceInfos() {
        Map<String, String> infos = new HashMap<String, String>();
        try {
            infos.put(INFO_WORKSPACES_ALL, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE), null)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_WORKSPACES_ALL, e);
        }
        try {
            infos.put(INFO_COLLECTIONS_ALL, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE), null)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_COLLECTIONS_ALL, e);
        }
        try {
            infos.put(INFO_OBJECTS_ALL, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE), null)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_OBJECTS_ALL, e);
        }
        try {
            infos.put(INFO_WORKSPACES_PUBLISHED, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE), Status.PUBLISHED)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_WORKSPACES_PUBLISHED, e);
        }
        try {
            infos.put(INFO_COLLECTIONS_PUBLISHED, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, Collection.OBJECT_TYPE), Status.PUBLISHED)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_COLLECTIONS_PUBLISHED, e);
        }
        try {
            infos.put(INFO_OBJECTS_PUBLISHED, Long.toString(registry.count(OrtolangObjectIdentifier
                    .buildJPQLFilterPattern(CoreService.SERVICE_NAME, DataObject.OBJECT_TYPE), Status.PUBLISHED)));
        } catch (Exception e) {
            LOGGER.log(Level.INFO, "unable to collect info: " + INFO_OBJECTS_PUBLISHED, e);
        }
        return infos;
    }

    @Override
    public String[] getObjectTypeList() {
        return OBJECT_TYPE_LIST;
    }

    @Override
    public String[] getObjectPermissionsList(String type) throws OrtolangException {
        for (int i = 0; i < OBJECT_PERMISSIONS_LIST.length; i++) {
            if (OBJECT_PERMISSIONS_LIST[i][0].equals(type)) {
                return OBJECT_PERMISSIONS_LIST[i][1].split(",");
            }
        }
        throw new OrtolangException("Unable to find object permissions list for object type : " + type);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public OrtolangObject findObject(String key) throws OrtolangException {
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(key);

            if (!identifier.getService().equals(CoreService.SERVICE_NAME)) {
                throw new OrtolangException(
                        "object identifier " + identifier + " does not refer to service " + getServiceName());
            }

            switch (identifier.getType()) {
            case Workspace.OBJECT_TYPE:
                return readWorkspace(key);
            case DataObject.OBJECT_TYPE:
                return readDataObject(key);
            case Collection.OBJECT_TYPE:
                return readCollection(key);
            case Link.OBJECT_TYPE:
                return readLink(key);
            case MetadataObject.OBJECT_TYPE:
                return readMetadataObject(key);
            }

            throw new OrtolangException(
                    "object identifier " + identifier + " does not refer to service " + getServiceName());
        } catch (CoreServiceException | RegistryServiceException | KeyNotFoundException e) {
            throw new OrtolangException("unable to find an object for key " + key);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public OrtolangObjectSize getSize(String key) throws OrtolangException {
        LOGGER.log(Level.FINE, "calculating size for object with key [" + key + "]");
        try {
            List<String> subjects = membership.getConnectedIdentifierSubjects();
            OrtolangObjectIdentifier cidentifier = registry.lookup(key);
            if (!cidentifier.getService().equals(CoreService.SERVICE_NAME)) {
                throw new OrtolangException(
                        "object identifier " + cidentifier + " does not refer to service " + getServiceName());
            }
            OrtolangObjectSize ortolangObjectSize = new OrtolangObjectSize();
            switch (cidentifier.getType()) {
            case DataObject.OBJECT_TYPE: {
                authorisation.checkPermission(key, subjects, "read");
                DataObject dataObject = em.find(DataObject.class, cidentifier.getId());
                ortolangObjectSize.addElement(DataObject.OBJECT_TYPE, dataObject.getSize());
                break;
            }
            case Collection.OBJECT_TYPE: {
                ortolangObjectSize = getCollectionSize(key, cidentifier, ortolangObjectSize, subjects);
                break;
            }
            case Workspace.OBJECT_TYPE: {
                authorisation.checkPermission(key, subjects, "read");
                Workspace workspace = em.find(Workspace.class, cidentifier.getId());
                ortolangObjectSize = getCollectionSize(workspace.getHead(), registry.lookup(workspace.getHead()),
                        ortolangObjectSize, subjects);
                for (SnapshotElement snapshotElement : workspace.getSnapshots()) {
                    ortolangObjectSize = getCollectionSize(snapshotElement.getKey(),
                            registry.lookup(snapshotElement.getKey()), ortolangObjectSize, subjects);
                }
                ortolangObjectSize.addElement("members", workspace.getMembers().split(",").length);
                ortolangObjectSize.addElement("snapshots", workspace.getSnapshots().size());
                break;
            }
            }
            return ortolangObjectSize;
        } catch (CoreServiceException | MembershipServiceException | RegistryServiceException
                | AuthorisationServiceException | AccessDeniedException | KeyNotFoundException e) {
            LOGGER.log(Level.SEVERE, "unexpected error while calculating object size", e);
            throw new OrtolangException("unable to calculate size for object with key [" + key + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<OrtolangObject> findObjectByBinaryHash(String hash) throws OrtolangException {
        try {
            TypedQuery<DataObject> query = em.createNamedQuery("findObjectByBinaryHash", DataObject.class)
                    .setParameter("hash", hash);
            List<DataObject> objects = query.getResultList();
            for (DataObject object : objects) {
                object.setKey(registry.lookup(object.getObjectIdentifier()));
            }
            TypedQuery<MetadataObject> query2 = em
                    .createNamedQuery("findMetadataObjectByBinaryHash", MetadataObject.class)
                    .setParameter("hash", hash);
            List<MetadataObject> objects2 = query2.getResultList();
            for (MetadataObject mobject : objects2) {
                mobject.setKey(registry.lookup(mobject.getObjectIdentifier()));
            }
            List<OrtolangObject> oobjects = new ArrayList<OrtolangObject>();
            oobjects.addAll(objects);
            oobjects.addAll(objects2);
            return oobjects;
        } catch (Exception e) {
            throw new OrtolangException("unable to find an object for hash " + hash);
        }
    }

    @Override
    public List<OrtolangIndexableContent> getIndexableContent(String key)
            throws KeyNotFoundException, RegistryServiceException, IndexingServiceException, OrtolangException {
        OrtolangObjectIdentifier identifier = registry.lookup(key);
        if (!identifier.getService().equals(CoreService.SERVICE_NAME)) {
            throw new OrtolangException(
                    "object identifier " + identifier + " does not refer to service " + getServiceName());
        }

        List<OrtolangIndexableContent> indexableContents = new ArrayList<>();
        OrtolangObject object = null;
        switch (identifier.getType()) {
        case DataObject.OBJECT_TYPE:
            DataObject dataObject = em.find(DataObject.class, identifier.getId());
            object = dataObject;
            dataObject.setKey(key);
            //            indexableContents.add(new DataObjectIndexableContent(dataObject));
            break;
        case Collection.OBJECT_TYPE:
            Collection collection = em.find(Collection.class, identifier.getId());
            object = collection;
            collection.setKey(key);
            //            indexableContents.add(new CollectionIndexableContent(collection));
            if (collection.isRoot() && Status.PUBLISHED.value().equals(registry.getPublicationStatus(key))) {
                try {
                    TypedQuery<Workspace> query = em
                            .createNamedQuery("findWorkspaceByRootCollection", Workspace.class)
                            .setParameter("root", "%" + collection.getKey() + "%");
                    Workspace workspace = query.getSingleResult();
                    String wskey = registry.lookup(workspace.getObjectIdentifier());
                    workspace.setKey(wskey);
                    String snapshot = workspace.findSnapshotByKey(collection.getKey()).getName();
                    TagElement tagElement = workspace.findTagBySnapshot(snapshot);
                    if (tagElement == null) {
                        // Do not index published root collection with no associated tag: replaced by new version (tag moved)
                        break;
                    }
                    String tag = tagElement.getName();
                    //                    CollectionContent collectionContent = listCollectionContent(collection.getKey());

                    // Rating metadata for root collections
                    int rate = 0;
                    MetadataElement ratingMetadata = collection.findMetadataByName(MetadataFormat.RATING);
                    if (ratingMetadata != null) {
                        OrtolangObjectIdentifier ratingMetadataIdentifier = registry
                                .lookup(ratingMetadata.getKey());
                        MetadataObject ratingMetadataObject = em.find(MetadataObject.class,
                                ratingMetadataIdentifier.getId());
                        ObjectMapper mapper = new ObjectMapper();
                        try {
                            Map<String, Object> content = mapper.readValue(
                                    binarystore.getFile(ratingMetadataObject.getStream()),
                                    new TypeReference<Map<String, Object>>() {
                                    });
                            Object score = content.get("score");
                            if (score != null && score instanceof Integer) {
                                rate = (Integer) score;
                            }
                        } catch (IOException | BinaryStoreServiceException | DataNotFoundException e) {
                        }

                    }

                    // Index item metadata for root collections
                    MetadataElement itemMetadata = collection.findMetadataByName(MetadataFormat.ITEM);
                    if (itemMetadata != null) {
                        OrtolangObjectIdentifier itemMetadataIdentifier = registry.lookup(itemMetadata.getKey());
                        MetadataObject metadataObject = em.find(MetadataObject.class,
                                itemMetadataIdentifier.getId());
                        // Indexes in item-all index : /item-all/{ortolang-type}
                        indexableContents.add(new OrtolangItemIndexableContent(metadataObject, collection,
                                workspace.getAlias(), snapshot, tag, rate, workspace.isArchive(), false));
                        String latestPublishedSnapshot = findWorkspaceLatestPublishedSnapshot(workspace.getKey());
                        if (snapshot.equals(latestPublishedSnapshot)) {
                            // Indexes in item index : /item/{ortolang-type}
                            indexableContents.add(new OrtolangItemIndexableContent(metadataObject, collection,
                                    workspace.getAlias(), snapshot, tag, rate, workspace.isArchive(), true));
                        }
                    }

                    //                    for (CollectionContentEntry entry : collectionContent.getContent()) {
                    //                        Map<String, Object> params = new HashMap<>();
                    //                        params.put("alias", workspace.getAlias());
                    //                        params.put("key", workspace.getKey());
                    //                        params.put("root", collection.getKey());
                    //                        params.put("path", entry.getPath());
                    //                        params.put("snapshot", snapshot);
                    //                        indexableContents.add(new OrtolangObjectScriptedUpdate(CoreService.SERVICE_NAME, entry.getType(), entry.getKey(),"updateRootCollectionChild", params));
                    //                    }
                } catch (IdentifierNotRegisteredException | CoreServiceException | NoResultException e) {
                    throw new OrtolangException(e.getMessage(), e);
                }
            }
            break;
        case Workspace.OBJECT_TYPE:
            Workspace workspace = em.find(Workspace.class, identifier.getId());
            workspace.setKey(key);
            // Indexes in core index : /core/workspace
            return Collections.singletonList(new WorkspaceIndexableContent(workspace));
        case MetadataObject.OBJECT_TYPE:
            MetadataObject metadataObject = em.find(MetadataObject.class, identifier.getId());
            MetadataFormat format = em.find(MetadataFormat.class, metadataObject.getFormat());
            metadataObject.setKey(key);
            // Indexes in core index : /core/metadata
            //            indexableContents.add(new MetadataObjectIndexableContent(metadataObject, format));
            if (!metadataObject.getName().startsWith("system-") && !metadataObject.getName().startsWith("ortolang-")
                    && Status.PUBLISHED.value().equals(registry.getPublicationStatus(key))) {
                // Indexes in core index :  /metadata/{format}
                indexableContents.add(new UserMetadataIndexableContent(metadataObject, format));
            }
        }
        //        if (object != null) {
        //            for (MetadataElement metadataElement : ((MetadataSource) object).getMetadatas()) {
        ////                if (metadataElement.getName().startsWith("system-x-")) {
        //                    OrtolangObjectIdentifier mdIdentifier = registry.lookup(metadataElement.getKey());
        //                    MetadataObject metadataObject = em.find(MetadataObject.class, mdIdentifier.getId());
        //                    MetadataFormat format = em.find(MetadataFormat.class, metadataObject.getFormat());
        //                    metadataObject.setKey(metadataElement.getKey());
        //                    indexableContents.add(new MetadataObjectIndexableContent(metadataObject, format));
        ////                }
        //            }
        //        }
        return indexableContents;
    }

    private void checkObjectType(OrtolangObjectIdentifier identifier, String objectType)
            throws CoreServiceException {
        if (!identifier.getService().equals(getServiceName())) {
            throw new CoreServiceException(
                    "object identifier " + identifier + " does not refer to service " + getServiceName());
        }

        if (!identifier.getType().equals(objectType)) {
            throw new CoreServiceException(
                    "object identifier " + identifier + " does not refer to an object of type " + objectType);
        }
    }

    @Override
    public OrtolangObjectXmlExportHandler getObjectXmlExportHandler(String key) throws OrtolangException {
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(key);
            if (!identifier.getService().equals(CoreService.SERVICE_NAME)) {
                throw new OrtolangException(
                        "object identifier " + identifier + " does not refer to service " + getServiceName());
            }

            switch (identifier.getType()) {
            case Workspace.OBJECT_TYPE:
                Workspace workspace = em.find(Workspace.class, identifier.getId());
                if (workspace == null) {
                    throw new OrtolangException(
                            "unable to load workspace with id [" + identifier.getId() + "] from storage");
                }
                return new WorkspaceExportHandler(workspace);
            case Collection.OBJECT_TYPE:
                Collection collection = em.find(Collection.class, identifier.getId());
                if (collection == null) {
                    throw new OrtolangException(
                            "unable to load collection with id [" + identifier.getId() + "] from storage");
                }
                return new CollectionExportHandler(collection);
            case DataObject.OBJECT_TYPE:
                DataObject object = em.find(DataObject.class, identifier.getId());
                if (object == null) {
                    throw new OrtolangException(
                            "unable to load dataobject with id [" + identifier.getId() + "] from storage");
                }
                return new DataObjectExportHandler(object);
            case MetadataObject.OBJECT_TYPE:
                MetadataObject metadata = em.find(MetadataObject.class, identifier.getId());
                if (metadata == null) {
                    throw new OrtolangException(
                            "unable to load metadata with id [" + identifier.getId() + "] from storage");
                }
                return new MetadataObjectExportHandler(metadata);
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, identifier.getId());
                if (link == null) {
                    throw new OrtolangException(
                            "unable to load link with id [" + identifier.getId() + "] from storage");
                }
                return new LinkExportHandler(link);
            }

        } catch (RegistryServiceException | KeyNotFoundException e) {
            throw new OrtolangException("unable to build object export handler " + key, e);
        }
        throw new OrtolangException("unable to build object export handler for key " + key);
    }

    @Override
    public OrtolangObjectXmlImportHandler getObjectXmlImportHandler(String type) throws OrtolangException {
        throw new OrtolangException("NOT IMPLEMENTED");
    }

    // System operations
    @Override
    @RolesAllowed({ "admin", "system" })
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public Workspace systemReadWorkspace(String wskey) throws CoreServiceException, KeyNotFoundException {
        LOGGER.log(Level.FINE, "#SYSTEM# reading workspace for key [" + wskey + "]");
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setKey(wskey);

            return workspace;
        } catch (RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while reading workspace", e);
            throw new CoreServiceException("unable to read workspace with key [" + wskey + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Set<String> systemListWorkspaceKeys(String wskey) throws CoreServiceException, KeyNotFoundException {
        LOGGER.log(Level.FINE, "#SYSTEM# listing workspace keys [" + wskey + "]");
        Set<String> keys = new HashSet<String>();
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);
            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            keys = systemListCollectionKeys(workspace.getHead(), keys);
            for (SnapshotElement snapshot : workspace.getSnapshots()) {
                keys = systemListCollectionKeys(snapshot.getKey(), keys);
            }

            return keys;
        } catch (RegistryServiceException e) {
            LOGGER.log(Level.SEVERE, "unexpected error occurred while listing workspace keys", e);
            throw new CoreServiceException("unable to list keys for workspace with key [" + wskey + "]", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private Set<String> systemListCollectionKeys(String key, Set<String> keys)
            throws RegistryServiceException, KeyNotFoundException, CoreServiceException {
        OrtolangObjectIdentifier cidentifier = registry.lookup(key);
        checkObjectType(cidentifier, Collection.OBJECT_TYPE);
        Collection collection = em.find(Collection.class, cidentifier.getId());
        if (collection == null) {
            throw new CoreServiceException(
                    "unable to load collection with id [" + cidentifier.getId() + "] from storage");
        }
        if (keys.add(key)) {
            for (MetadataElement element : collection.getMetadatas()) {
                keys.add(element.getKey());
            }

            for (CollectionElement element : collection.getElements()) {
                keys.add(element.getKey());
                if (element.getType().equals(Collection.OBJECT_TYPE)) {
                    systemListCollectionKeys(element.getKey(), keys);
                }
                if (element.getType().equals(DataObject.OBJECT_TYPE)) {
                    OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                    checkObjectType(identifier, DataObject.OBJECT_TYPE);
                    DataObject object = em.find(DataObject.class, identifier.getId());
                    if (object == null) {
                        throw new CoreServiceException(
                                "unable to load object with id [" + identifier.getId() + "] from storage");
                    }
                    keys.add(element.getKey());
                    for (MetadataElement mde : object.getMetadatas()) {
                        keys.add(mde.getKey());
                    }
                }
                if (element.getType().equals(Link.OBJECT_TYPE)) {
                    OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                    checkObjectType(identifier, Link.OBJECT_TYPE);
                    Link link = em.find(Link.class, identifier.getId());
                    if (link == null) {
                        throw new CoreServiceException(
                                "unable to load link with id [" + identifier.getId() + "] from storage");
                    }
                    keys.add(element.getKey());
                    for (MetadataElement mde : link.getMetadatas()) {
                        keys.add(mde.getKey());
                    }
                }
            }
        }

        return keys;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void systemSetWorkspaceReadOnly(String wskey, boolean readonly)
            throws CoreServiceException, KeyNotFoundException, NotificationServiceException {
        LOGGER.log(Level.FINE, "#SYSTEM# setting workspace [" + wskey + "] read only to [" + readonly + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setReadOnly(readonly);
            em.merge(workspace);

            registry.update(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(1).addArgument("ws-alias", workspace.getAlias());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE, OrtolangEvent
                    .buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, readonly ? "lock" : "unlock"),
                    argsBuilder.build());
        } catch (KeyLockedException | RegistryServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE,
                    "unexpected error occurred while setting workspace read only mode to [" + readonly + "]", e);
            throw new CoreServiceException(
                    "unable to set workspace with key [" + wskey + "] read only mode to  [" + readonly + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void systemUpdateWorkspace(String wskey, String alias, boolean changed, String head, String members,
            String privileged, boolean readOnly, String type)
            throws CoreServiceException, KeyNotFoundException, NotificationServiceException {
        LOGGER.log(Level.FINE, "#SYSTEM# updating workspace [" + wskey + "]");
        try {
            String caller = membership.getProfileKeyForConnectedIdentifier();
            OrtolangObjectIdentifier identifier = registry.lookup(wskey);
            checkObjectType(identifier, Workspace.OBJECT_TYPE);

            Workspace workspace = em.find(Workspace.class, identifier.getId());
            if (workspace == null) {
                throw new CoreServiceException(
                        "unable to load workspace with id [" + identifier.getId() + "] from storage");
            }
            workspace.setAlias(alias);
            workspace.setChanged(changed);
            workspace.setHead(head);
            workspace.setMembers(members);
            workspace.setPrivileged(privileged);
            workspace.setReadOnly(readOnly);
            workspace.setType(type);
            em.merge(workspace);

            registry.update(wskey);

            ArgumentsBuilder argsBuilder = new ArgumentsBuilder(1).addArgument("ws-alias", workspace.getAlias());
            notification.throwEvent(wskey, caller, Workspace.OBJECT_TYPE,
                    OrtolangEvent.buildEventType(CoreService.SERVICE_NAME, Workspace.OBJECT_TYPE, "update"),
                    argsBuilder.build());
        } catch (KeyLockedException | RegistryServiceException e) {
            ctx.setRollbackOnly();
            LOGGER.log(Level.SEVERE, "unexpected error occurred while setting workspace alias to [" + alias + "]",
                    e);
            throw new CoreServiceException(
                    "unable to set workspace with key [" + wskey + "] alias to  [" + alias + "]", e);
        }
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void systemCreateMetadata(String tkey, String name, String hash, String filename)
            throws KeyNotFoundException, CoreServiceException, MetadataFormatException, DataNotFoundException,
            BinaryStoreServiceException, KeyAlreadyExistsException, IdentifierAlreadyRegisteredException,
            RegistryServiceException, AuthorisationServiceException, IndexingServiceException {
        LOGGER.log(Level.FINE, "#SYSTEM# create metadata for key [" + tkey + "]");
        if (!name.startsWith("system-")) {
            throw new CoreServiceException("only system metadata can be added this way.");
        }
        OrtolangObjectIdentifier identifier = registry.lookup(tkey);
        if (!identifier.getService().equals(SERVICE_NAME)) {
            throw new CoreServiceException("metadata target can only be a Link, a DataObject or a Collection.");
        }
        OrtolangObject object;
        switch (identifier.getType()) {
        case DataObject.OBJECT_TYPE:
            object = em.find(DataObject.class, identifier.getId());
            break;
        case Link.OBJECT_TYPE:
            object = em.find(Link.class, identifier.getId());
            break;
        case Collection.OBJECT_TYPE:
            object = em.find(Collection.class, identifier.getId());
            break;
        default:
            throw new CoreServiceException("metadata target can only be a Link, a DataObject or a Collection.");
        }
        MetadataElement metadataElement = ((MetadataSource) object).findMetadataByName(name);
        if (metadataElement != null) {
            ((MetadataSource) object).removeMetadata(metadataElement);
        }
        MetadataObject meta = new MetadataObject();
        meta.setId(UUID.randomUUID().toString());
        meta.setName(name);

        MetadataFormat format = getMetadataFormat(name);
        if (format == null) {
            LOGGER.log(Level.SEVERE, "Unable to find a metadata format for name: " + name);
            throw new CoreServiceException("unknown metadata format for name: " + name);
        }

        if (hash != null && hash.length() > 0) {
            if (format.isValidationNeeded()) {
                validateMetadata(hash, format);
            }
            meta.setSize(binarystore.size(hash));
            if (filename != null) {
                meta.setContentType(binarystore.type(hash, filename));
            } else {
                meta.setContentType(binarystore.type(hash));
            }
            meta.setStream(hash);
        } else {
            meta.setSize(0);
            meta.setContentType("application/octet-stream");
            meta.setStream("");
        }

        meta.setFormat(format.getId());
        meta.setTarget(tkey);
        meta.setKey(UUID.randomUUID().toString());
        em.persist(meta);

        try {
            String wskey = registry.getProperty(tkey, WORKSPACE_REGISTRY_PROPERTY_KEY);
            Properties properties = new Properties();
            properties.put(WORKSPACE_REGISTRY_PROPERTY_KEY, wskey);
            registry.register(meta.getKey(), meta.getObjectIdentifier(), "system", properties);
        } catch (PropertyNotFoundException e) {
            registry.register(meta.getKey(), meta.getObjectIdentifier(), "system");
        }

        registry.refresh(tkey);
        indexing.index(tkey);
        authorisation.clonePolicy(meta.getKey(), tkey);

        ((MetadataSource) object).addMetadata(new MetadataElement(name, meta.getKey()));
        em.merge(object);
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Collection> systemListCollections() throws CoreServiceException {
        try {
            TypedQuery<Collection> query = em.createNamedQuery("findCollections", Collection.class);
            List<Collection> collections = query.getResultList();
            for (Iterator<Collection> iterator = collections.iterator(); iterator.hasNext();) {
                try {
                    Collection collection = iterator.next();
                    String key = registry.lookup(collection.getObjectIdentifier());
                    collection.setKey(key);
                } catch (IdentifierNotRegisteredException e) {
                    iterator.remove();
                }
            }
            return collections;
        } catch (RegistryServiceException e) {
            throw new CoreServiceException(e);
        }
    }

    /* ### Internal operations ### */

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private OrtolangObject readObject(String key) throws CoreServiceException, KeyNotFoundException {
        try {
            OrtolangObjectIdentifier identifier = registry.lookup(key);

            if (!identifier.getService().equals(CoreService.SERVICE_NAME)) {
                throw new CoreServiceException(
                        "object identifier " + identifier + " does not refer to service " + getServiceName());
            }

            switch (identifier.getType()) {
            case Workspace.OBJECT_TYPE:
                Workspace workspace = em.find(Workspace.class, identifier.getId());
                if (workspace == null) {
                    throw new CoreServiceException(
                            "unable to load workspace with id [" + identifier.getId() + "] from storage");
                }
                workspace.setKey(key);
                return workspace;
            case DataObject.OBJECT_TYPE:
                DataObject object = em.find(DataObject.class, identifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + identifier.getId() + "] from storage");
                }
                object.setKey(key);
                return object;
            case Collection.OBJECT_TYPE:
                Collection collection = em.find(Collection.class, identifier.getId());
                if (collection == null) {
                    throw new CoreServiceException(
                            "unable to load collection with id [" + identifier.getId() + "] from storage");
                }
                collection.setKey(key);
                return collection;
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, identifier.getId());
                if (link == null) {
                    throw new CoreServiceException(
                            "unable to load link with id [" + identifier.getId() + "] from storage");
                }
                link.setKey(key);
                return link;
            case MetadataObject.OBJECT_TYPE:
                MetadataObject meta = em.find(MetadataObject.class, identifier.getId());
                if (meta == null) {
                    throw new CoreServiceException(
                            "unable to load metadata with id [" + identifier.getId() + "] from storage");
                }
                meta.setKey(key);
                return meta;
            default:
                throw new CoreServiceException("object identifier " + identifier
                        + " refer to an unknown type for service " + getServiceName());
            }

        } catch (RegistryServiceException e) {
            throw new CoreServiceException("unable to read object with key " + key);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private Collection readCollectionAtPath(String root, PathBuilder path) throws PathNotFoundException,
            CoreServiceException, RegistryServiceException, KeyNotFoundException, InvalidPathException {
        LOGGER.log(Level.FINE, "Reading tree from root [" + root + "] to path [" + path.build() + "]");
        String[] parts = path.buildParts();
        Collection node;
        PathBuilder current = PathBuilder.newInstance();

        OrtolangObjectIdentifier ridentifier = registry.lookup(root);
        checkObjectType(ridentifier, Collection.OBJECT_TYPE);
        node = em.find(Collection.class, ridentifier.getId());
        if (node == null) {
            throw new CoreServiceException(
                    "unable to load root collection with id [" + ridentifier.getId() + "] from storage");
        }
        node.setKey(root);
        if (!node.isRoot()) {
            throw new CoreServiceException("root collection [" + root + "] is not flagged as a root collection");
        }

        for (int i = 0; i < parts.length; i++) {
            current.path(parts[i]);
            CollectionElement element = node.findElementByName(parts[i]);
            if (element == null) {
                throw new PathNotFoundException(path.build());
            }
            OrtolangObjectIdentifier cidentifier = registry.lookup(element.getKey());
            checkObjectType(cidentifier, Collection.OBJECT_TYPE);
            node = em.find(Collection.class, cidentifier.getId());
            if (node == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + cidentifier.getId() + "] from storage");
            }
            node.setKey(element.getKey());
            if (node.isRoot()) {
                LOGGER.log(Level.SEVERE, "WRONG ROOT FLAG found for collection key [" + element.getKey()
                        + "] at path [" + current.build() + "] with root [" + root + "]");
                throw new CoreServiceException("Internal Problem : collection [" + parts[i]
                        + "] is a root collection but is not a root node");
            }
        }

        return node;
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private Collection loadCollectionAtPath(String root, PathBuilder path, int clock) throws CoreServiceException,
            RegistryServiceException, KeyNotFoundException, InvalidPathException, PathNotFoundException {
        LOGGER.log(Level.FINE,
                "Loading tree from root [" + root + "] to path [" + path.build() + "] with clock [" + clock + "]");
        try {
            String[] parts = path.buildParts();
            Collection parent;
            Collection leaf;
            PathBuilder current = PathBuilder.newInstance();

            OrtolangObjectIdentifier ridentifier = registry.lookup(root);
            checkObjectType(ridentifier, Collection.OBJECT_TYPE);
            leaf = em.find(Collection.class, ridentifier.getId());
            if (leaf == null) {
                throw new CoreServiceException(
                        "unable to load root collection with id [" + ridentifier.getId() + "] from storage");
            }
            leaf.setKey(root);
            if (!leaf.isRoot()) {
                throw new CoreServiceException(
                        "root collection [" + root + "] is not flagged as a root collection");
            }
            if (leaf.getClock() < clock) {
                LOGGER.log(Level.SEVERE, "WRONG CLOCK found for root collection key [ " + root + "]");
                throw new CoreServiceException("root collection [" + root + "] clock is not good");
            }

            for (int i = 0; i < parts.length; i++) {
                parent = leaf;
                current.path(parts[i]);
                CollectionElement element = parent.findElementByName(parts[i]);
                if (element == null) {
                    throw new PathNotFoundException(path.build());
                }
                OrtolangObjectIdentifier cidentifier = registry.lookup(element.getKey());
                checkObjectType(cidentifier, Collection.OBJECT_TYPE);
                leaf = em.find(Collection.class, cidentifier.getId());
                if (leaf == null) {
                    throw new CoreServiceException(
                            "unable to load collection with id [" + cidentifier.getId() + "] from storage");
                }
                leaf.setKey(element.getKey());
                if (leaf.isRoot()) {
                    LOGGER.log(Level.SEVERE, "WRONG ROOT FLAG found for collection key [" + element.getKey()
                            + "] at path [" + current.build() + "] with root [" + root + "]");
                    throw new CoreServiceException("Internal Problem : collection [" + parts[i]
                            + "] is a root collection but is not a root node");
                }
                if (leaf.getClock() < clock) {
                    Collection clone = cloneCollection(root, leaf, clock);
                    parent.removeElement(element);
                    CollectionElement celement = new CollectionElement(Collection.OBJECT_TYPE, clone.getName(),
                            System.currentTimeMillis(), 0, Collection.MIME_TYPE, clone.getKey());
                    parent.addElement(celement);
                    registry.update(parent.getKey());
                    leaf = clone;
                }
            }

            return leaf;
        } catch (KeyLockedException | CloneException e) {
            ctx.setRollbackOnly();
            throw new CoreServiceException("unexpected error during loading collection at path " + path.build(), e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private Collection cloneCollection(String root, Collection origin, int clock) throws CloneException {
        LOGGER.log(Level.FINE, "cloning collection for origin [" + origin.getKey() + "]");
        try {
            String key = UUID.randomUUID().toString();

            Collection clone = new Collection();
            clone.setId(UUID.randomUUID().toString());
            clone.setName(origin.getName());
            clone.setRoot(origin.isRoot());
            clone.setClock(clock);
            Set<CollectionElement> elements = new HashSet<CollectionElement>();
            elements.addAll(origin.getElements());
            clone.setElements(elements);
            Set<MetadataElement> metadatas = new HashSet<MetadataElement>();
            for (MetadataElement mde : origin.getMetadatas()) {
                MetadataObject mdclone = cloneMetadataObject(root, mde.getKey(), key);
                metadatas.add(new MetadataElement(mde.getName(), mdclone.getKey()));
            }
            clone.setMetadatas(metadatas);
            clone.setKey(key);
            em.persist(clone);

            registry.register(key, clone.getObjectIdentifier(), origin.getKey(), true);
            indexing.index(key);
            authorisation.clonePolicy(key, root);

            return clone;
        } catch (RegistryServiceException | KeyAlreadyExistsException | IdentifierAlreadyRegisteredException
                | KeyNotFoundException | AuthorisationServiceException | IndexingServiceException e) {
            throw new CloneException("unable to clone collection with origin key [" + origin + "]", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private DataObject cloneDataObject(String root, DataObject origin, int clock) throws CloneException {
        LOGGER.log(Level.FINE, "cloning object for origin [" + origin.getKey() + "]");
        try {
            String key = UUID.randomUUID().toString();

            DataObject clone = new DataObject();
            clone.setId(UUID.randomUUID().toString());
            clone.setName(origin.getName());
            clone.setSize(origin.getSize());
            clone.setMimeType(origin.getMimeType());
            clone.setStream(origin.getStream());
            clone.setClock(clock);
            Set<MetadataElement> metadatas = new HashSet<MetadataElement>();
            for (MetadataElement mde : origin.getMetadatas()) {
                MetadataObject mdclone = cloneMetadataObject(root, mde.getKey(), key);
                metadatas.add(new MetadataElement(mde.getName(), mdclone.getKey()));
            }
            clone.setMetadatas(metadatas);
            clone.setKey(key);
            em.persist(clone);

            registry.register(key, clone.getObjectIdentifier(), origin.getKey(), true);
            indexing.index(key);
            authorisation.clonePolicy(key, root);

            return clone;
        } catch (RegistryServiceException | IdentifierAlreadyRegisteredException | AuthorisationServiceException
                | KeyAlreadyExistsException | KeyNotFoundException | IndexingServiceException e) {
            throw new CloneException("unable to clone object with origin [" + origin + "]", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private Link cloneLink(String root, Link origin, int clock) throws CloneException {
        LOGGER.log(Level.FINE, "cloning link for origin [" + origin + "]");
        try {
            String key = UUID.randomUUID().toString();

            Link clone = new Link();
            clone.setId(UUID.randomUUID().toString());
            clone.setName(origin.getName());
            clone.setTarget(origin.getTarget());
            clone.setClock(clock);
            Set<MetadataElement> metadatas = new HashSet<MetadataElement>();
            for (MetadataElement mde : origin.getMetadatas()) {
                MetadataObject mdclone = cloneMetadataObject(root, mde.getKey(), key);
                metadatas.add(new MetadataElement(mde.getName(), mdclone.getKey()));
            }
            clone.setMetadatas(metadatas);
            clone.setKey(key);
            em.persist(clone);

            registry.register(key, clone.getObjectIdentifier(), origin.getKey(), true);
            indexing.index(key);
            authorisation.clonePolicy(key, root);

            return clone;
        } catch (RegistryServiceException | IdentifierAlreadyRegisteredException | AuthorisationServiceException
                | KeyAlreadyExistsException | KeyNotFoundException | IndexingServiceException e) {
            throw new CloneException("unable to clone link with origin [" + origin + "]", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private MetadataObject cloneMetadataObject(String root, String origin, String target) throws CloneException {
        LOGGER.log(Level.FINE, "clone metadata for origin [" + origin + "], target [" + target + "]");
        try {
            String key = UUID.randomUUID().toString();

            OrtolangObjectIdentifier identifier = registry.lookup(origin);
            checkObjectType(identifier, MetadataObject.OBJECT_TYPE);

            MetadataObject meta = em.find(MetadataObject.class, identifier.getId());
            if (meta == null) {
                throw new CoreServiceException(
                        "unable to load metadata with id [" + identifier.getId() + "] from storage");
            }

            MetadataObject clone = new MetadataObject();
            clone.setId(UUID.randomUUID().toString());
            clone.setName(meta.getName());
            clone.setTarget(target);
            clone.setSize(meta.getSize());
            clone.setFormat(meta.getFormat());
            clone.setContentType(meta.getContentType());
            clone.setStream(meta.getStream());
            clone.setKey(key);
            em.persist(clone);

            registry.register(key, clone.getObjectIdentifier(), origin, true);
            indexing.index(target);
            authorisation.clonePolicy(key, root);

            return clone;
        } catch (RegistryServiceException | IdentifierAlreadyRegisteredException | AuthorisationServiceException
                | CoreServiceException | KeyNotFoundException | KeyAlreadyExistsException
                | IndexingServiceException e) {
            throw new CloneException(
                    "unable to clone metadata with origin [" + origin + "] and target [" + target + "]", e);
        }
    }

    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    private OrtolangObjectSize getCollectionSize(String key, OrtolangObjectIdentifier cidentifier,
            OrtolangObjectSize ortolangObjectSize, List<String> subjects)
            throws KeyNotFoundException, RegistryServiceException, CoreServiceException {
        LOGGER.log(Level.FINE, "calculating collection size for collection with key [" + key + "]");
        try {
            authorisation.checkPermission(key, subjects, "read");

            Collection collection = em.find(Collection.class, cidentifier.getId());
            if (collection == null) {
                throw new CoreServiceException(
                        "unable to load collection with id [" + cidentifier.getId() + "] from storage");
            }

            for (CollectionElement element : collection.getElements()) {
                if (element.getType().equals(DataObject.OBJECT_TYPE)) {
                    try {
                        authorisation.checkPermission(element.getKey(), subjects, "read");
                        ortolangObjectSize.addElement(element.getType(), element.getSize());
                    } catch (AuthorisationServiceException | AccessDeniedException e) {
                        ortolangObjectSize.setPartial(true);
                    }
                } else if (element.getType().equals(Collection.OBJECT_TYPE)) {
                    ortolangObjectSize.addElement(element.getType(), 0);
                    OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                    ortolangObjectSize = getCollectionSize(element.getKey(), identifier, ortolangObjectSize,
                            subjects);
                }
            }
        } catch (AuthorisationServiceException | AccessDeniedException e) {
            ortolangObjectSize.setPartial(true);
        }
        return ortolangObjectSize;
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private void deleteCollectionContent(Collection collection, int clock) throws CoreServiceException,
            RegistryServiceException, KeyNotFoundException, KeyLockedException, IndexingServiceException {
        LOGGER.log(Level.FINE, "delete content for collection with id [" + collection.getId() + "]");
        for (CollectionElement element : collection.getElements()) {
            switch (element.getType()) {
            case Collection.OBJECT_TYPE: {
                OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                checkObjectType(identifier, Collection.OBJECT_TYPE);
                Collection coll = em.find(Collection.class, identifier.getId());
                if (coll == null) {
                    throw new CoreServiceException(
                            "unable to load collection with id [" + identifier.getId() + "] from storage");
                }
                if (coll.getClock() == clock) {
                    deleteCollectionContent(coll, clock);
                    LOGGER.log(Level.FINEST, "collection clock [" + coll.getClock()
                            + "] is the same, key can be deleted and unindexed");
                    for (MetadataElement mde : coll.getMetadatas()) {
                        registry.delete(mde.getKey());
                    }
                    registry.delete(element.getKey());
                    indexing.remove(element.getKey());
                }
                break;
            }
            case DataObject.OBJECT_TYPE: {
                OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                checkObjectType(identifier, DataObject.OBJECT_TYPE);
                DataObject object = em.find(DataObject.class, identifier.getId());
                if (object == null) {
                    throw new CoreServiceException(
                            "unable to load object with id [" + identifier.getId() + "] from storage");
                }
                if (object.getClock() == clock) {
                    LOGGER.log(Level.FINEST, "object clock [" + object.getClock()
                            + "] is the same, key can be deleted and unindexed");
                    for (MetadataElement mde : object.getMetadatas()) {
                        registry.delete(mde.getKey());
                    }
                    registry.delete(element.getKey());
                    indexing.remove(element.getKey());
                }
                break;
            }
            case Link.OBJECT_TYPE: {
                OrtolangObjectIdentifier identifier = registry.lookup(element.getKey());
                checkObjectType(identifier, Link.OBJECT_TYPE);
                Link link = em.find(Link.class, identifier.getId());
                if (link == null) {
                    throw new CoreServiceException(
                            "unable to load link with id [" + identifier.getId() + "] from storage");
                }
                if (link.getClock() == clock) {
                    LOGGER.log(Level.FINEST,
                            "link clock [" + link.getClock() + "] is the same, key can be deleted and unindexed");
                    for (MetadataElement mde : link.getMetadatas()) {
                        registry.delete(mde.getKey());
                    }
                    registry.delete(element.getKey());
                    indexing.remove(element.getKey());
                }
                break;
            }
            }
        }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    private void purgeChildrenMetadata(Collection collection, String wskey, String path, String name)
            throws OrtolangException, KeyNotFoundException, InvalidPathException, PathNotFoundException,
            WorkspaceReadOnlyException, CoreServiceException, RegistryServiceException {
        LOGGER.log(Level.FINE, "purging children metadata for path: " + path + " and name: " + name);
        MetadataElement metadataElement;
        OrtolangObjectIdentifier identifier;
        for (CollectionElement collectionElement : collection.getElements()) {
            metadataElement = null;
            identifier = registry.lookup(collectionElement.getKey());
            switch (collectionElement.getType()) {
            case Collection.OBJECT_TYPE:
                Collection subCollection = em.find(Collection.class, identifier.getId());
                metadataElement = subCollection.findMetadataByName(name);
                LOGGER.log(Level.FINE, "path is a collection, calling recursively purgeChildrenMetadata()");
                purgeChildrenMetadata(subCollection, wskey, path + "/" + subCollection.getName(), name);
                break;
            case DataObject.OBJECT_TYPE:
                DataObject dataObject = em.find(DataObject.class, identifier.getId());
                metadataElement = dataObject.findMetadataByName(name);
                break;
            case Link.OBJECT_TYPE:
                Link link = em.find(Link.class, identifier.getId());
                metadataElement = link.findMetadataByName(name);
                break;
            }
            if (metadataElement != null) {
                LOGGER.log(Level.FINE, "metadata to purge: " + metadataElement.getKey());
                deleteMetadataObject(wskey, path + "/" + collectionElement.getName(), name, false);
            }
        }
    }

    @Override
    public void extractMetadata(String wskey, String path, String mimeType)
            throws CoreServiceException, OrtolangException, InvalidPathException, PathNotFoundException,
            ExtractionServiceException, KeyNotFoundException {
        String key = resolveWorkspacePath(wskey, Workspace.HEAD, path);
        OrtolangObject object = findObject(key);
        if (object instanceof DataObject) {
            extraction.extract(key);
        } else if (object instanceof Collection) {
            extractMetadata((Collection) object, wskey, path);
        }
    }

    private void extractMetadata(Collection collection, String wskey, String path)
            throws ExtractionServiceException, CoreServiceException, AccessDeniedException, KeyNotFoundException {
        for (CollectionElement collectionElement : collection.getElements()) {
            if (collectionElement.getType().equals(DataObject.OBJECT_TYPE)) {
                extraction.extract(collectionElement.getKey());
            } else if (collectionElement.getType().equals(Collection.OBJECT_TYPE)) {
                extractMetadata(readCollection(collection.getKey()), wskey,
                        path + "/" + collectionElement.getName());
            }
        }
    }

    private boolean applyReadOnly(String caller, List<String> subjects, Workspace workspace) {
        if (!caller.equals(MembershipService.SUPERUSER_IDENTIFIER)
                && !subjects.contains(MembershipService.MODERATORS_GROUP_KEY) && workspace.isReadOnly()) {
            return true;
        }
        return false;
    }

    private static class PublicationPolicy {

        private String template;

        public String getTemplate() {
            return template;
        }

    }

    private static class CollectionContentEntry {

        private OrtolangObject object;

        private String path;

        CollectionContentEntry(OrtolangObject object, String path) {
            this.object = object;
            this.path = path;
        }

        public OrtolangObject getObject() {
            return object;
        }

        public String getPath() {
            return path;
        }

        public String getKey() {
            return object.getObjectKey();
        }

        public String getType() {
            return object.getObjectIdentifier().getType();
        }
    }

    private static class CollectionContent {

        private Set<CollectionContentEntry> content;

        CollectionContent() {
            content = new HashSet<>();
        }

        public void add(OrtolangObject object, String path) {
            content.add(new CollectionContentEntry(object, path));
        }

        public Set<CollectionContentEntry> getContent() {
            return content;
        }

        public Map<String, String> getPathKeyMap() {
            Map<String, String> map = new HashMap<>();
            for (CollectionContentEntry entry : content) {
                map.put(entry.getPath(), entry.getKey());
            }
            return map;
        }
    }

}