ch.entwine.weblounge.contentrepository.impl.endpoint.PagesEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.contentrepository.impl.endpoint.PagesEndpoint.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.endpoint;

import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.content.SearchQuery;
import ch.entwine.weblounge.common.content.SearchQuery.Order;
import ch.entwine.weblounge.common.content.SearchResult;
import ch.entwine.weblounge.common.content.SearchResultItem;
import ch.entwine.weblounge.common.content.page.Composer;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.content.page.Pagelet;
import ch.entwine.weblounge.common.impl.content.GeneralResourceURIImpl;
import ch.entwine.weblounge.common.impl.content.ResourceURIImpl;
import ch.entwine.weblounge.common.impl.content.SearchQueryImpl;
import ch.entwine.weblounge.common.impl.content.SearchResultImpl;
import ch.entwine.weblounge.common.impl.content.page.PageImpl;
import ch.entwine.weblounge.common.impl.content.page.PageReader;
import ch.entwine.weblounge.common.impl.content.page.PageSearchResultItemImpl;
import ch.entwine.weblounge.common.impl.content.page.PageURIImpl;
import ch.entwine.weblounge.common.impl.security.SecurityUtils;
import ch.entwine.weblounge.common.impl.security.SystemRole;
import ch.entwine.weblounge.common.impl.security.UserImpl;
import ch.entwine.weblounge.common.impl.url.WebUrlImpl;
import ch.entwine.weblounge.common.impl.util.WebloungeDateFormat;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ReferentialIntegrityException;
import ch.entwine.weblounge.common.repository.WritableContentRepository;
import ch.entwine.weblounge.common.security.SecurityService;
import ch.entwine.weblounge.common.security.User;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.common.url.WebUrl;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.StringTokenizer;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.xml.parsers.ParserConfigurationException;

/**
 * This class implements the <code>REST</code> endpoint for page data.
 */
@Path("/")
@Produces(MediaType.APPLICATION_XML)
public class PagesEndpoint extends ContentRepositoryEndpoint {

    /** Logging facility */
    private static final Logger logger = LoggerFactory.getLogger(PagesEndpoint.class);

    /** The security service */
    protected SecurityService securityService = null;

    /** The endpoint documentation */
    private String docs = null;

    /**
     * Returns a collection of pages which match the given criteria.
     * 
     * @param request
     *          the request
     * @param path
     *          the page path (e.g. <code>/my/simple/path</code>)
     * @param subjectstring
     *          one ore more subjects, divided by a comma
     * @param searchterms
     *          fulltext search terms
     * @param sort
     *          sort order, possible values are
     *          <code>created-asc, created-desc, published-asc, published-desc, modified-asc & modified-desc</code>
     * @param limit
     *          search result limit
     * @param offset
     *          search result offset (for paging in combination with limit)
     * @param details
     *          switch for providing pages including their bodies
     * @return a collection of matching pages
     */
    @GET
    @Path("/")
    public Response getAllPages(@Context HttpServletRequest request, @QueryParam("path") String path,
            @QueryParam("subjects") String subjectstring, @QueryParam("searchterms") String searchterms,
            @QueryParam("filter") String filter, @QueryParam("sort") @DefaultValue("modified-desc") String sort,
            @QueryParam("version") @DefaultValue("-1") long version,
            @QueryParam("preferredversion") @DefaultValue("-1") long preferredVersion,
            @QueryParam("limit") @DefaultValue("10") int limit, @QueryParam("offset") @DefaultValue("0") int offset,
            @QueryParam("details") @DefaultValue("false") boolean details) {

        // Create search query
        Site site = getSite(request);
        SearchQuery q = new SearchQueryImpl(site);

        q.withTypes(Page.TYPE);
        if (version != -1)
            q.withVersion(version);

        if (preferredVersion != -1)
            q.withPreferredVersion(preferredVersion);

        // Path
        if (StringUtils.isNotBlank(path))
            q.withPath(path);

        // Subjects
        if (StringUtils.isNotBlank(subjectstring)) {
            StringTokenizer subjects = new StringTokenizer(subjectstring, ",");
            while (subjects.hasMoreTokens())
                q.withSubject(subjects.nextToken());
        }

        // Search terms
        if (StringUtils.isNotBlank(searchterms))
            q.withText(true, searchterms);

        Calendar today = Calendar.getInstance();
        today.set(Calendar.HOUR_OF_DAY, 0);
        today.set(Calendar.MINUTE, 0);
        today.set(Calendar.SECOND, 0);
        today.set(Calendar.MILLISECOND, 0);
        Calendar yesterday = Calendar.getInstance();
        yesterday.add(Calendar.DATE, -1);
        yesterday.set(Calendar.HOUR_OF_DAY, 0);
        yesterday.set(Calendar.MINUTE, 0);
        yesterday.set(Calendar.SECOND, 0);
        yesterday.set(Calendar.MILLISECOND, 0);
        Calendar tomorrow = Calendar.getInstance();
        tomorrow.add(Calendar.DATE, 1);
        tomorrow.set(Calendar.HOUR_OF_DAY, 0);
        tomorrow.set(Calendar.MINUTE, 0);
        tomorrow.set(Calendar.SECOND, 0);
        tomorrow.set(Calendar.MILLISECOND, 0);

        // Filter query
        if (StringUtils.isNotBlank(filter)) {
            if ("/".equals(filter)) {
                q.withPath("/");
            } else if (filter.contains("state:work")) {
                q.withVersion(Resource.WORK);
                q.withPreferredVersion(-1);
            } else if (filter.contains("state:live")) {
                q.withVersion(Resource.LIVE);
                q.withPreferredVersion(-1);
            } else if (filter.contains("state:locked")) {
                q.withLockOwner();
            }

            // by user
            else if (filter.startsWith("locked:") && filter.length() > "locked:".length()) {
                String lockOwner = StringUtils.trim(filter.substring("locked:".length()));
                if ("me".equals(lockOwner))
                    q.withLockOwner(securityService.getUser());
                else
                    q.withLockOwner(new UserImpl(lockOwner));
            } else if (filter.startsWith("creator:") && filter.length() > "creator:".length()) {
                String creator = StringUtils.trim(filter.substring("creator:".length()));
                if ("me".equals(creator))
                    q.withCreator(securityService.getUser());
                else
                    q.withCreator(new UserImpl(creator));
            } else if (filter.startsWith("modifier:") && filter.length() > "modifier:".length()) {
                String modifier = StringUtils.trim(filter.substring("modifier:".length()));
                if ("me".equals(modifier))
                    q.withModifier(securityService.getUser());
                else
                    q.withModifier(new UserImpl(modifier));
            } else if (filter.startsWith("publisher:") && filter.length() > "publisher:".length()) {
                String publisher = StringUtils.trim(filter.substring("publisher:".length()));
                if ("me".equals(publisher))
                    q.withPublisher(securityService.getUser());
                else
                    q.withPublisher(new UserImpl(publisher));
            }

            // by date

            else if (filter.startsWith("created:") && filter.length() > "created:".length()) {
                String created = StringUtils.trim(filter.substring("created:".length()));
                if ("today".equals(created))
                    q.withCreationDateBetween(today.getTime()).and(tomorrow.getTime());
                else if ("yesterday".equals(created))
                    q.withCreationDateBetween(yesterday.getTime()).and(today.getTime());
                else
                    q.withCreationDate(tomorrow.getTime());
            } else if (filter.startsWith("modified:") && filter.length() > "modified:".length()) {
                String modified = StringUtils.trim(filter.substring("modified:".length()));
                if ("today".equals(modified))
                    q.withModificationDateBetween(today.getTime()).and(tomorrow.getTime());
                else if ("yesterday".equals(modified))
                    q.withModificationDateBetween(yesterday.getTime()).and(today.getTime());
                else
                    q.withCreationDate(tomorrow.getTime());
            } else if (filter.startsWith("publisher:") && filter.length() > "publisher:".length()) {
                String published = StringUtils.trim(filter.substring("published:".length()));
                if ("today".equals(published))
                    q.withPublishingDateBetween(today.getTime()).and(tomorrow.getTime());
                else if ("yesterday".equals(published))
                    q.withPublishingDateBetween(yesterday.getTime()).and(today.getTime());
                else
                    q.withCreationDate(tomorrow.getTime());
            }

            // by id
            else if (filter.contains("id:")) {
                String[] searchTerms = StringUtils.split(filter);
                for (String searchTerm : searchTerms) {
                    if (searchTerm.startsWith("id:") && filter.length() > "id:".length()) {
                        q.withIdentifier(StringUtils.trim(searchTerm.substring("id:".length())));
                    }
                }
            }

            // simple filter
            else if (filter.contains("/")) {
                q.withPathPrefix(filter);
            } else {
                q.withFulltext(true, filter);
            }

        }

        // Limit and Offset
        q.withLimit(limit);
        q.withOffset(offset);

        // Sort order
        if (StringUtils.equalsIgnoreCase("modified-asc", sort)) {
            q.sortByModificationDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("modified-desc", sort)) {
            q.sortByModificationDate(Order.Descending);
        } else if (StringUtils.equalsIgnoreCase("created-asc", sort)) {
            q.sortByCreationDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("created-desc", sort)) {
            q.sortByCreationDate(Order.Descending);
        } else if (StringUtils.equalsIgnoreCase("published-asc", sort)) {
            q.sortByPublishingDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("published-desc", sort)) {
            q.sortByPublishingDate(Order.Descending);
        }

        // Load the result
        String result = loadResultSet(q, details);

        return Response.ok(result).build();
    }

    /**
     * Returns a collection of pages that are defined as pending.
     * 
     * @param request
     *          the request
     * @param filter
     *          further search result filtering
     * @param sort
     *          sort order, possible values are
     *          <code>created-asc, created-desc, published-asc, published-desc, modified-asc & modified-desc</code>
     * @param limit
     *          search result limit
     * @param offset
     *          search result offset (for paging in combination with limit)
     * @param details
     *          switch for providing pages including their bodies
     * @return a collection of matching pages
     */
    @GET
    @Path("/pending")
    public Response getPending(@Context HttpServletRequest request, @QueryParam("filter") String filter,
            @QueryParam("sort") @DefaultValue("modified-desc") String sort,
            @QueryParam("limit") @DefaultValue("10") int limit, @QueryParam("offset") @DefaultValue("0") int offset,
            @QueryParam("details") @DefaultValue("false") boolean details) {

        // Create search query
        Site site = getSite(request);
        SearchQuery q = new SearchQueryImpl(site);
        q.withVersion(Resource.WORK);

        // Only take resources that have not been modified
        q.withoutPublication();

        // Type
        q.withTypes(Page.TYPE);

        // Filter query
        if (StringUtils.isNotBlank(filter))
            q.withFilter(filter);

        // Limit and Offset
        q.withLimit(limit);
        q.withOffset(offset);

        // Sort order
        if (StringUtils.equalsIgnoreCase("modified-asc", sort)) {
            q.sortByModificationDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("modified-desc", sort)) {
            q.sortByModificationDate(Order.Descending);
        } else if (StringUtils.equalsIgnoreCase("created-asc", sort)) {
            q.sortByCreationDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("created-desc", sort)) {
            q.sortByCreationDate(Order.Descending);
        } else if (StringUtils.equalsIgnoreCase("published-asc", sort)) {
            q.sortByPublishingDate(Order.Ascending);
        } else if (StringUtils.equalsIgnoreCase("published-desc", sort)) {
            q.sortByPublishingDate(Order.Descending);
        }

        // Load the result
        String result = loadResultSet(q, details);

        // Return the response
        return Response.ok(result).build();

    }

    /**
     * Returns the page with the given identifier or a <code>404</code> if the
     * page could not be found.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @return the page
     */
    @GET
    @Path("/{page}")
    public Response getPageById(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @QueryParam("version") @DefaultValue("0") long version) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Resolve name clash
        if ("docs".equals(pageId)) {
            return Response.ok(getDocumentation(request)).type(MediaType.TEXT_HTML).build();
        }

        // Load the page
        Page page = (Page) loadResource(request, pageId, Page.TYPE, version);
        if (page == null) {
            return Response.status(Status.NOT_FOUND).build();
        }

        // Is there an up-to-date, cached version on the client side?
        if (!ResourceUtils.hasChanged(request, page)) {
            return Response.notModified().build();
        }

        // Create the response
        ResponseBuilder response = Response.ok(page.toXml());
        response.tag(ResourceUtils.getETagValue(page));
        response.lastModified(ResourceUtils.getModificationDate(page));
        return response.build();
    }

    /**
     * Returns child pages of the page with the given identifier or a
     * <code>404</code> if the page could not be found.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @return the child pages
     */
    @GET
    @Path("/{page}/children")
    public Response getChildPagesByURI(@Context HttpServletRequest request, @PathParam("page") String pageId) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Load the page
        Page page = (Page) loadResource(request, pageId, Page.TYPE);
        if (page == null) {
            return Response.status(Status.NOT_FOUND).build();
        }

        Site site = getSite(request);
        SearchQuery q = new SearchQueryImpl(site);
        q.withVersion(Resource.LIVE);
        q.withTypes(Page.TYPE);
        q.withPathPrefix(page.getURI().getPath());

        ContentRepository repository = getContentRepository(site, false);
        SearchResult result = null;
        try {
            result = repository.find(q);
        } catch (ContentRepositoryException e) {
            return Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        StringBuffer buf = new StringBuffer("<pages>");
        for (SearchResultItem item : result.getItems()) {
            if (pageId.equals(item.getId()))
                continue;
            String headerXml = ((PageSearchResultItemImpl) item).getPageHeaderXml();
            buf.append(headerXml);
        }
        buf.append("</pages>");

        // Create the response
        return Response.ok(buf.toString()).build();
    }

    /**
     * Returns pages containing pagelets with properties of name
     * <code>resourceid</code> and a value equal to that of the page identifier.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @return the referring pages
     */
    @GET
    @Path("/{page}/referrer")
    public Response getReferencesByURI(@Context HttpServletRequest request, @PathParam("page") String pageId) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        Site site = getSite(request);
        SearchQuery q = new SearchQueryImpl(site);
        q.withTypes(Page.TYPE);
        q.withVersion(Resource.LIVE);
        q.withProperty("resourceid", pageId);

        ContentRepository repository = getContentRepository(site, false);
        SearchResult result = null;
        try {
            result = repository.find(q);
        } catch (ContentRepositoryException e) {
            return Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }

        StringBuffer buf = new StringBuffer("<pages>");
        for (SearchResultItem item : result.getItems()) {
            String headerXml = ((PageSearchResultItemImpl) item).getPageHeaderXml();
            buf.append(headerXml);
        }
        buf.append("</pages>");

        // Create the response
        return Response.ok(buf.toString()).build();
    }

    /**
     * Updates the indicated page.
     * 
     * @param request
     *          the http request
     * @param pageId
     *          the page identifier
     * @param pageXml
     *          the updated page
     * @param ifMatchHeader
     *          the page's <code>etag</code> value
     * @param asynchronous
     *          <code>true</code> to prevent blocking while content is being
     *          updated
     * @return response an empty response
     * @throws WebApplicationException
     *           if the update fails
     */
    @PUT
    @Path("/{page}")
    public Response updatePage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @FormParam("content") String pageXml, @HeaderParam("If-Match") String ifMatchHeader,
            @FormParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();
        if (pageXml == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Extract the site
        Site site = getSite(request);

        // Make sure the content repository is writable
        if (site.getContentRepository().isReadOnly()) {
            logger.warn("Attempt to write to read-only content repository {}", site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);
        ResourceURI workURI = new PageURIImpl(site, null, pageId, Resource.WORK);

        // Does the page exist?
        Page currentPage = null;
        try {
            currentPage = (Page) contentRepository.get(workURI);
            if (currentPage == null) {
                logger.warn("Attempt to update a page without creating a work version first");
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
            workURI.setPath(currentPage.getURI().getPath());
        } catch (ContentRepositoryException e) {
            logger.warn("Error lookup up page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Check the value of the If-Match header against the etag
        if (ifMatchHeader != null) {
            String etag = ResourceUtils.getETagValue(currentPage);
            if (!etag.equals(ifMatchHeader)) {
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has editing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.EDITOR))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (currentPage.isLocked() && (!currentPage.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Parse the page and update it in the repository
        Page page = null;
        try {
            PageReader pageReader = new PageReader();
            page = pageReader.read(IOUtils.toInputStream(pageXml, "utf-8"), site);
            if (StringUtils.isBlank(page.getURI().getPath()))
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            page.setModified(user, new Date());
            page.setVersion(Resource.WORK);

            // TODO: Preview generation disabled due to performance problems
            contentRepository.putAsynchronously(page, false);

            // Check if the page has been moved
            String currentPath = currentPage.getURI().getPath();
            String newPath = page.getURI().getPath();
            if ((currentPath != null && !currentPath.equals(newPath) || (currentPath == null && newPath != null))) {
                contentRepository.moveAsynchronously(currentPage.getURI(), newPath, true);
            }
        } catch (SecurityException e) {
            logger.warn("Tried to update page {} of site '{}' without permission", workURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error reading updated page {} from request", workURI);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (ParserConfigurationException e) {
            logger.warn("Error configuring parser to read updated page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (SAXException e) {
            logger.warn("Error parsing updated page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.BAD_REQUEST);
        } catch (IllegalStateException e) {
            logger.warn("Error updating page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error updating page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.ok();
        response.tag(ResourceUtils.getETagValue(page));
        response.lastModified(ResourceUtils.getModificationDate(page));
        return response.build();
    }

    /**
     * Creates a page at the site's content repository and returns the location to
     * post updates to.
     * <p>
     * Note that any of identifier, path and version that may be contained in an
     * initial <code>pageXml</code> will be overwritten.
     * 
     * @param request
     *          the http request
     * @param pageXml
     *          the new page
     * @param path
     *          the path to store the page at
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          created in the database
     * @return response the page location
     */
    @POST
    @Path("/")
    public Response createPage(@Context HttpServletRequest request, @FormParam("content") String pageXml,
            @FormParam("path") String path, @FormParam("asynchronous") boolean asynchronous) {

        Site site = getSite(request);
        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);

        // Create the page uri
        ResourceURIImpl pageURI = null;
        String uuid = UUID.randomUUID().toString();
        if (!StringUtils.isBlank(path)) {
            try {
                if (!path.startsWith("/"))
                    path = "/" + path;
                WebUrl url = new WebUrlImpl(site, path);
                path = url.getPath();
            } catch (IllegalArgumentException e) {
                logger.warn("Tried to create a page with an invalid path '{}': {}", path, e.getMessage());
                throw new WebApplicationException(Status.BAD_REQUEST);
            }
        } else {
            path = "/" + uuid.replaceAll("-", "");
        }

        pageURI = new PageURIImpl(site, path, uuid, Resource.WORK);

        // Make sure the page doesn't exist
        try {
            if (contentRepository.existsInAnyVersion(new GeneralResourceURIImpl(site, pageURI.getPath()))) {
                logger.warn("Tried to create already existing page {} in site '{}'", pageURI, site);
                throw new WebApplicationException(Status.CONFLICT);
            }
        } catch (IllegalArgumentException e) {
            logger.warn("Tried to create a page with an invalid path '{}': {}", path, e.getMessage());
            throw new WebApplicationException(Status.BAD_REQUEST);
        } catch (ContentRepositoryException e) {
            logger.warn("Page lookup {} failed for site '{}'", pageURI, site);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has editing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.EDITOR))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Parse the page and store it
        PageImpl page = null;
        URI uri = null;
        if (!StringUtils.isBlank(pageXml)) {
            logger.debug("Adding page to {}", pageURI);
            try {
                PageReader pageReader = new PageReader();
                page = pageReader.read(IOUtils.toInputStream(pageXml, "utf-8"), site);
                page.setIdentifier(pageURI.getIdentifier());
                page.setPath(pageURI.getPath());
                page.setVersion(pageURI.getVersion());
            } catch (IOException e) {
                logger.warn("Error reading page {} from request", pageURI);
                throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
            } catch (ParserConfigurationException e) {
                logger.warn("Error configuring parser to read updated page {}: {}", pageURI, e.getMessage());
                throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
            } catch (SAXException e) {
                logger.warn("Error parsing updated page {}: {}", pageURI, e.getMessage());
                throw new WebApplicationException(Status.BAD_REQUEST);
            }
        } else {
            logger.debug("Creating new page at {}", pageURI);
            page = new PageImpl(pageURI);
            page.setTemplate(site.getDefaultTemplate().getIdentifier());
            page.setCreated(user, new Date());
        }

        // Store the new page
        try {
            contentRepository.put(page, true);
            uri = new URI(UrlUtils.concat(request.getRequestURL().toString(), pageURI.getIdentifier()));
        } catch (URISyntaxException e) {
            logger.warn("Error creating a uri for page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (SecurityException e) {
            logger.warn("Tried to update page {} of site '{}' without permission", pageURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error writing new page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error adding new page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error adding new page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.created(uri);
        response.tag(ResourceUtils.getETagValue(page));
        response.lastModified(ResourceUtils.getModificationDate(page));
        return response.build();
    }

    /**
     * Removes the indicated page from the site.
     * 
     * @param request
     *          the http request
     * @param pageId
     *          the page identifier
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          deleted from the database
     * @return response an empty response
     */
    @DELETE
    @Path("/{page}")
    public Response deletePage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @QueryParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        Site site = getSite(request);
        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);

        ResourceURI livePageURI = new PageURIImpl(site, null, pageId, Resource.LIVE);
        ResourceURI workPageURI = new PageURIImpl(site, null, pageId, Resource.WORK);

        try {
            if (!contentRepository.existsInAnyVersion(livePageURI)) {
                logger.warn("Tried to delete non existing page {} in site '{}'", livePageURI, site);
                throw new WebApplicationException(Status.NOT_FOUND);
            }
        } catch (ContentRepositoryException e) {
            logger.warn("Page lookup {} failed for site '{}'", livePageURI, site);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        Page page = null;
        try {
            page = (Page) contentRepository.get(livePageURI);
            if (page != null) {
                livePageURI.setPath(page.getURI().getPath());
            } else {
                page = (Page) contentRepository.get(workPageURI);
                workPageURI.setPath(page.getURI().getPath());
            }
        } catch (ContentRepositoryException e) {
            logger.warn("Error lookup up page {} from repository: {}", livePageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has editing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.EDITOR))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // If the page is published, the user needs publishing rights
        if (page.isPublished() && !SecurityUtils.userHasRole(user, SystemRole.PUBLISHER))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (page.isLocked() && (!page.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Delete the page
        try {
            contentRepository.delete(page.getURI(), true);
        } catch (SecurityException e) {
            logger.warn("Tried to delete page {} of site '{}' without permission", livePageURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (ReferentialIntegrityException e) {
            logger.warn("Tried to delete referenced page {} of site '{}'", livePageURI, site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (IOException e) {
            logger.warn("Error deleting page {} from site '{}': {}",
                    new Object[] { livePageURI, site, e.getMessage() });
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (ContentRepositoryException e) {
            logger.warn("Error deleting page {} from site '{}': {}",
                    new Object[] { livePageURI, site, e.getMessage() });
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        return Response.ok().build();
    }

    /**
     * Returns the composer specified by <code>composerId</code> and
     * <code>pageletIndex</code> or a <code>404</code> if either the page or the
     * composer does not exist.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param composerId
     *          the composer identifier
     * @return the composer
     */
    @GET
    @Path("/{page}/composers/{composer}")
    public Response getComposer(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @PathParam("composer") String composerId, @QueryParam("version") @DefaultValue("0") long version) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();
        else if (composerId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Load the page
        Page page = (Page) loadResource(request, pageId, Page.TYPE, version);
        if (page == null) {
            return Response.status(Status.NOT_FOUND).build();
        }

        // Is there an up-to-date, cached version on the client side?
        if (!ResourceUtils.hasChanged(request, page)) {
            return Response.notModified().build();
        }

        // Load the composer
        Composer composer = page.getComposer(composerId);
        if (composer == null) {
            return Response.status(Status.NOT_FOUND).build();
        }

        // Return the composer
        return Response.ok(composer.toXml()).lastModified(ResourceUtils.getModificationDate(page)).build();
    }

    /**
     * Returns the pagelet specified by <code>pageId</code>,
     * <code>composerId</code> and <code>pageletIndex</code> or a <code>404</code>
     * if either of the the page, the composer or the pagelet does not exist.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param composerId
     *          the composer identifier
     * @param pageletIndex
     *          the pagelet index within the composer
     * @return the pagelet
     */
    @GET
    @Path("/{page}/composers/{composer}/pagelets/{pageletindex}")
    public Response getPagelet(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @PathParam("composer") String composerId, @PathParam("pageletindex") int pageletIndex,
            @QueryParam("version") @DefaultValue("0") long version) {

        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();
        else if (composerId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Load the page
        Page page = (Page) loadResource(request, pageId, Page.TYPE, version);
        if (page == null) {
            return Response.status(Status.NOT_FOUND).build();
        }

        // Is there an up-to-date, cached version on the client side?
        if (!ResourceUtils.hasChanged(request, page)) {
            return Response.notModified().build();
        }

        // Load the composer
        Composer composer = page.getComposer(composerId);
        if (composer == null || composer.size() < pageletIndex) {
            return Response.status(Status.NOT_FOUND).build();
        }

        // Return the pagelet
        ResponseBuilder response = Response.ok(composer.getPagelet(pageletIndex).toXml());
        response.lastModified(ResourceUtils.getModificationDate(page));
        return response.build();
    }

    /**
     * Locks the page and returns with a <code>200</code> status code if the lock
     * operation succeeds, <code>400</code> if the page is not found or
     * <code>403</code> if another user has already locked the page.
     * <p>
     * If <code>user</code> is not specified, then the current user will be used
     * for lock acquisition.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          locked
     * @return the page
     */
    @PUT
    @Path("/{page}/lock")
    public Response lockPage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @HeaderParam("If-Match") String ifMatchHeader, @FormParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Extract the site
        Site site = getSite(request);

        // Make sure the content repository is writable
        if (site.getContentRepository().isReadOnly()) {
            logger.warn("Attempt to lock a page in a read-only content repository {}", site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);
        ResourceURI workURI = new PageURIImpl(site, null, pageId, Resource.WORK);

        // Does the page exist at all?
        Page workPage = null;
        try {
            if (!contentRepository.existsInAnyVersion(workURI))
                throw new WebApplicationException(Status.NOT_FOUND);
        } catch (ContentRepositoryException e) {
            logger.warn("Error looking up page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Make sure we have a work page. If it doesn't exist yet, it needs
        // to be created as a result of the lock operation
        ResourceURI liveURI = new PageURIImpl(site, null, pageId, Resource.LIVE);
        try {
            workPage = (Page) contentRepository.get(workURI);
            if (workPage == null) {
                logger.debug("Creating work version of {}", liveURI);
                PageReader reader = new PageReader();
                Page livePage = (Page) contentRepository.get(liveURI);
                workPage = reader.read(IOUtils.toInputStream(livePage.toXml(), "utf-8"), site);
                workPage.setVersion(Resource.WORK);
                contentRepository.putAsynchronously(workPage, false);
            } else {
                workURI.setPath(workPage.getURI().getPath());
            }
        } catch (ContentRepositoryException e) {
            logger.warn("Error lookup up page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error putting a page work copy {} to repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (IOException e) {
            logger.warn("Error putting a page work copy {} to repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (ParserConfigurationException e) {
            logger.warn("Error reading live page {} from repository: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (SAXException e) {
            logger.warn("Error reading live page {} from repository: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Check the value of the If-Match header against the etag
        if (ifMatchHeader != null) {
            String etag = ResourceUtils.getETagValue(workPage);
            if (!etag.equals(ifMatchHeader)) {
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has editing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.EDITOR))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (workPage.isLocked() && (!workPage.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Finally, perform the lock operation (on all resource versions)
        try {
            contentRepository.lockAsynchronously(workURI, user);
            logger.info("Page {} has been locked by {}", workURI, user);
        } catch (SecurityException e) {
            logger.warn("Tried to lock page {} of site '{}' without permission", workURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error writing page lock {} to repository", workURI);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error locking page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error locking page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.ok();
        response.tag(ResourceUtils.getETagValue(workPage));
        response.lastModified(workPage.getLastModified());
        return response.build();
    }

    /**
     * Unlocks the page and returns with a <code>200</code> status code if the
     * unlock operation succeeds, <code>400</code> if the page is not found or
     * <code>403</code> if the page is locked by a different user.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          unlocked
     * @return the page
     */
    @DELETE
    @Path("/{page}/lock")
    public Response unlockPage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @HeaderParam("If-Match") String ifMatchHeader, @QueryParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Extract the site
        Site site = getSite(request);

        // Make sure the content repository is writable
        if (site.getContentRepository().isReadOnly()) {
            logger.warn("Attempt to unlock a page in a read-only content repository {}", site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);
        ResourceURI pageURI = new PageURIImpl(site, null, pageId, Resource.WORK);

        // Does the page exist?
        Page page = null;
        try {
            ResourceURI[] versions = contentRepository.getVersions(pageURI);
            if (versions.length == 0)
                throw new WebApplicationException(Status.NOT_FOUND);
            page = (Page) contentRepository.get(versions[0]);
            if (page == null)
                throw new WebApplicationException(Status.NOT_FOUND);
            pageURI.setPath(page.getURI().getPath());
        } catch (ContentRepositoryException e) {
            logger.warn("Error lookup up page {} from repository: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Check the value of the If-Match header against the etag
        if (ifMatchHeader != null) {
            String etag = ResourceUtils.getETagValue(page);
            if (!etag.equals(ifMatchHeader)) {
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has editing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.EDITOR))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (page.isLocked() && (!page.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Finally, perform the lock operation (on all resource versions)
        try {
            contentRepository.unlockAsynchronously(pageURI, user);
            logger.info("Page {} has been unlocked by {}", pageURI, user);
        } catch (SecurityException e) {
            logger.warn("Tried to unlock page {} of site '{}' without permission", pageURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error removing page lock {} from repository", pageURI);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error unlocking page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error unlocking page {}: {}", pageURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.ok();
        response.tag(ResourceUtils.getETagValue(page));
        response.lastModified(ResourceUtils.getModificationDate(page));
        return response.build();
    }

    /**
     * Publishes the page for the given date range and returns with a
     * <code>200</code> status code if the publish operation succeeds,
     * <code>400</code> if the page is not found or <code>403</code> if the page
     * is currently locked by a different user.
     * <p>
     * If <code>startdate</code> is not specified, then the page will be published
     * immediately. A missing <code>enddate</code> indicates to publish the page
     * forever.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param startDateText
     *          the optional publishing start date
     * @param endDateText
     *          the optional publishing end date
     * @param modified
     *          <code>true</code> to update the page's modified date
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          published
     * @return the page
     */
    @PUT
    @Path("/{page}/publish")
    public Response publishPage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @FormParam("startdate") String startDateText, @FormParam("enddate") String endDateText,
            @HeaderParam("If-Match") String ifMatchHeader, @FormParam("modified") boolean setModified,
            @FormParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Extract the site
        Site site = getSite(request);

        // Make sure the content repository is writable
        if (site.getContentRepository().isReadOnly()) {
            logger.warn("Attempt to publish a page in a read-only content repository {}", site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);
        ResourceURI workURI = new PageURIImpl(site, null, pageId, Resource.WORK);

        // Does the work page exist?
        Page workPage = null;
        try {
            if (!contentRepository.existsInAnyVersion(workURI))
                throw new WebApplicationException(Status.NOT_FOUND);
            workPage = (Page) contentRepository.get(workURI);
            if (workPage == null)
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            workURI.setPath(workPage.getURI().getPath());
        } catch (ContentRepositoryException e) {
            logger.warn("Error looking up page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Check the value of the If-Match header against the etag
        if (ifMatchHeader != null) {
            String etag = ResourceUtils.getETagValue(workPage);
            if (!etag.equals(ifMatchHeader)) {
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
        }

        // Make sure the page does not contain references to resources that don't
        // exist anymore.
        logger.debug("Checking referenced resources on {}", workPage);
        try {
            for (Pagelet pagelet : workPage.getPagelets()) {
                String resourceId = pagelet.getProperty("resourceid");
                if (StringUtils.isEmpty(resourceId))
                    continue;
                ResourceURI resourceURI = contentRepository.getResourceURI(resourceId);
                if (resourceURI == null) {
                    logger.warn("Page {} references non existing resource '{}'", workPage, resourceId);
                    throw new WebApplicationException(Status.PRECONDITION_FAILED);
                }
                resourceURI.setVersion(Resource.LIVE);
                if (!contentRepository.exists(resourceURI)) {
                    logger.warn("Page {} references unpublished resource '{}'", workPage, resourceURI);
                    throw new WebApplicationException(Status.PRECONDITION_FAILED);
                }
            }
        } catch (ContentRepositoryException e) {
            logger.warn("Error looking up referenced resources", e);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has publishing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.PUBLISHER))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (workPage.isLocked() && (!workPage.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Fix the dates
        Date startDate = null;
        Date endDate = null;
        DateFormat df = new SimpleDateFormat();

        // Parse the start date
        if (StringUtils.isNotBlank(startDateText)) {
            try {
                startDate = df.parse(startDateText);
            } catch (ParseException e) {
                try {
                    startDate = WebloungeDateFormat.parseStatic(startDateText);
                } catch (ParseException e2) {
                    throw new WebApplicationException(Status.BAD_REQUEST);
                }
            }
        } else {
            startDate = new Date();
        }

        // Parse the end date
        if (StringUtils.isNotBlank(endDateText)) {
            try {
                endDate = df.parse(endDateText);
            } catch (ParseException e) {
                try {
                    endDate = WebloungeDateFormat.parseStatic(endDateText);
                } catch (ParseException e2) {
                    throw new WebApplicationException(Status.BAD_REQUEST);
                }
            }
        }

        // Finally, perform the publish operation
        try {
            PageReader reader = new PageReader();
            Page livePage = reader.read(IOUtils.toInputStream(workPage.toXml(), "utf-8"), site);
            livePage.setVersion(Resource.LIVE);
            if (setModified)
                livePage.setModified(user, new Date());
            if (!livePage.isPublished())
                livePage.setPublished(user, startDate, endDate);
            contentRepository.putAsynchronously(livePage);
            contentRepository.deleteAsynchronously(workURI);
            logger.info("Page {} has been published by {}", workURI, user);
        } catch (SecurityException e) {
            logger.warn("Tried to publish page {} of site '{}' without permission", workURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error writing published page {} to repository", workURI);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error publishing page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error publishing page {}: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (ParserConfigurationException e) {
            logger.warn("Error reading work page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (SAXException e) {
            logger.warn("Error reading work page {} from repository: {}", workURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.ok();
        response.tag(ResourceUtils.getETagValue(workPage));
        response.lastModified(workPage.getLastModified());
        return response.build();
    }

    /**
     * Unpublishes the page on the given date and returns with a <code>200</code>
     * status code if the unlock operation succeeds, <code>400</code> if the page
     * is not found or <code>403</code> if the page is locked by a different user.
     * 
     * @param request
     *          the request
     * @param pageId
     *          the page identifier
     * @param enddate
     *          the optional publishing end date
     * @param asynchronous
     *          <code>true</code> to prevent blocking while the page is being
     *          unpublished
     * @return the page
     */
    @DELETE
    @Path("/{page}/publish")
    public Response unpublishPage(@Context HttpServletRequest request, @PathParam("page") String pageId,
            @HeaderParam("If-Match") String ifMatchHeader, @QueryParam("asynchronous") boolean asynchronous) {

        // Check the parameters
        if (pageId == null)
            return Response.status(Status.BAD_REQUEST).build();

        // Extract the site
        Site site = getSite(request);

        // Make sure the content repository is writable
        if (site.getContentRepository().isReadOnly()) {
            logger.warn("Attempt to unlock a page in a read-only content repository {}", site);
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        WritableContentRepository contentRepository = (WritableContentRepository) getContentRepository(site, true);
        ResourceURI liveURI = new PageURIImpl(site, null, pageId, Resource.LIVE);

        // Does the page exist?
        Page livePage = null;
        try {
            if (!contentRepository.existsInAnyVersion(liveURI))
                throw new WebApplicationException(Status.NOT_FOUND);
            livePage = (Page) contentRepository.get(liveURI);
            if (livePage == null)
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            liveURI.setPath(livePage.getURI().getPath());
        } catch (ContentRepositoryException e) {
            logger.warn("Error lookup up page {} from repository: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error unpublishing page {}: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }

        // Check the value of the If-Match header against the etag
        if (ifMatchHeader != null) {
            String etag = ResourceUtils.getETagValue(livePage);
            if (!etag.equals(ifMatchHeader)) {
                throw new WebApplicationException(Status.PRECONDITION_FAILED);
            }
        }

        // Get the user
        User user = securityService.getUser();
        if (user == null)
            throw new WebApplicationException(Status.UNAUTHORIZED);

        // Make sure the user has publishing rights
        if (!SecurityUtils.userHasRole(user, SystemRole.PUBLISHER))
            throw new WebApplicationException(Status.UNAUTHORIZED);

        boolean isAdmin = SecurityUtils.userHasRole(user, SystemRole.SITEADMIN);

        // If the page is locked by a different user, refuse
        if (livePage.isLocked() && (!livePage.getLockOwner().equals(user) && !isAdmin)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        // Finally, perform the unpublish operation, including saving the current
        // live version of the page as the new work version.
        try {
            contentRepository.delete(liveURI);
            ResourceURI workURI = new ResourceURIImpl(liveURI, Resource.WORK);
            if (!contentRepository.exists(workURI)) {
                logger.debug("Creating work version of {}", workURI);
                PageReader reader = new PageReader();
                Page workPage = reader.read(IOUtils.toInputStream(livePage.toXml(), "utf-8"), site);
                workPage.setVersion(Resource.WORK);
                workPage.setPublished(null, null, null);
                contentRepository.putAsynchronously(workPage);
            }
            logger.info("Page {} has been unpublished by {}", liveURI, user);
        } catch (SecurityException e) {
            logger.warn("Tried to unpublish page {} of site '{}' without permission", liveURI, site);
            throw new WebApplicationException(Status.FORBIDDEN);
        } catch (IOException e) {
            logger.warn("Error removing writing unpublished page {} to repository", liveURI);
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (IllegalStateException e) {
            logger.warn("Error unpublishing page {}: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        } catch (ContentRepositoryException e) {
            logger.warn("Error unpublishing page {}: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (ParserConfigurationException e) {
            logger.warn("Error reading live page {} from repository: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        } catch (SAXException e) {
            logger.warn("Error reading live page {} from repository: {}", liveURI, e.getMessage());
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }

        // Create the response
        ResponseBuilder response = Response.ok();
        response.tag(ResourceUtils.getETagValue(livePage));
        response.lastModified(ResourceUtils.getModificationDate(livePage));
        return response.build();
    }

    /**
     * Returns the endpoint documentation.
     * 
     * @return the endpoint documentation
     */
    @GET
    @Path("/docs")
    @Produces(MediaType.TEXT_HTML)
    public String getDocumentation(@Context HttpServletRequest request) {
        if (docs == null) {
            String docsPath = request.getRequestURI();
            String docsPathExtension = request.getPathInfo();
            String servicePath = request.getRequestURI().substring(0,
                    docsPath.length() - docsPathExtension.length());
            docs = PagesEndpointDocs.createDocumentation(servicePath);
        }
        return docs;
    }

    /**
     * Callback from OSGi to set the security service.
     * 
     * @param securityService
     *          the security service
     */
    void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    /**
     * Loads the pages from the site's content repository.
     * 
     * @param q
     *          the search query
     * @param details
     *          whether to display detailed information or just the header
     * @return the files
     * @throws WebApplicationException
     *           if the content repository is unavailable or if the content can't
     *           be loaded
     */
    private String loadResultSet(SearchQuery q, boolean details) throws WebApplicationException {
        ContentRepository repository = getContentRepository(q.getSite(), false);
        if (repository == null)
            throw new WebApplicationException(Status.SERVICE_UNAVAILABLE);

        SearchResult result = null;
        Page pageByPath = null;
        try {
            if (q.getVersion() == Resource.WORK && q.getPath() != null) {
                ResourceURI uri = new PageURIImpl(q.getSite(), q.getPath(), q.getVersion());
                pageByPath = (Page) repository.get(uri);
                int count = pageByPath != null ? 1 : 0;
                result = new SearchResultImpl(q, count, count);
            } else {
                result = repository.find(q);
            }
        } catch (ContentRepositoryException e) {
            throw new WebApplicationException(e);
        }

        StringBuffer buf = new StringBuffer("<pages ");
        buf.append("hits=\"").append(result.getHitCount()).append("\" ");
        buf.append("offset=\"").append(result.getOffset()).append("\" ");
        if (q.getLimit() > 0)
            buf.append("limit=\"").append(result.getLimit()).append("\" ");
        buf.append("page=\"").append(result.getPage()).append("\" ");
        buf.append("pagesize=\"").append(result.getPageSize()).append("\"");
        buf.append(">");
        if (pageByPath != null) {
            String xml = pageByPath.toXml();
            if (!details) {
                xml = xml.replaceAll("<body>.*</body>", "");
                xml = xml.replaceAll("<body/>", "");
            }
            buf.append(xml);
        } else {
            for (SearchResultItem item : result.getItems()) {
                String xml = null;
                if (details)
                    xml = ((PageSearchResultItemImpl) item).getResourceXml();
                else
                    xml = ((PageSearchResultItemImpl) item).getPageHeaderXml();
                buf.append(xml);
            }
        }
        buf.append("</pages>");

        return buf.toString();
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "Pages rest endpoint";
    }

}