Index: /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistry.java
===================================================================
--- /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistry.java	(revision 5615)
+++ /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistry.java	(revision 5616)
@@ -1,104 +1,11 @@
 package eu.clarin.cmdi.virtualcollectionregistry;
 
-import eu.clarin.cmdi.oai.provider.impl.OAIProvider;
 import eu.clarin.cmdi.virtualcollectionregistry.model.User;
-import eu.clarin.cmdi.virtualcollectionregistry.model.User_;
 import eu.clarin.cmdi.virtualcollectionregistry.model.VirtualCollection;
 import eu.clarin.cmdi.virtualcollectionregistry.model.VirtualCollectionList;
-import eu.clarin.cmdi.virtualcollectionregistry.pid.PersistentIdentifier;
-import eu.clarin.cmdi.virtualcollectionregistry.pid.PersistentIdentifierProvider;
-import eu.clarin.cmdi.virtualcollectionregistry.query.ParsedQuery;
-import eu.clarin.cmdi.virtualcollectionregistry.service.VirtualCollectionValidator;
 import java.security.Principal;
-import java.util.Date;
 import java.util.List;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import javax.persistence.EntityManager;
-import javax.persistence.EntityTransaction;
-import javax.persistence.LockModeType;
-import javax.persistence.NoResultException;
-import javax.persistence.TypedQuery;
-import javax.persistence.criteria.CriteriaBuilder;
-import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Order;
-import javax.persistence.criteria.Predicate;
-import javax.persistence.criteria.Root;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.DisposableBean;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
 
-@Service
-public class VirtualCollectionRegistry implements InitializingBean, DisposableBean {
-
-    @Autowired
-    private DataStore datastore; //TODO: replace with Spring managed EM?
-    @Autowired
-    private PersistentIdentifierProvider pid_provider;
-    @Autowired
-    private OAIProvider oaiProvider;
-    @Autowired
-    @Qualifier("creation")
-    private VirtualCollectionValidator validator;
-    @Autowired
-    private AdminUsersService adminUsersService;
-
-    private static final Logger logger
-            = LoggerFactory.getLogger(VirtualCollectionRegistry.class);
-    private final AtomicBoolean intialized = new AtomicBoolean(false);
-    /**
-     * Scheduled executor service for the maintenance check
-     *
-     * @see #maintenance(long)
-     */
-    private final ScheduledExecutorService maintenanceExecutor
-            = createSingleThreadScheduledExecutor("VirtualCollectionRegistry-Maintenance");
-
-    @Override
-    public void afterPropertiesSet() throws VirtualCollectionRegistryException {
-        // called by Spring directly after Bean construction
-        doInitalize();
-    }
-
-    private void doInitalize() throws VirtualCollectionRegistryException {
-        if (intialized.get()) {
-            throw new VirtualCollectionRegistryException("already initialized");
-        }
-        logger.info("Initializing virtual collection registry ...");
-        try {
-            maintenanceExecutor.scheduleWithFixedDelay(new Runnable() {
-
-                @Override
-                public void run() {
-                    maintenance(new Date().getTime());
-                }
-            }, 60, 60, TimeUnit.SECONDS);
-            this.intialized.set(true);
-            logger.info("virtual collection registry successfully intialized");
-        } catch (RuntimeException e) {
-            logger.error("error initalizing virtual collection registry", e);
-            throw e;
-        }
-    }
-
-    @Override
-    public void destroy() throws VirtualCollectionRegistryException, InterruptedException {
-        logger.info("Stopping Virtual Collection Registry maintenance schedule");
-        maintenanceExecutor.shutdown();
-        if (!maintenanceExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
-            logger.warn("Timeout while waiting for maintenance thread to terminate, will try to shut down");
-        }
-
-        logger.info("Shutting down OAI provider");
-        oaiProvider.shutdown();
-    }
+public interface VirtualCollectionRegistry {
 
     /**
@@ -112,241 +19,19 @@
      * @throws VirtualCollectionRegistryException
      */
-    public long createVirtualCollection(Principal principal,
-            VirtualCollection vc) throws VirtualCollectionRegistryException {
-        if (principal == null) {
-            throw new NullPointerException("principal == null");
-        }
-        if (vc == null) {
-            throw new NullPointerException("vc == null");
-        }
+    long createVirtualCollection(Principal principal,
+            VirtualCollection vc) throws VirtualCollectionRegistryException;
 
-        logger.debug("creating virtual collection");
+    long updateVirtualCollection(Principal principal, long id,
+            VirtualCollection vc) throws VirtualCollectionRegistryException;
 
-        validator.validate(vc);
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
+    long deleteVirtualCollection(Principal principal, long id)
+            throws VirtualCollectionRegistryException;
 
-            // fetch user, if user does not exist create new
-            User user = fetchUser(em, principal);
-            if (user == null) {
-                user = new User(principal.getName());
-                em.persist(user);
-            }
-            vc.setOwner(user);
+    VirtualCollection.State getVirtualCollectionState(long id)
+            throws VirtualCollectionRegistryException;
 
-            // force new collection to be private
-            vc.setState(VirtualCollection.State.PRIVATE);
-
-            // store virtual collection
-            logger.debug("persisting new virtual collection", vc.getId());
-            em.persist(vc);
-            em.getTransaction().commit();
-            logger.debug("virtual collection created (id={})", vc.getId());
-            return vc.getId();
-        } catch (Exception e) {
-            logger.error("error while creating virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while creating virtual collection", e);
-        }
-    }
-
-    public long updateVirtualCollection(Principal principal, long id,
-            VirtualCollection vc) throws VirtualCollectionRegistryException {
-        if (principal == null) {
-            throw new NullPointerException("principal == null");
-        }
-        if (id <= 0) {
-            throw new IllegalArgumentException("id <= 0");
-        }
-        if (vc == null) {
-            throw new NullPointerException("vc == null");
-        }
-
-        logger.debug("updating virtual collection (id={})", id);
-
-        validator.validate(vc);
-
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
-            VirtualCollection c = em.find(VirtualCollection.class,
-                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
-            /*
-             * Do not check for deleted state here, as we might want to
-             * resurrect deleted virtual collections.
-             */
-            if (c == null) {
-                logger.debug("virtual collection (id={}) not found", id);
-                throw new VirtualCollectionNotFoundException(id);
-            }
-            if (!isAllowedToModify(principal, c)) {
-                throw new VirtualCollectionRegistryPermissionException(
-                        "permission denied for user \""
-                        + principal.getName() + "\"");
-            }
-
-            // update virtual collection
-            c.updateFrom(vc);
-
-            validator.validate(c);
-            em.merge(c);
-            em.getTransaction().commit();
-            logger.debug("updated virtual collection (id={})", vc.getId());
-            return c.getId();
-        } catch (VirtualCollectionRegistryException e) {
-            logger.warn("failed updating virtual collecion (id={}): {}", id,
-                    e.getMessage());
-            throw e;
-        } catch (Exception e) {
-            logger.error("error while updating virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while updating virtual collection", e);
-        }
-    }
-
-    public long deleteVirtualCollection(Principal principal, long id)
-            throws VirtualCollectionRegistryException {
-        if (principal == null) {
-            throw new NullPointerException("principal == null");
-        }
-        if (id <= 0) {
-            throw new IllegalArgumentException("id <= 0");
-        }
-
-        logger.debug("deleting virtual collection (id={})", id);
-
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
-            VirtualCollection vc = em.find(VirtualCollection.class,
-                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
-            if ((vc == null) || vc.isDeleted()) {
-                logger.debug("virtual collection (id={}) not found", id);
-                throw new VirtualCollectionNotFoundException(id);
-            }
-            if (!isAllowedToModify(principal, vc)) {
-                logger.debug("virtual collection (id={}) not owned by "
-                        + "user '{}'", id, principal.getName());
-                throw new VirtualCollectionRegistryPermissionException(
-                        "permission denied for user \""
-                        + principal.getName() + "\"");
-            }
-            if (!vc.isPrivate()) {
-                logger.debug("virtual collection (id={}) cannot be "
-                        + "deleted (invalid state)", id);
-                throw new VirtualCollectionRegistryPermissionException(
-                        "virtual collection cannot be deleted");
-            }
-            vc.setState(VirtualCollection.State.DELETED);
-            em.getTransaction().commit();
-            return vc.getId();
-        } catch (VirtualCollectionRegistryException e) {
-            logger.debug("failed deleting virtual collecion (id={}): {}", id,
-                    e.getMessage());
-            throw e;
-        } catch (Exception e) {
-            logger.error("error while deleting virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while deleting virtual collection", e);
-        }
-    }
-
-    public VirtualCollection.State getVirtualCollectionState(long id)
-            throws VirtualCollectionRegistryException {
-        if (id <= 0) {
-            throw new IllegalArgumentException("id <= 0");
-        }
-
-        logger.debug("retrieve virtual collection state (id={})", id);
-
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
-            VirtualCollection vc
-                    = em.find(VirtualCollection.class, Long.valueOf(id));
-            em.getTransaction().commit();
-            if ((vc == null) || vc.isDeleted()) {
-                logger.debug("virtual collection (id={}) not found", id);
-                throw new VirtualCollectionNotFoundException(id);
-            }
-            return vc.getState();
-        } catch (VirtualCollectionRegistryException e) {
-            throw e;
-        } catch (Exception e) {
-            logger.error(
-                    "error while retrieving state of virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while retrieving state of virtual collection", e);
-        }
-    }
-
-    public void setVirtualCollectionState(Principal principal, long id,
+    void setVirtualCollectionState(Principal principal, long id,
             VirtualCollection.State state)
-            throws VirtualCollectionRegistryException {
-        if (principal == null) {
-            throw new NullPointerException("principal == null");
-        }
-        if (id <= 0) {
-            throw new IllegalArgumentException("id <= 0");
-        }
-        if (state == null) {
-            throw new NullPointerException("state == null");
-        }
-        if ((state != VirtualCollection.State.PUBLIC_PENDING)
-                && (state != VirtualCollection.State.PRIVATE)) {
-            throw new IllegalArgumentException(
-                    "only PUBLIC_PENDING or PRIVATE are allowed");
-        }
-
-        logger.debug("setting state virtual collection state (id={}) to '{}'",
-                id, state);
-
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
-            VirtualCollection vc = em.find(VirtualCollection.class,
-                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
-            if ((vc == null) || vc.isDeleted()) {
-                logger.debug("virtual collection (id={}) not found", id);
-                throw new VirtualCollectionNotFoundException(id);
-            }
-            if (!isAllowedToModify(principal, vc)) {
-                logger.debug("virtual collection (id={}) not owned by "
-                        + "user '{}'", id, principal.getName());
-                throw new VirtualCollectionRegistryPermissionException(
-                        "permission denied for user \""
-                        + principal.getName() + "\"");
-            }
-
-            /*
-             * XXX: deny update from public to private?
-             */
-            boolean update = false;
-            switch (state) {
-                case PRIVATE:
-                    update = vc.getState() != state;
-                    break;
-                case PUBLIC_PENDING:
-                    update = vc.getState() != VirtualCollection.State.PUBLIC;
-                    break;
-                default:
-                    /* silence warning; update will stay false */
-                    break;
-            }
-            if (update) {
-                vc.setState(state);
-                em.persist(vc);
-            }
-            em.getTransaction().commit();
-        } catch (VirtualCollectionRegistryException e) {
-            throw e;
-        } catch (Exception e) {
-            logger.error(
-                    "error while setting state of virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while setting state of virtual collection", e);
-        }
-    }
+            throws VirtualCollectionRegistryException;
 
     /**
@@ -357,351 +42,22 @@
      * the specified identifier exists
      */
-    public VirtualCollection retrieveVirtualCollection(long id)
-            throws VirtualCollectionRegistryException {
-        if (id <= 0) {
-            throw new IllegalArgumentException("id <= 0");
-        }
+    VirtualCollection retrieveVirtualCollection(long id)
+            throws VirtualCollectionRegistryException;
 
-        logger.debug("retrieve virtual collection (id={})", id);
+    VirtualCollectionList getVirtualCollections(String query,
+            int offset, int count) throws VirtualCollectionRegistryException;
 
-        try {
-            EntityManager em = datastore.getEntityManager();
-            em.getTransaction().begin();
-            VirtualCollection vc
-                    = em.find(VirtualCollection.class, Long.valueOf(id));
-            em.getTransaction().commit();
-            if ((vc == null) || vc.isDeleted()) {
-                logger.debug("virtual collection (id={}) not found", id);
-                throw new VirtualCollectionNotFoundException(id);
-            }
-            logger.debug("virtual collection retrieved (id={})", id);
-            return vc;
-        } catch (VirtualCollectionRegistryException e) {
-            throw e;
-        } catch (Exception e) {
-            logger.error("error while retrieving virtual collection", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while retrieving virtual collection", e);
-        }
-    }
+    VirtualCollectionList getVirtualCollections(Principal principal,
+            String query, int offset, int count)
+            throws VirtualCollectionRegistryException;
 
-    public VirtualCollectionList getVirtualCollections(String query,
-            int offset, int count) throws VirtualCollectionRegistryException {
-        EntityManager em = datastore.getEntityManager();
-        try {
-            em.getTransaction().begin();
+    int getVirtualCollectionCount(QueryOptions options)
+            throws VirtualCollectionRegistryException;
 
-            // setup queries
-            TypedQuery<Long> cq = null;
-            TypedQuery<VirtualCollection> q = null;
-            if (query != null) {
-                ParsedQuery parsedQuery = ParsedQuery.parseQuery(query);
-                if (logger.isDebugEnabled()) {
-                    logger.debug(parsedQuery.getPrettyPrinted());
-                }
-                cq = parsedQuery.getCountQuery(em, null, VirtualCollection.State.PUBLIC);
-                q = parsedQuery.getQuery(em, null, VirtualCollection.State.PUBLIC);
-            } else {
-                cq = em.createNamedQuery("VirtualCollection.countAllPublic",
-                        Long.class);
-                q = em.createNamedQuery("VirtualCollection.findAllPublic",
-                        VirtualCollection.class);
-            }
+    List<User> getUsers();
 
-            // commence query ...
-            List<VirtualCollection> results = null;
-            long totalCount = cq.getSingleResult();
+    List<VirtualCollection> getVirtualCollections(
+            int first, int count, QueryOptions options)
+            throws VirtualCollectionRegistryException;
 
-            // optimization; don't query, if we won't get any results
-            /*
-             *  FIXME: offset == -1 is temporary hack for just fetching
-             *  total count; re-factor to have fetch-count and fetch-data
-             *  methods!
-             */
-            if ((totalCount > 0) && (offset > -1)) {
-                if (offset > 0) {
-                    q.setFirstResult(offset);
-                }
-                if (count > 0) {
-                    q.setMaxResults(count);
-                }
-                results = q.getResultList();
-            }
-            return new VirtualCollectionList(results, offset, (int) totalCount);
-        } catch (Exception e) {
-            logger.error("error while enumerating virtual collections", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while enumerating virtual collections", e);
-        } finally {
-            EntityTransaction tx = em.getTransaction();
-            if ((tx != null) && !tx.getRollbackOnly()) {
-                tx.commit();
-            }
-        }
-    }
-
-    public VirtualCollectionList getVirtualCollections(Principal principal,
-            String query, int offset, int count)
-            throws VirtualCollectionRegistryException {
-        if (principal == null) {
-            throw new NullPointerException("principal == null");
-        }
-        EntityManager em = datastore.getEntityManager();
-        try {
-            List<VirtualCollection> results = null;
-            long totalCount = 0;
-
-            em.getTransaction().begin();
-
-            /*
-             * fetch user. if user is not found, he has not yet registered any
-             * virtual collections, so just return an empty list
-             */
-            User user = fetchUser(em, principal);
-            if (user != null) {
-                // setup queries
-                TypedQuery<Long> cq = null;
-                TypedQuery<VirtualCollection> q = null;
-                if (query != null) {
-                    ParsedQuery parsedQuery = ParsedQuery.parseQuery(query);
-                    if (logger.isDebugEnabled()) {
-                        logger.debug(parsedQuery.getPrettyPrinted());
-                    }
-                    cq = parsedQuery.getCountQuery(em, user, null);
-                    q = parsedQuery.getQuery(em, user, null);
-                } else {
-                    cq = em.createNamedQuery("VirtualCollection.countByOwner",
-                            Long.class);
-                    cq.setParameter("owner", user);
-                    q = em.createNamedQuery("VirtualCollection.findByOwner",
-                            VirtualCollection.class);
-                    q.setParameter("owner", user);
-                }
-
-                // commence query ...
-                totalCount = cq.getSingleResult();
-
-                // optimization; don't query, if we won't get any results
-                /*
-                 *  FIXME: offset == -1 is temporary hack for just fetching
-                 *  total count; re-factor to have fetch-count and fetch-data
-                 *  methods!
-                 */
-                if ((totalCount > 0) && (offset > -1)) {
-                    if (offset > 0) {
-                        q.setFirstResult(offset);
-                    }
-                    if (count > 0) {
-                        q.setMaxResults(count);
-                    }
-                    results = q.getResultList();
-                }
-            }
-            return new VirtualCollectionList(results, offset, (int) totalCount);
-        } catch (Exception e) {
-            logger.error("error while enumerating virtual collections", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while enumerating virtual collections", e);
-        } finally {
-            EntityTransaction tx = em.getTransaction();
-            if ((tx != null) && !tx.getRollbackOnly()) {
-                tx.commit();
-            }
-        }
-    }
-
-    public int getVirtualCollectionCount(QueryOptions options)
-            throws VirtualCollectionRegistryException {
-        logger.trace("Getting virtual collection count");
-        EntityManager em = datastore.getEntityManager();
-        try {
-            CriteriaBuilder cb = em.getCriteriaBuilder();
-            CriteriaQuery<Long> cq = cb.createQuery(Long.class);
-            Root<VirtualCollection> root = cq.from(VirtualCollection.class);
-            if (options != null) {
-                Predicate where = options.getWhere(cb, cq, root);
-                if (where != null) {
-                    cq.where(where);
-                }
-            }
-            em.getTransaction().begin();
-            TypedQuery<Long> query
-                    = em.createQuery(cq.select(cb.count(root)));
-            final long count = query.getSingleResult();
-            if (count >= Integer.MAX_VALUE) {
-                throw new VirtualCollectionRegistryException(
-                        "resultset too large");
-            }
-            logger.trace("Counted {} collections", count);
-            return (int) count;
-        } catch (Exception e) {
-            logger.error("error while counting virtual collections", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while counting virtual collections", e);
-        } finally {
-            EntityTransaction tx = em.getTransaction();
-            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
-                tx.commit();
-            }
-        }
-    }
-
-    public List<User> getUsers() {
-        final EntityManager em = datastore.getEntityManager();
-        try {
-            final CriteriaBuilder cb = em.getCriteriaBuilder();
-            final CriteriaQuery<User> cq = cb.createQuery(User.class);
-            final Root<User> root = cq.from(User.class);
-
-            // select all users, sort by display name then name
-            cq.select(root);
-            cq.orderBy(
-                    cb.asc(root.get(User_.displayName)),
-                    cb.asc(root.get(User_.name)));
-
-            em.getTransaction().begin();
-            final TypedQuery<User> query = em.createQuery(cq);
-            return query.getResultList();
-        } finally {
-            EntityTransaction tx = em.getTransaction();
-            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
-                tx.commit();
-            }
-        }
-    }
-
-    public List<VirtualCollection> getVirtualCollections(
-            int first, int count, QueryOptions options)
-            throws VirtualCollectionRegistryException {
-        EntityManager em = datastore.getEntityManager();
-        try {
-            CriteriaBuilder cb = em.getCriteriaBuilder();
-            CriteriaQuery<VirtualCollection> cq
-                    = cb.createQuery(VirtualCollection.class);
-            Root<VirtualCollection> root = cq.from(VirtualCollection.class);
-            if (options != null) {
-                final Predicate where = options.getWhere(cb, cq, root);
-                if (where != null) {
-                    cq.where(where);
-                }
-                final Order[] order = options.getOrderBy(cb, root);
-                if (order != null) {
-                    cq.orderBy(order);
-                }
-            }
-            em.getTransaction().begin();
-            TypedQuery<VirtualCollection> query
-                    = em.createQuery(cq.select(root));
-            if (first > -1) {
-                query.setFirstResult(first);
-            }
-            if (count > 0) {
-                query.setMaxResults(count);
-            }
-            return query.getResultList();
-        } catch (Exception e) {
-            logger.error("error while fetching virtual collections", e);
-            throw new VirtualCollectionRegistryException(
-                    "error while fetching virtual collections", e);
-        } finally {
-            EntityTransaction tx = em.getTransaction();
-            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
-                tx.commit();
-            }
-        }
-    }
-
-    private void maintenance(long now) {
-        logger.debug("Maintenance check");
-        // allocate persistent identifier roughly after 30 seconds
-        final Date nowDateAlloc = new Date(now - 30 * 1000);
-        // (for now) purge deleted collection roughly after 30 seconds
-        final Date nowDatePurge = new Date(now - 30 * 1000);
-
-        EntityManager em = datastore.getEntityManager();
-        try {
-            /*
-             * delayed allocation of persistent identifier
-             */
-            em.getTransaction().begin();
-            TypedQuery<VirtualCollection> q
-                    = em.createNamedQuery("VirtualCollection.findAllByState",
-                            VirtualCollection.class);
-            q.setParameter("state", VirtualCollection.State.PUBLIC_PENDING);
-            q.setParameter("date", nowDateAlloc);
-            q.setLockMode(LockModeType.PESSIMISTIC_WRITE);
-            for (VirtualCollection vc : q.getResultList()) {
-                if (vc.getPersistentIdentifier() == null) {
-                    PersistentIdentifier pid = pid_provider.createIdentifier(vc);
-                    vc.setPersistentIdentifier(pid);
-                }
-                vc.setState(VirtualCollection.State.PUBLIC);
-                em.persist(vc);
-                logger.info("assigned pid (identifer='{}') to virtual"
-                        + "collection (id={})",
-                        vc.getPersistentIdentifier().getIdentifier(),
-                        vc.getId());
-            }
-            em.getTransaction().commit();
-
-            /*
-             * delayed purging of deleted virtual collections
-             */
-            em.getTransaction().begin();
-            q.setParameter("state", VirtualCollection.State.DELETED);
-            q.setParameter("date", nowDatePurge);
-            q.setLockMode(LockModeType.PESSIMISTIC_WRITE);
-            for (VirtualCollection vc : q.getResultList()) {
-                vc.setState(VirtualCollection.State.DEAD);
-                em.remove(vc);
-                logger.debug("purged virtual collection (id={})", vc.getId());
-            }
-            em.getTransaction().commit();
-        } catch (VirtualCollectionRegistryException e) {
-            logger.error("error while doing maintenance", e);
-        } finally {
-            datastore.closeEntityManager();
-        }
-    }
-
-    private static User fetchUser(EntityManager em, Principal principal) {
-        User user = null;
-        try {
-            TypedQuery<User> q
-                    = em.createNamedQuery("User.findByName", User.class);
-            q.setParameter("name", principal.getName());
-            user = q.getSingleResult();
-        } catch (NoResultException e) {
-            /* IGNORE */
-        }
-        return user;
-    }
-
-    /**
-     * Creates a single thread scheduled executor with the specified thread name
-     *
-     * @param threadName name for new executor threads
-     * @return
-     */
-    private static ScheduledExecutorService createSingleThreadScheduledExecutor(final String threadName) {
-        return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
-            // decorate default thread factory so that we can provide a 
-            // custom thread name
-            final AtomicInteger i = new AtomicInteger(0);
-
-            @Override
-            public Thread newThread(Runnable r) {
-                final Thread thread = Executors.defaultThreadFactory().newThread(r);
-                thread.setName(threadName + "-" + i.addAndGet(1));
-                return thread;
-            }
-        });
-    }
-
-    private boolean isAllowedToModify(Principal principal, VirtualCollection c) {
-        // admin and owner are allowed to modify collections
-        return adminUsersService.isAdmin(principal.getName())
-                || c.getOwner().equalsPrincipal(principal);
-    }
-
-} // class VirtualCollectionRegistry
+} // interface VirtualCollectionRegistry
Index: /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistryImpl.java
===================================================================
--- /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistryImpl.java	(revision 5616)
+++ /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/VirtualCollectionRegistryImpl.java	(revision 5616)
@@ -0,0 +1,707 @@
+package eu.clarin.cmdi.virtualcollectionregistry;
+
+import eu.clarin.cmdi.oai.provider.impl.OAIProvider;
+import eu.clarin.cmdi.virtualcollectionregistry.model.User;
+import eu.clarin.cmdi.virtualcollectionregistry.model.User_;
+import eu.clarin.cmdi.virtualcollectionregistry.model.VirtualCollection;
+import eu.clarin.cmdi.virtualcollectionregistry.model.VirtualCollectionList;
+import eu.clarin.cmdi.virtualcollectionregistry.pid.PersistentIdentifier;
+import eu.clarin.cmdi.virtualcollectionregistry.pid.PersistentIdentifierProvider;
+import eu.clarin.cmdi.virtualcollectionregistry.query.ParsedQuery;
+import eu.clarin.cmdi.virtualcollectionregistry.service.VirtualCollectionValidator;
+import java.security.Principal;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.persistence.EntityManager;
+import javax.persistence.EntityTransaction;
+import javax.persistence.LockModeType;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Order;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+@Service
+public class VirtualCollectionRegistryImpl implements VirtualCollectionRegistry, InitializingBean, DisposableBean {
+
+    @Autowired
+    private DataStore datastore; //TODO: replace with Spring managed EM?
+    @Autowired
+    private PersistentIdentifierProvider pid_provider;
+    @Autowired
+    private OAIProvider oaiProvider;
+    @Autowired
+    @Qualifier("creation")
+    private VirtualCollectionValidator validator;
+    @Autowired
+    private AdminUsersService adminUsersService;
+
+    private static final Logger logger
+            = LoggerFactory.getLogger(VirtualCollectionRegistryImpl.class);
+    private final AtomicBoolean intialized = new AtomicBoolean(false);
+    /**
+     * Scheduled executor service for the maintenance check
+     *
+     * @see #maintenance(long)
+     */
+    private final ScheduledExecutorService maintenanceExecutor
+            = createSingleThreadScheduledExecutor("VirtualCollectionRegistry-Maintenance");
+
+    @Override
+    public void afterPropertiesSet() throws VirtualCollectionRegistryException {
+        // called by Spring directly after Bean construction
+        doInitalize();
+    }
+
+    private void doInitalize() throws VirtualCollectionRegistryException {
+        if (intialized.get()) {
+            throw new VirtualCollectionRegistryException("already initialized");
+        }
+        logger.info("Initializing virtual collection registry ...");
+        try {
+            maintenanceExecutor.scheduleWithFixedDelay(new Runnable() {
+
+                @Override
+                public void run() {
+                    maintenance(new Date().getTime());
+                }
+            }, 60, 60, TimeUnit.SECONDS);
+            this.intialized.set(true);
+            logger.info("virtual collection registry successfully intialized");
+        } catch (RuntimeException e) {
+            logger.error("error initalizing virtual collection registry", e);
+            throw e;
+        }
+    }
+
+    @Override
+    public void destroy() throws VirtualCollectionRegistryException, InterruptedException {
+        logger.info("Stopping Virtual Collection Registry maintenance schedule");
+        maintenanceExecutor.shutdown();
+        if (!maintenanceExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+            logger.warn("Timeout while waiting for maintenance thread to terminate, will try to shut down");
+        }
+
+        logger.info("Shutting down OAI provider");
+        oaiProvider.shutdown();
+    }
+
+    /**
+     * Will store the specified collection; it will also set the owner according
+     * to the specified principal and set its state to
+     * {@link VirtualCollection.State#PRIVATE}
+     *
+     * @param principal owner principal
+     * @param vc collection to store
+     * @return identifier of the persisted collection
+     * @throws VirtualCollectionRegistryException
+     */
+    public long createVirtualCollection(Principal principal,
+            VirtualCollection vc) throws VirtualCollectionRegistryException {
+        if (principal == null) {
+            throw new NullPointerException("principal == null");
+        }
+        if (vc == null) {
+            throw new NullPointerException("vc == null");
+        }
+
+        logger.debug("creating virtual collection");
+
+        validator.validate(vc);
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+
+            // fetch user, if user does not exist create new
+            User user = fetchUser(em, principal);
+            if (user == null) {
+                user = new User(principal.getName());
+                em.persist(user);
+            }
+            vc.setOwner(user);
+
+            // force new collection to be private
+            vc.setState(VirtualCollection.State.PRIVATE);
+
+            // store virtual collection
+            logger.debug("persisting new virtual collection", vc.getId());
+            em.persist(vc);
+            em.getTransaction().commit();
+            logger.debug("virtual collection created (id={})", vc.getId());
+            return vc.getId();
+        } catch (Exception e) {
+            logger.error("error while creating virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while creating virtual collection", e);
+        }
+    }
+
+    public long updateVirtualCollection(Principal principal, long id,
+            VirtualCollection vc) throws VirtualCollectionRegistryException {
+        if (principal == null) {
+            throw new NullPointerException("principal == null");
+        }
+        if (id <= 0) {
+            throw new IllegalArgumentException("id <= 0");
+        }
+        if (vc == null) {
+            throw new NullPointerException("vc == null");
+        }
+
+        logger.debug("updating virtual collection (id={})", id);
+
+        validator.validate(vc);
+
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+            VirtualCollection c = em.find(VirtualCollection.class,
+                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
+            /*
+             * Do not check for deleted state here, as we might want to
+             * resurrect deleted virtual collections.
+             */
+            if (c == null) {
+                logger.debug("virtual collection (id={}) not found", id);
+                throw new VirtualCollectionNotFoundException(id);
+            }
+            if (!isAllowedToModify(principal, c)) {
+                throw new VirtualCollectionRegistryPermissionException(
+                        "permission denied for user \""
+                        + principal.getName() + "\"");
+            }
+
+            // update virtual collection
+            c.updateFrom(vc);
+
+            validator.validate(c);
+            em.merge(c);
+            em.getTransaction().commit();
+            logger.debug("updated virtual collection (id={})", vc.getId());
+            return c.getId();
+        } catch (VirtualCollectionRegistryException e) {
+            logger.warn("failed updating virtual collecion (id={}): {}", id,
+                    e.getMessage());
+            throw e;
+        } catch (Exception e) {
+            logger.error("error while updating virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while updating virtual collection", e);
+        }
+    }
+
+    public long deleteVirtualCollection(Principal principal, long id)
+            throws VirtualCollectionRegistryException {
+        if (principal == null) {
+            throw new NullPointerException("principal == null");
+        }
+        if (id <= 0) {
+            throw new IllegalArgumentException("id <= 0");
+        }
+
+        logger.debug("deleting virtual collection (id={})", id);
+
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+            VirtualCollection vc = em.find(VirtualCollection.class,
+                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
+            if ((vc == null) || vc.isDeleted()) {
+                logger.debug("virtual collection (id={}) not found", id);
+                throw new VirtualCollectionNotFoundException(id);
+            }
+            if (!isAllowedToModify(principal, vc)) {
+                logger.debug("virtual collection (id={}) not owned by "
+                        + "user '{}'", id, principal.getName());
+                throw new VirtualCollectionRegistryPermissionException(
+                        "permission denied for user \""
+                        + principal.getName() + "\"");
+            }
+            if (!vc.isPrivate()) {
+                logger.debug("virtual collection (id={}) cannot be "
+                        + "deleted (invalid state)", id);
+                throw new VirtualCollectionRegistryPermissionException(
+                        "virtual collection cannot be deleted");
+            }
+            vc.setState(VirtualCollection.State.DELETED);
+            em.getTransaction().commit();
+            return vc.getId();
+        } catch (VirtualCollectionRegistryException e) {
+            logger.debug("failed deleting virtual collecion (id={}): {}", id,
+                    e.getMessage());
+            throw e;
+        } catch (Exception e) {
+            logger.error("error while deleting virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while deleting virtual collection", e);
+        }
+    }
+
+    public VirtualCollection.State getVirtualCollectionState(long id)
+            throws VirtualCollectionRegistryException {
+        if (id <= 0) {
+            throw new IllegalArgumentException("id <= 0");
+        }
+
+        logger.debug("retrieve virtual collection state (id={})", id);
+
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+            VirtualCollection vc
+                    = em.find(VirtualCollection.class, Long.valueOf(id));
+            em.getTransaction().commit();
+            if ((vc == null) || vc.isDeleted()) {
+                logger.debug("virtual collection (id={}) not found", id);
+                throw new VirtualCollectionNotFoundException(id);
+            }
+            return vc.getState();
+        } catch (VirtualCollectionRegistryException e) {
+            throw e;
+        } catch (Exception e) {
+            logger.error(
+                    "error while retrieving state of virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while retrieving state of virtual collection", e);
+        }
+    }
+
+    public void setVirtualCollectionState(Principal principal, long id,
+            VirtualCollection.State state)
+            throws VirtualCollectionRegistryException {
+        if (principal == null) {
+            throw new NullPointerException("principal == null");
+        }
+        if (id <= 0) {
+            throw new IllegalArgumentException("id <= 0");
+        }
+        if (state == null) {
+            throw new NullPointerException("state == null");
+        }
+        if ((state != VirtualCollection.State.PUBLIC_PENDING)
+                && (state != VirtualCollection.State.PRIVATE)) {
+            throw new IllegalArgumentException(
+                    "only PUBLIC_PENDING or PRIVATE are allowed");
+        }
+
+        logger.debug("setting state virtual collection state (id={}) to '{}'",
+                id, state);
+
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+            VirtualCollection vc = em.find(VirtualCollection.class,
+                    Long.valueOf(id), LockModeType.PESSIMISTIC_WRITE);
+            if ((vc == null) || vc.isDeleted()) {
+                logger.debug("virtual collection (id={}) not found", id);
+                throw new VirtualCollectionNotFoundException(id);
+            }
+            if (!isAllowedToModify(principal, vc)) {
+                logger.debug("virtual collection (id={}) not owned by "
+                        + "user '{}'", id, principal.getName());
+                throw new VirtualCollectionRegistryPermissionException(
+                        "permission denied for user \""
+                        + principal.getName() + "\"");
+            }
+
+            /*
+             * XXX: deny update from public to private?
+             */
+            boolean update = false;
+            switch (state) {
+                case PRIVATE:
+                    update = vc.getState() != state;
+                    break;
+                case PUBLIC_PENDING:
+                    update = vc.getState() != VirtualCollection.State.PUBLIC;
+                    break;
+                default:
+                    /* silence warning; update will stay false */
+                    break;
+            }
+            if (update) {
+                vc.setState(state);
+                em.persist(vc);
+            }
+            em.getTransaction().commit();
+        } catch (VirtualCollectionRegistryException e) {
+            throw e;
+        } catch (Exception e) {
+            logger.error(
+                    "error while setting state of virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while setting state of virtual collection", e);
+        }
+    }
+
+    /**
+     *
+     * @param id identifier of the virtual collection to retrieve
+     * @return the identified virtual collection, never null
+     * @throws VirtualCollectionRegistryException if no virtual collection with
+     * the specified identifier exists
+     */
+    public VirtualCollection retrieveVirtualCollection(long id)
+            throws VirtualCollectionRegistryException {
+        if (id <= 0) {
+            throw new IllegalArgumentException("id <= 0");
+        }
+
+        logger.debug("retrieve virtual collection (id={})", id);
+
+        try {
+            EntityManager em = datastore.getEntityManager();
+            em.getTransaction().begin();
+            VirtualCollection vc
+                    = em.find(VirtualCollection.class, Long.valueOf(id));
+            em.getTransaction().commit();
+            if ((vc == null) || vc.isDeleted()) {
+                logger.debug("virtual collection (id={}) not found", id);
+                throw new VirtualCollectionNotFoundException(id);
+            }
+            logger.debug("virtual collection retrieved (id={})", id);
+            return vc;
+        } catch (VirtualCollectionRegistryException e) {
+            throw e;
+        } catch (Exception e) {
+            logger.error("error while retrieving virtual collection", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while retrieving virtual collection", e);
+        }
+    }
+
+    public VirtualCollectionList getVirtualCollections(String query,
+            int offset, int count) throws VirtualCollectionRegistryException {
+        EntityManager em = datastore.getEntityManager();
+        try {
+            em.getTransaction().begin();
+
+            // setup queries
+            TypedQuery<Long> cq = null;
+            TypedQuery<VirtualCollection> q = null;
+            if (query != null) {
+                ParsedQuery parsedQuery = ParsedQuery.parseQuery(query);
+                if (logger.isDebugEnabled()) {
+                    logger.debug(parsedQuery.getPrettyPrinted());
+                }
+                cq = parsedQuery.getCountQuery(em, null, VirtualCollection.State.PUBLIC);
+                q = parsedQuery.getQuery(em, null, VirtualCollection.State.PUBLIC);
+            } else {
+                cq = em.createNamedQuery("VirtualCollection.countAllPublic",
+                        Long.class);
+                q = em.createNamedQuery("VirtualCollection.findAllPublic",
+                        VirtualCollection.class);
+            }
+
+            // commence query ...
+            List<VirtualCollection> results = null;
+            long totalCount = cq.getSingleResult();
+
+            // optimization; don't query, if we won't get any results
+            /*
+             *  FIXME: offset == -1 is temporary hack for just fetching
+             *  total count; re-factor to have fetch-count and fetch-data
+             *  methods!
+             */
+            if ((totalCount > 0) && (offset > -1)) {
+                if (offset > 0) {
+                    q.setFirstResult(offset);
+                }
+                if (count > 0) {
+                    q.setMaxResults(count);
+                }
+                results = q.getResultList();
+            }
+            return new VirtualCollectionList(results, offset, (int) totalCount);
+        } catch (Exception e) {
+            logger.error("error while enumerating virtual collections", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while enumerating virtual collections", e);
+        } finally {
+            EntityTransaction tx = em.getTransaction();
+            if ((tx != null) && !tx.getRollbackOnly()) {
+                tx.commit();
+            }
+        }
+    }
+
+    public VirtualCollectionList getVirtualCollections(Principal principal,
+            String query, int offset, int count)
+            throws VirtualCollectionRegistryException {
+        if (principal == null) {
+            throw new NullPointerException("principal == null");
+        }
+        EntityManager em = datastore.getEntityManager();
+        try {
+            List<VirtualCollection> results = null;
+            long totalCount = 0;
+
+            em.getTransaction().begin();
+
+            /*
+             * fetch user. if user is not found, he has not yet registered any
+             * virtual collections, so just return an empty list
+             */
+            User user = fetchUser(em, principal);
+            if (user != null) {
+                // setup queries
+                TypedQuery<Long> cq = null;
+                TypedQuery<VirtualCollection> q = null;
+                if (query != null) {
+                    ParsedQuery parsedQuery = ParsedQuery.parseQuery(query);
+                    if (logger.isDebugEnabled()) {
+                        logger.debug(parsedQuery.getPrettyPrinted());
+                    }
+                    cq = parsedQuery.getCountQuery(em, user, null);
+                    q = parsedQuery.getQuery(em, user, null);
+                } else {
+                    cq = em.createNamedQuery("VirtualCollection.countByOwner",
+                            Long.class);
+                    cq.setParameter("owner", user);
+                    q = em.createNamedQuery("VirtualCollection.findByOwner",
+                            VirtualCollection.class);
+                    q.setParameter("owner", user);
+                }
+
+                // commence query ...
+                totalCount = cq.getSingleResult();
+
+                // optimization; don't query, if we won't get any results
+                /*
+                 *  FIXME: offset == -1 is temporary hack for just fetching
+                 *  total count; re-factor to have fetch-count and fetch-data
+                 *  methods!
+                 */
+                if ((totalCount > 0) && (offset > -1)) {
+                    if (offset > 0) {
+                        q.setFirstResult(offset);
+                    }
+                    if (count > 0) {
+                        q.setMaxResults(count);
+                    }
+                    results = q.getResultList();
+                }
+            }
+            return new VirtualCollectionList(results, offset, (int) totalCount);
+        } catch (Exception e) {
+            logger.error("error while enumerating virtual collections", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while enumerating virtual collections", e);
+        } finally {
+            EntityTransaction tx = em.getTransaction();
+            if ((tx != null) && !tx.getRollbackOnly()) {
+                tx.commit();
+            }
+        }
+    }
+
+    public int getVirtualCollectionCount(QueryOptions options)
+            throws VirtualCollectionRegistryException {
+        logger.trace("Getting virtual collection count");
+        EntityManager em = datastore.getEntityManager();
+        try {
+            CriteriaBuilder cb = em.getCriteriaBuilder();
+            CriteriaQuery<Long> cq = cb.createQuery(Long.class);
+            Root<VirtualCollection> root = cq.from(VirtualCollection.class);
+            if (options != null) {
+                Predicate where = options.getWhere(cb, cq, root);
+                if (where != null) {
+                    cq.where(where);
+                }
+            }
+            em.getTransaction().begin();
+            TypedQuery<Long> query
+                    = em.createQuery(cq.select(cb.count(root)));
+            final long count = query.getSingleResult();
+            if (count >= Integer.MAX_VALUE) {
+                throw new VirtualCollectionRegistryException(
+                        "resultset too large");
+            }
+            logger.trace("Counted {} collections", count);
+            return (int) count;
+        } catch (Exception e) {
+            logger.error("error while counting virtual collections", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while counting virtual collections", e);
+        } finally {
+            EntityTransaction tx = em.getTransaction();
+            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
+                tx.commit();
+            }
+        }
+    }
+
+    public List<User> getUsers() {
+        final EntityManager em = datastore.getEntityManager();
+        try {
+            final CriteriaBuilder cb = em.getCriteriaBuilder();
+            final CriteriaQuery<User> cq = cb.createQuery(User.class);
+            final Root<User> root = cq.from(User.class);
+
+            // select all users, sort by display name then name
+            cq.select(root);
+            cq.orderBy(
+                    cb.asc(root.get(User_.displayName)),
+                    cb.asc(root.get(User_.name)));
+
+            em.getTransaction().begin();
+            final TypedQuery<User> query = em.createQuery(cq);
+            return query.getResultList();
+        } finally {
+            EntityTransaction tx = em.getTransaction();
+            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
+                tx.commit();
+            }
+        }
+    }
+
+    public List<VirtualCollection> getVirtualCollections(
+            int first, int count, QueryOptions options)
+            throws VirtualCollectionRegistryException {
+        EntityManager em = datastore.getEntityManager();
+        try {
+            CriteriaBuilder cb = em.getCriteriaBuilder();
+            CriteriaQuery<VirtualCollection> cq
+                    = cb.createQuery(VirtualCollection.class);
+            Root<VirtualCollection> root = cq.from(VirtualCollection.class);
+            if (options != null) {
+                final Predicate where = options.getWhere(cb, cq, root);
+                if (where != null) {
+                    cq.where(where);
+                }
+                final Order[] order = options.getOrderBy(cb, root);
+                if (order != null) {
+                    cq.orderBy(order);
+                }
+            }
+            em.getTransaction().begin();
+            TypedQuery<VirtualCollection> query
+                    = em.createQuery(cq.select(root));
+            if (first > -1) {
+                query.setFirstResult(first);
+            }
+            if (count > 0) {
+                query.setMaxResults(count);
+            }
+            return query.getResultList();
+        } catch (Exception e) {
+            logger.error("error while fetching virtual collections", e);
+            throw new VirtualCollectionRegistryException(
+                    "error while fetching virtual collections", e);
+        } finally {
+            EntityTransaction tx = em.getTransaction();
+            if ((tx != null) && tx.isActive() && !tx.getRollbackOnly()) {
+                tx.commit();
+            }
+        }
+    }
+
+    private void maintenance(long now) {
+        logger.debug("Maintenance check");
+        // allocate persistent identifier roughly after 30 seconds
+        final Date nowDateAlloc = new Date(now - 30 * 1000);
+        // (for now) purge deleted collection roughly after 30 seconds
+        final Date nowDatePurge = new Date(now - 30 * 1000);
+
+        EntityManager em = datastore.getEntityManager();
+        try {
+            /*
+             * delayed allocation of persistent identifier
+             */
+            em.getTransaction().begin();
+            TypedQuery<VirtualCollection> q
+                    = em.createNamedQuery("VirtualCollection.findAllByState",
+                            VirtualCollection.class);
+            q.setParameter("state", VirtualCollection.State.PUBLIC_PENDING);
+            q.setParameter("date", nowDateAlloc);
+            q.setLockMode(LockModeType.PESSIMISTIC_WRITE);
+            for (VirtualCollection vc : q.getResultList()) {
+                if (vc.getPersistentIdentifier() == null) {
+                    PersistentIdentifier pid = pid_provider.createIdentifier(vc);
+                    vc.setPersistentIdentifier(pid);
+                }
+                vc.setState(VirtualCollection.State.PUBLIC);
+                em.persist(vc);
+                logger.info("assigned pid (identifer='{}') to virtual"
+                        + "collection (id={})",
+                        vc.getPersistentIdentifier().getIdentifier(),
+                        vc.getId());
+            }
+            em.getTransaction().commit();
+
+            /*
+             * delayed purging of deleted virtual collections
+             */
+            em.getTransaction().begin();
+            q.setParameter("state", VirtualCollection.State.DELETED);
+            q.setParameter("date", nowDatePurge);
+            q.setLockMode(LockModeType.PESSIMISTIC_WRITE);
+            for (VirtualCollection vc : q.getResultList()) {
+                vc.setState(VirtualCollection.State.DEAD);
+                em.remove(vc);
+                logger.debug("purged virtual collection (id={})", vc.getId());
+            }
+            em.getTransaction().commit();
+        } catch (VirtualCollectionRegistryException e) {
+            logger.error("error while doing maintenance", e);
+        } finally {
+            datastore.closeEntityManager();
+        }
+    }
+
+    private static User fetchUser(EntityManager em, Principal principal) {
+        User user = null;
+        try {
+            TypedQuery<User> q
+                    = em.createNamedQuery("User.findByName", User.class);
+            q.setParameter("name", principal.getName());
+            user = q.getSingleResult();
+        } catch (NoResultException e) {
+            /* IGNORE */
+        }
+        return user;
+    }
+
+    /**
+     * Creates a single thread scheduled executor with the specified thread name
+     *
+     * @param threadName name for new executor threads
+     * @return
+     */
+    private static ScheduledExecutorService createSingleThreadScheduledExecutor(final String threadName) {
+        return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+            // decorate default thread factory so that we can provide a 
+            // custom thread name
+            final AtomicInteger i = new AtomicInteger(0);
+
+            @Override
+            public Thread newThread(Runnable r) {
+                final Thread thread = Executors.defaultThreadFactory().newThread(r);
+                thread.setName(threadName + "-" + i.addAndGet(1));
+                return thread;
+            }
+        });
+    }
+
+    private boolean isAllowedToModify(Principal principal, VirtualCollection c) {
+        // admin and owner are allowed to modify collections
+        return adminUsersService.isAdmin(principal.getName())
+                || c.getOwner().equalsPrincipal(principal);
+    }
+
+} // class VirtualCollectionRegistry
Index: /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResource.java
===================================================================
--- /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResource.java	(revision 5615)
+++ /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/main/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResource.java	(revision 5616)
@@ -69,4 +69,16 @@
     }
 
+    // for testing
+    protected VirtualCollectionResource(VirtualCollectionRegistry registry, VirtualCollectionMarshaller marshaller, SecurityContext security, HttpHeaders headers, UriInfo uriInfo, Long id) {
+        this.registry = registry;
+        this.marshaller = marshaller;
+        this.security = security;
+        this.headers = headers;
+        this.uriInfo = uriInfo;
+        this.id = id;
+    }
+
+    
+    
     /**
      * Sets the id for this resource; should be called exactly once per
@@ -104,6 +116,6 @@
             // exclude CMDI from the options and check if this is ok for request
             final List<Variant> variants = Variant.mediaTypes(
-                    MediaType.TEXT_XML_TYPE, 
-                    MediaType.APPLICATION_XML_TYPE, 
+                    MediaType.TEXT_XML_TYPE,
+                    MediaType.APPLICATION_XML_TYPE,
                     MediaType.APPLICATION_JSON_TYPE).add().build();
             final Variant selectVariant = request.selectVariant(variants);
@@ -212,5 +224,5 @@
             throws VirtualCollectionRegistryException {
         VirtualCollection.State state = registry.getVirtualCollectionState(id);
-        State result = null;
+        final State result;
         switch (state) {
             case PUBLIC_PENDING:
Index: /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/test/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResourceTest.java
===================================================================
--- /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/test/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResourceTest.java	(revision 5616)
+++ /VirtualCollectionRegistry/trunk/VirtualCollectionRegistry/src/test/java/eu/clarin/cmdi/virtualcollectionregistry/rest/VirtualCollectionResourceTest.java	(revision 5616)
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2014 CLARIN
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package eu.clarin.cmdi.virtualcollectionregistry.rest;
+
+import com.sun.jersey.api.uri.UriBuilderImpl;
+import eu.clarin.cmdi.virtualcollectionregistry.VirtualCollectionRegistry;
+import eu.clarin.cmdi.virtualcollectionregistry.model.VirtualCollection;
+import eu.clarin.cmdi.virtualcollectionregistry.pid.PersistentIdentifier;
+import eu.clarin.cmdi.virtualcollectionregistry.service.VirtualCollectionMarshaller;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.Principal;
+import java.util.List;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Variant;
+import static org.hamcrest.Matchers.*;
+import org.jmock.Expectations;
+import static org.jmock.Expectations.equal;
+import static org.jmock.Expectations.returnValue;
+import org.jmock.Mockery;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ * @author twagoo
+ */
+public class VirtualCollectionResourceTest {
+
+    private final static long ID = 123L;
+
+    private Mockery context = new JUnit4Mockery();
+
+    private VirtualCollectionResource instance;
+    private VirtualCollectionRegistry registry;
+    private VirtualCollectionMarshaller marshaller;
+
+    private UriInfo uriInfo;
+    private HttpHeaders headers;
+    private SecurityContext security;
+
+    @Before
+    public void setUp() throws Exception {
+        registry = context.mock(VirtualCollectionRegistry.class);
+        marshaller = context.mock(VirtualCollectionMarshaller.class);
+        uriInfo = context.mock(UriInfo.class);
+        headers = context.mock(HttpHeaders.class);
+        security = context.mock(SecurityContext.class);
+        instance = new VirtualCollectionResource(registry, marshaller, security, headers, uriInfo, ID);
+    }
+
+    /**
+     * Test of getVirtualCollection method, of class VirtualCollectionResource.
+     */
+    @Test
+    public void testGetPublicVirtualCollection() throws Exception {
+        final Request request = context.mock(Request.class);
+
+        // create a public VC to have returned by the registry service
+        final VirtualCollection vc = new VirtualCollection();
+        vc.setId(ID);
+        vc.setName("Test VC");
+        vc.setState(VirtualCollection.State.PUBLIC_PENDING);
+        // set PID and set state to public
+        vc.setPersistentIdentifier(new PersistentIdentifier(vc, PersistentIdentifier.Type.DUMMY, "PID"));
+
+        context.checking(new Expectations() {
+            {
+                oneOf(registry).retrieveVirtualCollection(ID);
+                will(returnValue(vc));
+            }
+        });
+        final Response response = instance.getVirtualCollection(request);
+        assertEquals(200, response.getStatus());
+        assertEquals(vc, response.getEntity());
+    }
+
+    /**
+     * Test of getVirtualCollection method, of class VirtualCollectionResource.
+     */
+    @Test
+    public void testGetPrivateVirtualCollection() throws Exception {
+        final Request request = context.mock(Request.class);
+
+        // create a public VC to have returned by the registry service
+        final VirtualCollection vc = new VirtualCollection();
+        vc.setId(ID);
+        vc.setName("Test VC");
+        vc.setState(VirtualCollection.State.PRIVATE);
+
+        // prepare variants for content negotiation
+        final List<Variant> variants = Variant.mediaTypes(MediaType.APPLICATION_XML_TYPE).add().build();
+
+        context.checking(new Expectations() {
+            {
+                oneOf(registry).retrieveVirtualCollection(ID);
+                will(returnValue(vc));
+                // it's private, so service will check whether non-CMDI XML is acceptable
+                exactly(1).of(equal(request))
+                        .method("selectVariant").with(hasItems(variants.toArray()));
+                // we will accept XML
+                will(returnValue(variants.get(0)));
+            }
+        });
+        final Response response = instance.getVirtualCollection(request);
+        assertEquals(200, response.getStatus());
+        assertEquals(vc, response.getEntity());
+    }
+
+    /**
+     * Test of getVirtualCollectionDetailsRedirect method, of class
+     * VirtualCollectionResource.
+     */
+    @Test
+    public void testGetVirtualCollectionDetailsRedirect() throws Exception {
+        context.checking(new Expectations() {
+            {
+                oneOf(uriInfo).getBaseUriBuilder();
+                will(returnValue(new UriBuilderImpl().path("http://server/vcr")));
+            }
+        });
+        final Response response = instance.getVirtualCollectionDetailsRedirect();
+        assertEquals(303, response.getStatus());
+        final List<Object> location = response.getMetadata().get("Location");
+        assertFalse(location.isEmpty());
+        assertEquals(URI.create("http://server/vcr/../app/details/" + ID), location.get(0));
+    }
+
+    /**
+     * Test of updateVirtualCollection method, of class
+     * VirtualCollectionResource.
+     */
+    @Test
+    public void testUpdateVirtualCollection() throws Exception {
+        // principal and input stream stubs (usually provided by client via Jersey)
+        final Principal principal = context.mock(Principal.class);
+        final InputStream input = new ByteArrayInputStream(new byte[0]);
+
+        // create a public VC to have returned by the marshaller
+        final VirtualCollection vc = new VirtualCollection();
+        vc.setId(ID);
+        vc.setName("Test VC");
+        vc.setState(VirtualCollection.State.PRIVATE);
+
+        context.checking(new Expectations() {
+            {
+                oneOf(security).getUserPrincipal();
+                will(returnValue(principal));
+                allowing(headers).getMediaType();
+                will(returnValue(MediaType.APPLICATION_XML_TYPE));
+                oneOf(marshaller).unmarshal(input, VirtualCollectionMarshaller.Format.XML, "utf-8");
+                will(returnValue(vc));
+                oneOf(registry).updateVirtualCollection(principal, ID, vc);
+                will(returnValue(ID));
+            }
+        });
+
+        final Response result = instance.updateVirtualCollection(input);
+        assertEquals(200, result.getStatus());
+    }
+
+    /**
+     * Test of deleteVirtualCollection method, of class
+     * VirtualCollectionResource.
+     */
+    @Test
+    public void testDeleteVirtualCollection() throws Exception {
+        // principal stream stub (usually provided by client via Jersey)
+        final Principal principal = context.mock(Principal.class);
+
+        context.checking(new Expectations() {
+            {
+                oneOf(security).getUserPrincipal();
+                will(returnValue(principal));
+                oneOf(registry).deleteVirtualCollection(principal, ID);
+                will(returnValue(ID));
+            }
+        });
+
+        final Response result = instance.deleteVirtualCollection();
+        assertEquals(200, result.getStatus());
+    }
+
+    /**
+     * Test of getVirtualCollectionState method, of class
+     * VirtualCollectionResource.
+     */
+    @Test
+    public void testGetVirtualCollectionState() throws Exception {
+        // private state
+        context.checking(new Expectations() {
+            {
+                oneOf(registry).getVirtualCollectionState(ID);
+                will(returnValue(VirtualCollection.State.PRIVATE));
+            }
+        });
+        Response result = instance.getVirtualCollectionState();
+        assertEquals(200, result.getStatus());
+        assertEquals(State.PRIVATE, result.getEntity());
+
+        // public state
+        context.checking(new Expectations() {
+            {
+                oneOf(registry).getVirtualCollectionState(ID);
+                will(returnValue(VirtualCollection.State.PUBLIC));
+            }
+        });
+        result = instance.getVirtualCollectionState();
+        assertEquals(200, result.getStatus());
+        assertEquals(State.PUBLIC, result.getEntity());
+    }
+
+    /**
+     * Test of setVirtualCollectionState method, of class
+     * VirtualCollectionResource.
+     */
+    @Test
+    public void testSetVirtualCollectionState() throws Exception {
+        // principal stream stub (usually provided by client via Jersey)
+        final Principal principal = context.mock(Principal.class);
+
+        // set to private
+        context.checking(new Expectations() {
+            {
+                allowing(security).getUserPrincipal();
+                will(returnValue(principal));
+
+                oneOf(registry).setVirtualCollectionState(principal, ID, VirtualCollection.State.PRIVATE);
+            }
+        });
+        Response result = instance.setVirtualCollectionState(ID, State.PRIVATE);
+        assertEquals(200, result.getStatus());
+
+        // set to public
+        context.checking(new Expectations() {
+            {
+                // setting public status will *mark* it for publication, so the
+                // actual status will be 'public pending' initially
+                oneOf(registry).setVirtualCollectionState(principal, ID, VirtualCollection.State.PUBLIC_PENDING);
+            }
+        });
+        result = instance.setVirtualCollectionState(ID, State.PUBLIC);
+        assertEquals(200, result.getStatus());
+    }
+
+}
