ch.entwine.weblounge.contentrepository.impl.AbstractWritableContentRepository.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.contentrepository.impl.AbstractWritableContentRepository.java

Source

/*
 *  Weblounge: Web Content Management System
 *  Copyright (c) 2003 - 2011 The Weblounge Team
 *  http://entwinemedia.com/weblounge
 *
 *  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 2
 *  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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software Foundation
 *  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package ch.entwine.weblounge.contentrepository.impl;

import static ch.entwine.weblounge.common.content.ResourceUtils.equalsByIdOrPath;
import static ch.entwine.weblounge.contentrepository.impl.index.IndexSchema.PATH;

import ch.entwine.weblounge.cache.ResponseCacheTracker;
import ch.entwine.weblounge.common.content.MalformedResourceURIException;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceContent;
import ch.entwine.weblounge.common.content.ResourceMetadata;
import ch.entwine.weblounge.common.content.ResourceReader;
import ch.entwine.weblounge.common.content.ResourceSearchResultItem;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.SearchQuery;
import ch.entwine.weblounge.common.content.SearchResult;
import ch.entwine.weblounge.common.content.SearchResultItem;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.impl.content.ResourceURIImpl;
import ch.entwine.weblounge.common.impl.content.SearchQueryImpl;
import ch.entwine.weblounge.common.impl.content.page.PageImpl;
import ch.entwine.weblounge.common.impl.request.CacheTagImpl;
import ch.entwine.weblounge.common.impl.security.UserImpl;
import ch.entwine.weblounge.common.impl.url.WebUrlImpl;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ContentRepositoryOperation;
import ch.entwine.weblounge.common.repository.ContentRepositoryResourceOperation;
import ch.entwine.weblounge.common.repository.DeleteContentOperation;
import ch.entwine.weblounge.common.repository.DeleteOperation;
import ch.entwine.weblounge.common.repository.IndexOperation;
import ch.entwine.weblounge.common.repository.LockOperation;
import ch.entwine.weblounge.common.repository.MoveOperation;
import ch.entwine.weblounge.common.repository.PutContentOperation;
import ch.entwine.weblounge.common.repository.PutOperation;
import ch.entwine.weblounge.common.repository.ReferentialIntegrityException;
import ch.entwine.weblounge.common.repository.ResourceSelector;
import ch.entwine.weblounge.common.repository.ResourceSerializer;
import ch.entwine.weblounge.common.repository.UnlockOperation;
import ch.entwine.weblounge.common.repository.WritableContentRepository;
import ch.entwine.weblounge.common.request.CacheTag;
import ch.entwine.weblounge.common.request.ResponseCache;
import ch.entwine.weblounge.common.security.User;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.PathUtils;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.contentrepository.VersionedContentRepositoryIndex;
import ch.entwine.weblounge.contentrepository.impl.index.ContentRepositoryIndex;
import ch.entwine.weblounge.contentrepository.impl.operation.CurrentOperation;
import ch.entwine.weblounge.contentrepository.impl.operation.DeleteContentOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.DeleteOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.IndexOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.LockOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.MoveOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.PutContentOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.PutOperationImpl;
import ch.entwine.weblounge.contentrepository.impl.operation.UnlockOperationImpl;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Abstract base implementation of a <code>WritableContentRepository</code>.
 */
public abstract class AbstractWritableContentRepository extends AbstractContentRepository
        implements WritableContentRepository {

    /** The logging facility */
    static final Logger logger = LoggerFactory.getLogger(AbstractWritableContentRepository.class);

    /** Holds pages while they are written to the index */
    protected OperationProcessor processor = null;

    /** The response cache tracker */
    private ResponseCacheTracker responseCacheTracker = null;

    /** The environment tracker */
    private EnvironmentTracker environmentTracker = null;

    /** True to create a homepage when an empty repository is started */
    protected boolean createHomepage = true;

    /** Flag to indicate off-site indexing */
    protected boolean indexingOffsite = false;

    /**
     * Creates a new instance of the content repository.
     * 
     * @param type
     *          the repository type
     */
    public AbstractWritableContentRepository(String type) {
        super(type);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#connect(ch.entwine.weblounge.common.site.Site)
     */
    @Override
    public void connect(Site site) throws ContentRepositoryException {
        processor = new OperationProcessor(this);

        super.connect(site);

        if (createHomepage) {
            createHomepage();
        }

        Bundle bundle = loadBundle(site);
        if (bundle != null) {
            responseCacheTracker = new ResponseCacheTracker(bundle.getBundleContext(), site.getIdentifier());
            responseCacheTracker.open();
            environmentTracker = new EnvironmentTracker(bundle.getBundleContext(), this);
            environmentTracker.open();
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#disconnect()
     */
    @Override
    public void disconnect() throws ContentRepositoryException {

        // Finalize running operations
        if (processor != null)
            processor.stop();

        super.disconnect();

        // Make sure the bundle is still active. If not, unregistering the trackers
        // below will throw an IllegalStateException
        Bundle bundle = loadBundle(site);
        if (bundle == null || bundle.getState() != Bundle.ACTIVE)
            return;

        // Close the cache tracker
        if (responseCacheTracker != null) {
            responseCacheTracker.close();
            responseCacheTracker = null;
        }

        // Close the environment tracker
        if (environmentTracker != null) {
            environmentTracker.close();
            environmentTracker = null;
        }
    }

    /**
     * Returns the site's response cache.
     * 
     * @return the cache
     */
    protected ResponseCache getCache() {
        if (responseCacheTracker == null)
            return null;
        return responseCacheTracker.getCache();
    }

    /**
     * Creates an empty homepage in the content repository if it doesn't exist
     * yet.
     * 
     * @throws IllegalStateException
     * @throws ContentRepositoryException
     */
    protected void createHomepage() throws IllegalStateException, ContentRepositoryException {
        // Make sure there is a home page
        ResourceURI homeURI = new ResourceURIImpl(Page.TYPE, site, "/");
        if (!existsInAnyVersion(homeURI)) {
            try {
                Page page = new PageImpl(homeURI);
                User siteAdmininstrator = new UserImpl(site.getAdministrator());
                page.setTemplate(site.getDefaultTemplate().getIdentifier());
                page.setCreated(siteAdmininstrator, new Date());
                page.setPublished(siteAdmininstrator, new Date(), null);
                put(page, true);
                logger.info("Created homepage for {}", site.getIdentifier());
            } catch (IOException e) {
                logger.warn("Error creating home page in empty site '{}': {}", site.getIdentifier(),
                        e.getMessage());
            }
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#exists(ch.entwine.weblounge.common.content.ResourceURI)
     */
    @Override
    public boolean exists(ResourceURI uri) throws ContentRepositoryException {
        for (ResourceURI u : getVersions(uri)) {
            if (u.equals(uri))
                return true;
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#existsInAnyVersion(ch.entwine.weblounge.common.content.ResourceURI)
     */
    @Override
    public boolean existsInAnyVersion(ResourceURI uri) throws ContentRepositoryException {
        return getVersions(uri).length > 0;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#getVersions(ch.entwine.weblounge.common.content.ResourceURI)
     */
    @Override
    public ResourceURI[] getVersions(ResourceURI uri) throws ContentRepositoryException {
        Set<ResourceURI> uris = new HashSet<ResourceURI>();
        uris.addAll(Arrays.asList(super.getVersions(uri)));

        // Iterate over the resources that are currently being processed
        synchronized (processor) {
            for (ContentRepositoryOperation<?> op : processor.getOperations()) {

                // Is this a resource operation?
                if (!(op instanceof ContentRepositoryResourceOperation<?>))
                    continue;

                // Apply the changes to the original resource
                ContentRepositoryResourceOperation<?> resourceOp = (ContentRepositoryResourceOperation<?>) op;

                // Is the resource about to be deleted?
                ResourceURI opURI = resourceOp.getResourceURI();
                if (op instanceof DeleteOperation && equalsByIdOrPath(uri, opURI)) {
                    DeleteOperation deleteOp = (DeleteOperation) op;
                    List<ResourceURI> deleteCandidates = new ArrayList<ResourceURI>();
                    for (ResourceURI u : uris) {
                        if (deleteOp.allVersions() || u.getVersion() == opURI.getVersion()) {
                            deleteCandidates.add(u);
                        }
                    }
                    uris.removeAll(deleteCandidates);
                }

                // Is the resource simply being updated?
                if (op instanceof PutOperation && equalsByIdOrPath(uri, opURI)) {
                    uris.add(opURI);
                }

            }
        }

        return uris.toArray(new ResourceURI[uris.size()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#get(ch.entwine.weblounge.common.content.ResourceURI)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <R extends Resource<?>> R get(ResourceURI uri) throws ContentRepositoryException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Check if resource is in temporary cache and wait until it's clear which
        // version is the latest one
        Resource<?> resource = super.get(uri);

        // Iterate over the resources that are currently being processed
        synchronized (processor) {
            for (ContentRepositoryOperation<?> op : processor.getOperations()) {

                // Is this a resource operation?
                if (!(op instanceof ContentRepositoryResourceOperation<?>))
                    continue;

                // Apply the changes to the original resource
                ContentRepositoryResourceOperation<?> resourceOp = (ContentRepositoryResourceOperation<?>) op;
                resource = resourceOp.apply(uri, resource);
            }
        }

        // If we found a resource, let's return it
        return (R) resource;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#lock(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.security.User)
     */
    public Resource<?> lock(ResourceURI uri, User user)
            throws IllegalStateException, ContentRepositoryException, IOException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof LockOperation)) {
            return lockAsynchronously(uri, user).get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, locking anyway", uri);
        }

        // Update all resources in memory
        Resource<?> resource = null;
        ContentRepositoryOperation<?> lockOperation = CurrentOperation.get();
        for (ResourceURI u : getVersions(uri)) {
            Resource<?> r = get(u);
            if (r == null) {
                logger.debug("Version {} of {} has been removed in the meantime", u.getVersion(), u);
                continue;
            }
            r.lock(user);
            PutOperation putOp = new PutOperationImpl(r, false);
            try {
                CurrentOperation.set(putOp);
                put(r, false);
            } finally {
                CurrentOperation.set(lockOperation);
            }
            if (r.getVersion() == uri.getVersion())
                resource = r;
        }

        return resource;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#lockAsynchronously(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.security.User)
     */
    public LockOperation lockAsynchronously(final ResourceURI uri, final User user)
            throws IOException, ContentRepositoryException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        LockOperation lockOperation = new LockOperationImpl(uri, user);
        processor.enqueue(lockOperation);
        return lockOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#unlock(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.security.User)
     */
    public Resource<?> unlock(ResourceURI uri, User user)
            throws ContentRepositoryException, IllegalStateException, IOException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof UnlockOperation)) {
            return unlockAsynchronously(uri, user).get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, unlocking anyway", uri);
        }

        // Update all resources in memory
        Resource<?> resource = null;
        ContentRepositoryOperation<?> unlockOperation = CurrentOperation.get();
        for (ResourceURI u : getVersions(uri)) {
            Resource<?> r = get(u);
            r.unlock();
            PutOperation putOp = new PutOperationImpl(r, false);
            try {
                CurrentOperation.set(putOp);
                put(r, false);
            } finally {
                CurrentOperation.set(unlockOperation);
            }
            if (r.getVersion() == uri.getVersion())
                resource = r;
        }

        return resource;

    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#unlockAsynchronously(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.security.User)
     */
    public UnlockOperation unlockAsynchronously(final ResourceURI uri, final User user)
            throws IOException, ContentRepositoryException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        UnlockOperation lockOperation = new UnlockOperationImpl(uri, user);
        processor.enqueue(lockOperation);
        return lockOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#isLocked(ch.entwine.weblounge.common.content.ResourceURI)
     */
    public boolean isLocked(ResourceURI uri) throws ContentRepositoryException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        Resource<?> r = get(uri);

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, return lock status anyway", uri);
        }

        return r.isLocked();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#delete(ch.entwine.weblounge.common.content.ResourceURI)
     */
    public boolean delete(ResourceURI uri) throws ContentRepositoryException, IOException {
        return delete(uri, false);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#deleteAsynchronously(ch.entwine.weblounge.common.content.ResourceURI)
     */
    public DeleteOperation deleteAsynchronously(final ResourceURI uri)
            throws ContentRepositoryException, IOException {
        return deleteAsynchronously(uri, false);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#delete(ch.entwine.weblounge.common.content.ResourceURI,
     *      boolean)
     */
    public boolean delete(ResourceURI uri, boolean allRevisions) throws ContentRepositoryException, IOException {
        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof DeleteOperation)) {
            DeleteOperation deleteOperation = deleteAsynchronously(uri, allRevisions);
            if (deleteOperation == null)
                return true;
            return deleteOperation.get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, removing anyway", uri);
        }

        // See if the resource exists
        if (allRevisions && !index.existsInAnyVersion(uri) && processor.isProcessingVersionOf(uri)) {
            logger.warn("Resource '{}' not found in repository index", uri);
            return false;
        }

        // Make sure the resource is not being referenced elsewhere
        if (allRevisions || uri.getVersion() == Resource.LIVE) {
            SearchQuery searchByResource = new SearchQueryImpl(uri.getSite());
            searchByResource.withVersion(Resource.LIVE);
            searchByResource.withProperty("resourceid", uri.getIdentifier());
            if (index.find(searchByResource).getDocumentCount() > 0) {
                logger.debug("Resource '{}' is still being referenced", uri);
                throw new ReferentialIntegrityException(uri.getIdentifier());
            }
        }

        // Get the revisions to delete
        long[] revisions = new long[] { uri.getVersion() };
        if (allRevisions) {
            if (uri.getVersion() != Resource.LIVE)
                uri = new ResourceURIImpl(uri, Resource.LIVE);
            revisions = index.getRevisions(uri);
        }

        // Delete resources, but get an in-memory representation first
        Resource<?> resource = ((DeleteOperation) CurrentOperation.get()).getResource();
        deleteResource(uri, revisions);

        // Delete the index entries
        for (long revision : revisions) {
            index.delete(new ResourceURIImpl(uri, revision));
        }

        // Delete previews
        deletePreviews(resource);

        // Make sure related stuff gets thrown out of the cache
        ResponseCache cache = getCache();
        if (cache != null && (allRevisions || uri.getVersion() == Resource.LIVE)) {
            cache.invalidate(new CacheTag[] { new CacheTagImpl(CacheTag.Resource, uri.getIdentifier()) }, true);
        }

        return true;

    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#deleteAsynchronously(ch.entwine.weblounge.common.content.ResourceURI,
     *      boolean)
     */
    public DeleteOperation deleteAsynchronously(ResourceURI uri, boolean allRevisions)
            throws ContentRepositoryException, IOException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        Resource<?> resource = get(uri);
        if (resource == null)
            return null;
        DeleteOperation deleteOperation = new DeleteOperationImpl(resource, allRevisions);
        processor.enqueue(deleteOperation);
        return deleteOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#move(ch.entwine.weblounge.common.content.ResourceURI,
     *      String, boolean)
     */
    public void move(ResourceURI uri, String targetPath, boolean moveChildren)
            throws IOException, ContentRepositoryException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof MoveOperation)) {
            moveAsynchronously(uri, targetPath, moveChildren).get();
            return;
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, moving anyway", uri);
        }

        String originalPathPrefix = uri.getPath();
        if (originalPathPrefix == null)
            throw new IllegalArgumentException("Cannot move resource with null path");
        if (StringUtils.isEmpty(targetPath))
            throw new IllegalArgumentException("Cannot move resource to empty path");
        if (!targetPath.startsWith("/"))
            throw new IllegalArgumentException("Cannot move resource to relative path '" + targetPath + "'");
        if (originalPathPrefix.equals(targetPath))
            return;

        // Locate the resources to move
        Set<ResourceURI> documentsToMove = new HashSet<ResourceURI>();
        documentsToMove.add(uri);

        // Also move children?
        if (moveChildren) {
            SearchQuery q = new SearchQueryImpl(site).withPreferredVersion(Resource.LIVE);
            q.withPathPrefix(originalPathPrefix);

            SearchResult result = index.find(q);
            if (result.getDocumentCount() == 0) {
                logger.warn("Trying to move non existing resource {}", uri);
                return;
            }

            // We need to check the prefix again, since the search query will also
            // match parts of the originalPathPrefix
            for (SearchResultItem searchResult : result.getItems()) {
                if (!(searchResult instanceof ResourceSearchResultItem))
                    continue;
                ResourceSearchResultItem rsri = (ResourceSearchResultItem) searchResult;
                String resourcePath = rsri.getResourceURI().getPath();

                // Add the document if the paths match and it is not already contained
                // in our list (never mind path and version, just look at the id)
                if (resourcePath != null && resourcePath.startsWith(originalPathPrefix)) {
                    boolean existing = false;
                    for (ResourceURI u : documentsToMove) {
                        if (u.getIdentifier().equals(rsri.getResourceURI().getIdentifier())) {
                            existing = true;
                            break;
                        }
                    }
                    if (!existing)
                        documentsToMove.add(rsri.getResourceURI());
                }
            }
        }

        // Finally, move all resources
        for (ResourceURI u : documentsToMove) {
            String originalPath = u.getPath();
            String pathSuffix = originalPath.substring(originalPathPrefix.length());
            String newPath = null;

            // Is the original path just a prefix, or is it an exact match?
            if (StringUtils.isNotBlank(pathSuffix))
                newPath = UrlUtils.concat(targetPath, pathSuffix);
            else
                newPath = targetPath;

            // Move every version of the resource, since we want the path to be
            // in sync across resource versions
            for (long version : index.getRevisions(u)) {
                ResourceURI candidateURI = new ResourceURIImpl(u.getType(), site, null, u.getIdentifier(), version);

                // Load the resource, adjust the path and store it again
                Resource<?> r = get(candidateURI);

                // Store the updated resource
                r.getURI().setPath(newPath);
                storeResource(r);

                // Update the index
                r.getURI().setPath(originalPath);
                index.move(r.getURI(), newPath);

                // Create the preview images
                if (connected && !initializing)
                    createPreviews(r);
            }
        }

        // Make sure related stuff gets thrown out of the cache
        ResponseCache cache = getCache();
        if (cache != null) {
            cache.invalidate(new CacheTag[] { new CacheTagImpl(CacheTag.Resource, uri.getIdentifier()) }, true);
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#moveAsynchronously(ch.entwine.weblounge.common.content.ResourceURI,
     *      java.lang.String, boolean)
     */
    public MoveOperation moveAsynchronously(final ResourceURI uri, final String path, final boolean moveChildren)
            throws ContentRepositoryException, IOException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        MoveOperation moveOperation = new MoveOperationImpl(uri, path, moveChildren);
        processor.enqueue(moveOperation);
        return moveOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#put(ch.entwine.weblounge.common.content.Resource)
     */
    public Resource<?> put(Resource<?> resource)
            throws ContentRepositoryException, IOException, IllegalStateException {

        return put(resource, true);
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#putAsynchronously(ch.entwine.weblounge.common.content.Resource)
     */
    public PutOperation putAsynchronously(Resource<?> resource)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        PutOperation putOperation = new PutOperationImpl(resource, false);
        processor.enqueue(putOperation);
        return putOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#putAsynchronously(ch.entwine.weblounge.common.content.Resource)
     */
    public PutOperation putAsynchronously(Resource<?> resource, boolean updatePreviews)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        PutOperation putOperation = new PutOperationImpl(resource, updatePreviews);
        processor.enqueue(putOperation);
        return putOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#put(ch.entwine.weblounge.common.content.Resource,
     *      boolean)
     */
    public Resource<?> put(Resource<?> resource, boolean updatePreviews)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof PutOperation)) {
            return putAsynchronously(resource, updatePreviews).get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(resource.getURI())) {
            logger.debug("Resource '{}' is being processed, putting anyway", resource.getURI());
        }

        ResourceURI uri = resource.getURI();

        // If the document exists in the given version, update it otherwise add it
        // to the index
        if (index.exists(uri)) {
            index.update(resource);
        } else {
            if (resource.contents().size() > 0)
                throw new IllegalStateException("Cannot add content metadata without content");
            index.add(resource);
        }

        // Make sure related stuff gets thrown out of the cache
        ResponseCache cache = getCache();
        if (cache != null && uri.getVersion() == Resource.LIVE) {
            List<CacheTag> tags = new ArrayList<CacheTag>();

            // resource id
            tags.add(new CacheTagImpl(CacheTag.Resource, uri.getIdentifier()));

            // subjects, so that resource lists get updated
            for (String subject : resource.getSubjects())
                tags.add(new CacheTagImpl(CacheTag.Subject, subject));

            cache.invalidate(tags.toArray(new CacheTagImpl[tags.size()]), true);
        }

        // Write the updated resource to disk
        storeResource(resource);

        // Create the preview images. Don't if the site is currently being created.
        if (updatePreviews && connected && !initializing)
            createPreviews(resource);

        return resource;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#putContent(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.content.ResourceContent,
     *      java.io.InputStream)
     */
    public Resource<?> putContent(ResourceURI uri, ResourceContent content, InputStream is)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof PutContentOperation)) {
            return putContentAsynchronously(uri, content, is).get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, adding content anyway", uri);
        }

        // Make sure the resource exists
        if (!index.exists(uri))
            throw new IllegalStateException("Cannot add content to missing resource " + uri);
        Resource<ResourceContent> resource = null;
        try {
            resource = get(uri);
            if (resource == null) {
                throw new IllegalStateException("Resource " + uri + " not found");
            }
        } catch (ClassCastException e) {
            logger.error("Trying to add content of type {} to incompatible resource", content.getClass());
            throw new IllegalStateException(e);
        }

        // Store the content and add entry to index
        try {
            resource.addContent(content);
        } catch (ClassCastException e) {
            logger.error("Trying to add content of type {} to incompatible resource", content.getClass());
            throw new IllegalStateException(e);
        }

        storeResourceContent(uri, content, is);
        storeResource(resource);
        index.update(resource);

        // Create the preview images
        if (connected && !initializing)
            createPreviews(resource);

        // Make sure related stuff gets thrown out of the cache
        ResponseCache cache = getCache();
        if (cache != null) {
            cache.invalidate(new CacheTag[] { new CacheTagImpl(CacheTag.Resource, uri.getIdentifier()) }, true);
        }

        return resource;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#putContentAsynchronously(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.content.ResourceContent,
     *      java.io.InputStream)
     */
    public PutContentOperation putContentAsynchronously(final ResourceURI uri, final ResourceContent content,
            final InputStream is) throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        PutContentOperation putOperation = new PutContentOperationImpl(uri, content, is);
        processor.enqueue(putOperation);
        return putOperation;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#deleteContent(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.content.ResourceContent)
     */
    public Resource<?> deleteContent(ResourceURI uri, ResourceContent content)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Is this a new request or a scheduled asynchronous execution?
        if (!(CurrentOperation.get() instanceof DeleteContentOperation)) {
            return deleteContentAsynchronously(uri, content).get();
        }

        // Check if resource is in temporary cache already by another operation
        if (processor.isProcessing(uri)) {
            logger.debug("Resource '{}' is being processed, removing content anyway", uri);
        }

        // Make sure the resource exists
        if (!index.exists(uri))
            throw new IllegalStateException("Cannot remove content from missing resource " + uri);
        Resource<?> resource = null;
        try {
            resource = get(uri);
            if (resource == null) {
                throw new IllegalStateException("Resource " + uri + " not found");
            }
        } catch (ClassCastException e) {
            logger.error("Trying to remove content of type {} from incompatible resource", content.getClass());
            throw new IllegalStateException(e);
        }

        // Store the content and add entry to index
        resource.removeContent(content.getLanguage());
        deleteResourceContent(uri, content);
        storeResource(resource);
        index.update(resource);

        // Delete previews
        deletePreviews(resource, content.getLanguage());

        // Make sure related stuff gets thrown out of the cache
        ResponseCache cache = getCache();
        if (cache != null) {
            cache.invalidate(new CacheTag[] { new CacheTagImpl(CacheTag.Resource, uri.getIdentifier()) }, true);
        }

        return resource;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#deleteContent(ch.entwine.weblounge.common.content.ResourceURI,
     *      ch.entwine.weblounge.common.content.ResourceContent,
     *      ch.entwine.weblounge.common.repository.ContentRepositoryOperationListener)
     */
    public DeleteContentOperation deleteContentAsynchronously(final ResourceURI uri, final ResourceContent content)
            throws ContentRepositoryException, IOException, IllegalStateException {

        if (!isStarted())
            throw new IllegalStateException("Content repository is not connected");

        // Create an asynchronous operation representation and return it
        DeleteContentOperation deleteContentOperation = new DeleteContentOperationImpl(uri, content);
        processor.enqueue(deleteContentOperation);
        return deleteContentOperation;
    };

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#index()
     */
    public void index() throws ContentRepositoryException {

        if (indexing || indexingOffsite) {
            logger.warn("Ignoring additional index request for {}", this);
            return;
        }

        boolean oldReadOnly = readOnly;
        readOnly = true;
        logger.info("Switching site '{}' to read only mode", site);

        ContentRepositoryIndex newIndex = null;

        // Clear previews directory
        logger.info("Removing cached preview images");
        File previewsDir = new File(
                PathUtils.concat(System.getProperty("java.io.tmpdir"), "sites", site.getIdentifier(), "images"));
        FileUtils.deleteQuietly(previewsDir);

        // Create the new index
        try {
            newIndex = new ContentRepositoryIndex(site, resourceSerializer, false);
            indexingOffsite = true;
            rebuildIndex(newIndex);
        } catch (IOException e) {
            indexingOffsite = false;
            throw new ContentRepositoryException("Error creating index " + site.getIdentifier(), e);
        } finally {
            try {
                if (newIndex != null)
                    newIndex.close();
            } catch (IOException e) {
                throw new ContentRepositoryException("Error closing new index " + site.getIdentifier(), e);
            }
        }

        try {
            indexing = true;
            index.close();
            logger.info("Loading new index");
            index = new ContentRepositoryIndex(site, resourceSerializer, oldReadOnly);
        } catch (IOException e) {
            Throwable cause = e.getCause();
            if (cause == null)
                cause = e;
            throw new ContentRepositoryException("Error during reindex of '" + site.getIdentifier() + "'", cause);
        } finally {
            indexing = false;
            indexingOffsite = false;
            logger.info("Switching site '{}' back to write mode", site);
            readOnly = oldReadOnly;
        }

    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.common.repository.WritableContentRepository#indexAsynchronously()
     */
    public IndexOperation indexAsynchronously() throws ContentRepositoryException {
        IndexOperation op = new IndexOperationImpl();
        processor.enqueue(op);
        return op;
    }

    /**
     * Creates a new content repository index at the given location as specified
     * by <code>idx</code>.
     * 
     * @param idx
     *          the index
     * @throws ContentRepositoryException
     *           if indexing fails
     */
    private void buildIndex(ContentRepositoryIndex idx) throws ContentRepositoryException {
        boolean oldReadOnly = readOnly;
        readOnly = true;
        indexing = true;

        if (!oldReadOnly)
            logger.info("Switching site '{}' to read only mode", site.getIdentifier());

        rebuildIndex(idx);

        indexing = false;
        if (!oldReadOnly)
            logger.info("Switching site '{}' back to write mode", site.getIdentifier());
        readOnly = oldReadOnly;
    }

    /**
     * Creates a new content repository index at the given location as specified
     * by <code>idx</code>.
     * 
     * @param idx
     *          the index
     * @throws ContentRepositoryException
     *           if indexing fails
     */
    private void rebuildIndex(ContentRepositoryIndex idx) throws ContentRepositoryException {
        boolean success = true;

        try {
            // Clear the current index, which might be null if the site has not been
            // started yet.
            if (idx == null)
                idx = loadIndex();

            logger.info("Creating site index '{}'...", site.getIdentifier());
            long time = System.currentTimeMillis();
            long resourceCount = 0;

            // Index each and every known resource type
            for (ResourceSerializer<?, ?> serializer : getSerializers()) {
                long added = index(idx, serializer.getType());
                if (added > 0)
                    logger.info("Added {} {}s to index", added, serializer.getType().toLowerCase());
                resourceCount += added;
            }

            if (resourceCount > 0) {
                time = System.currentTimeMillis() - time;
                logger.info("Site index populated in {} ms", ConfigurationUtils.toHumanReadableDuration(time));
                logger.info("{} resources added to index", resourceCount);
            }
        } catch (IOException e) {
            success = false;
            throw new ContentRepositoryException("Error while writing to index", e);
        } catch (MalformedResourceURIException e) {
            success = false;
            throw new ContentRepositoryException("Error while reading resource uri for index", e);
        } finally {
            if (!success) {
                try {
                    idx.clear();
                } catch (IOException e) {
                    logger.error("Error while trying to cleanup after failed indexing operation", e);
                }
            }
        }
    }

    /**
     * This method indexes a certain type of resources and expects the resources
     * to be located in a sub directory of the site directory named
     * <tt>&lt;resourceType&gt;s<tt>.
     * 
     * @param idx
     *          the content repository index
     * @param resourceType
     *          the resource type
     * @return the number of resources that were indexed
     * @throws IOException
     *           if accessing a file fails
     */
    protected long index(ContentRepositoryIndex idx, String resourceType)
            throws ContentRepositoryException, IOException {

        logger.info("Populating site index '{}' with {}s...", site, resourceType);

        ResourceSerializer<?, ?> serializer = getSerializerByType(resourceType);
        if (serializer == null) {
            logger.warn("Unable to index resources of type '{}': no resource serializer found", resourceType);
            return 0;
        }

        long resourceCount = 0;
        long resourceVersionCount = 0;
        ResourceSelector selector = new ResourceSelectorImpl(site).withTypes(resourceType);

        // Ask for all existing resources of the current type and index them
        for (ResourceURI uri : list(selector)) {
            try {
                Resource<?> resource = null;
                ResourceReader<?, ?> reader = serializer.getReader();
                InputStream is = null;

                // Read the resource
                try {
                    is = loadResource(uri);
                    resource = reader.read(is, site);
                    if (resource == null) {
                        logger.warn("Unkown error loading '{}'", uri);
                        continue;
                    }

                    // Fix malformed paths stemming from content conversion
                    for (ResourceMetadata<?> metadataItem : serializer.toMetadata(resource)) {
                        if (PATH.equals(metadataItem.getName())) {
                            String path = (String) metadataItem.getValues().get(0);
                            try {
                                // try to create a web url, which will reveal invalid paths
                                new WebUrlImpl(site, path);
                            } catch (IllegalArgumentException e) {
                                logger.info("Updating {} {}:{} to remove invalid path '{}'",
                                        new Object[] { serializer.getType().toLowerCase(), site.getIdentifier(),
                                                resource.getIdentifier(), path });
                                resource.setPath(null);
                                storeResource(resource);
                            }
                        }
                    }
                } catch (Throwable t) {
                    logger.error("Error loading '{}': {}", uri, t.getMessage());
                    continue;
                } finally {
                    IOUtils.closeQuietly(is);
                }

                logger.info("Indexing {} [{}]", resource, resource.getVersion());
                idx.add(resource);
                resourceVersionCount++;

            } catch (Throwable t) {
                logger.error("Error indexing {} {}: {}", new Object[] { resourceType, uri, t.getMessage() });
            }
        }

        // Log the work
        if (resourceCount > 0) {
            logger.info("{} {}s and {} revisions added to index",
                    new Object[] { resourceCount, resourceType, resourceVersionCount - resourceCount });
        }

        return resourceCount;
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#loadIndex()
     */
    @Override
    protected ContentRepositoryIndex loadIndex() throws IOException, ContentRepositoryException {
        logger.debug("Trying to load site index");

        ContentRepositoryIndex idx = null;

        logger.debug("Loading site index '{}'", site.getIdentifier());

        // Add content if there is any
        idx = new ContentRepositoryIndex(site, resourceSerializer, readOnly);

        // Create the idx if there is nothing in place so far
        if (idx.getResourceCount() <= 0) {
            logger.info("Index of '{}' is empty, triggering reindex", site.getIdentifier());
            buildIndex(idx);
        }

        // Make sure the version matches the implementation
        else if (idx.getIndexVersion() < VersionedContentRepositoryIndex.INDEX_VERSION) {
            logger.info("Index of '{}' needs to be updated, triggering reindex", site.getIdentifier());
            buildIndex(idx);
        } else if (idx.getIndexVersion() != VersionedContentRepositoryIndex.INDEX_VERSION) {
            logger.warn("Index '{}' needs to be downgraded, triggering reindex", site.getIdentifier());
            buildIndex(idx);
        }

        // Is there an existing idex?
        long resourceCount = idx.getResourceCount();
        long resourceVersionCount = idx.getRevisionCount();
        logger.info("Loaded site idx with {} resources and {} revisions", resourceCount,
                resourceVersionCount - resourceCount);

        return idx;
    }

    /**
     * Writes a new resource to the repository storage.
     * 
     * @param resource
     *          the resource content
     * @throws ContentRepositoryException
     *           if updating the index fails
     * @throws IOException
     *           if the resource can't be written to the storage
     */
    protected abstract Resource<?> storeResource(Resource<?> resource)
            throws ContentRepositoryException, IOException;

    /**
     * Writes the resource content to the repository storage.
     * 
     * @param uri
     *          the resource uri
     * @param content
     *          the resource content
     * @param is
     *          the input stream
     * @throws ContentRepositoryException
     *           if updating the content repository index fails
     * @throws IOException
     *           if the resource can't be written to the storage
     */
    protected abstract ResourceContent storeResourceContent(ResourceURI uri, ResourceContent content,
            InputStream is) throws ContentRepositoryException, IOException;

    /**
     * Deletes the indicated revisions of resource <code>uri</code> from the
     * repository. The concrete implementation is responsible for making the
     * deletion of multiple revisions safe, i. e. transactional.
     * 
     * @param uri
     *          the resource uri
     * @param revisions
     *          the revisions to remove
     * @throws ContentRepositoryException
     *           if deleting the resource from the index fails
     * @throws IOException
     *           if removing the resource from disk fails
     */
    protected abstract void deleteResource(ResourceURI uri, long[] revisions)
            throws ContentRepositoryException, IOException;

    /**
     * Deletes the resource content from the repository storage.
     * 
     * @param uri
     *          the resource uri
     * @param content
     *          the resource content
     * @throws ContentRepositoryException
     *           if deleting the resource content from the index fails
     * @throws IOException
     *           if the resource can't be written to the storage
     */
    protected abstract void deleteResourceContent(ResourceURI uri, ResourceContent content)
            throws ContentRepositoryException, IOException;

    /**
     * This class is used as a way to keep track of what has been added to the
     * repository but has not been flushed to disk.
     */
    public final class OperationProcessor {

        /** The operations counter */
        private final Map<ResourceURI, List<ContentRepositoryOperation<?>>> operationsPerResource = new HashMap<ResourceURI, List<ContentRepositoryOperation<?>>>();

        /** The repository operations */
        protected List<ContentRepositoryOperation<?>> operations = new ArrayList<ContentRepositoryOperation<?>>();

        /** The worker thread */
        private Thread processorWorker = null;

        /** Running flag */
        protected boolean keepRunning = true;

        /**
         * Creates a new operation processor.
         */
        public OperationProcessor(final WritableContentRepository repository) {
            final OperationProcessor monitor = this;
            processorWorker = new Thread(new Runnable() {
                public void run() {
                    while (keepRunning) {
                        List<ContentRepositoryOperation<?>> opList = new ArrayList<ContentRepositoryOperation<?>>(
                                operations);
                        for (ContentRepositoryOperation<?> op : opList) {
                            try {
                                CurrentOperation.set(op);
                                op.execute(repository);
                            } catch (Throwable t) {
                                logger.debug("Error while executing {}: {}", op, t.getMessage());
                                // This will be dealt with by the operation itself
                            } finally {
                                CurrentOperation.remove();

                                // Remove the operation form the operations list
                                synchronized (operations) {
                                    operations.remove(op);
                                    operations.notifyAll();
                                }

                                // Tell everyone that we are down 1
                                synchronized (monitor) {
                                    monitor.notifyAll();
                                }
                            }
                        }

                        // Is there more work to be done? If not, wait for more
                        synchronized (operations) {
                            while (keepRunning && operations.size() == 0) {
                                try {
                                    operations.wait();
                                } catch (InterruptedException e) {
                                    logger.debug("Interrupted while waiting for more work");
                                }
                            }
                        }
                    }
                }
            });
            processorWorker.start();
        }

        /**
         * Returns <code>true</code> if the cache contains the uri itself or a
         * different version of it.
         * <p>
         * Note that this method is not considering operations returned by
         * {@link CurrentOperation#get()}.
         * 
         * @param uri
         *          the uri
         * @return <code>true</code> if it contains a version of this resource
         */
        public boolean isProcessingVersionOf(ResourceURI uri) {
            synchronized (operations) {
                for (ResourceURI u : operationsPerResource.keySet()) {
                    if (u.getIdentifier().equals(uri.getIdentifier())) {
                        ContentRepositoryOperation<?> currentOp = CurrentOperation.get();
                        if (currentOp instanceof ContentRepositoryResourceOperation<?>) {
                            ResourceURI currentURI = ((ContentRepositoryResourceOperation<?>) currentOp)
                                    .getResourceURI();
                            if (!u.equals(currentURI))
                                return true;
                        }
                    }
                }
            }
            return false;
        }

        /**
         * Returns <code>true</code> if the scheduler is processing work related to
         * the given resource and the version indicated by the uri.
         * 
         * @param uri
         *          the uri
         * @return <code>true</code> if the resource is being processed
         */
        public boolean isProcessing(ResourceURI uri) {
            synchronized (operations) {
                for (ResourceURI u : operationsPerResource.keySet()) {
                    if (u.getIdentifier().equals(uri.getIdentifier()))
                        return true;
                }
            }
            return false;
        }

        /**
         * Returns the list of currently scheduled content repository operation.
         * 
         * @return the content repository operations
         */
        public List<ContentRepositoryOperation<?>> getOperations() {
            return new ArrayList<ContentRepositoryOperation<?>>(operations);
        }

        /**
         * Adds the given operation to the list of resources that need to be
         * processed-
         * 
         * @param operation
         *          the operation
         */
        public void enqueue(ContentRepositoryOperation<?> operation) {
            operations.add(operation);
            synchronized (operations) {
                operations.notifyAll();
            }
        }

        /**
         * Stops the scheduler.
         */
        public void stop() {
            keepRunning = false;
            operationsPerResource.clear();
            synchronized (operations) {
                operations.clear();
                operations.notifyAll();
            }
        }

    }

}