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

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.contentrepository.impl.endpoint.PreviewsEndpoint.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 static ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils.DEFAULT_PREVIEW_FORMAT;

import ch.entwine.weblounge.common.content.PreviewGenerator;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceContent;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
import ch.entwine.weblounge.common.impl.request.RequestUtils;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.UnknownLanguageException;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ResourceSerializer;
import ch.entwine.weblounge.common.repository.ResourceSerializerService;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.ImageScalingMode;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.service.component.ComponentContext;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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.ws.rs.core.StreamingOutput;

/**
 * This class implements the <code>REST</code> endpoint for resource previews.
 */
@Path("/")
public class PreviewsEndpoint extends ContentRepositoryEndpoint {

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

    /** The list of image styles */
    private final List<ImageStyle> styles = new ArrayList<ImageStyle>();

    /** The request environment */
    protected Environment environment = Environment.Production;

    /** The resource serializer service */
    private ResourceSerializerService serializerService = null;

    /**
     * OSGi callback on component inactivation.
     * 
     * @param ctx
     *          the component context
     */
    void deactivate(ComponentContext ctx) {
        styles.clear();
    }

    /**
     * Returns the resource with the given identifier and styled using the
     * requested image style or a <code>404</code> if the resource or the resource
     * content could not be found.
     * <p>
     * If the content is not available in the requested language, the original
     * language version is used.
     * 
     * @param request
     *          the request
     * @param resourceId
     *          the resource identifier
     * @param languageId
     *          the language identifier
     * @param styleId
     *          the image style identifier
     * @return the image
     */
    @GET
    @Path("/{resource}/locales/{language}/styles/{style}")
    public Response getPreview(@Context HttpServletRequest request, @PathParam("resource") String resourceId,
            @PathParam("language") String languageId, @PathParam("style") String styleId,
            @QueryParam("version") @DefaultValue("0") long version,
            @QueryParam("force") @DefaultValue("false") boolean force) {

        // Check the parameters
        if (resourceId == null)
            throw new WebApplicationException(Status.BAD_REQUEST);

        // Get the resource
        final Site site = getSite(request);
        final Resource<?> resource = loadResource(request, resourceId, null, version);
        if (resource == null)
            throw new WebApplicationException(Status.NOT_FOUND);

        // Extract the language
        Language language = null;
        try {
            language = LanguageUtils.getLanguage(languageId);
            if (!resource.supportsLanguage(language)) {
                if (!resource.contents().isEmpty())
                    language = resource.getOriginalContent().getLanguage();
                else if (resource.supportsLanguage(site.getDefaultLanguage()))
                    language = site.getDefaultLanguage();
                else if (resource.languages().size() == 1)
                    language = resource.languages().iterator().next();
                else
                    throw new WebApplicationException(Status.NOT_FOUND);
            }
        } catch (UnknownLanguageException e) {
            throw new WebApplicationException(Status.BAD_REQUEST);
        }

        // Search the site for the image style
        ImageStyle style = null;
        for (Module m : site.getModules()) {
            style = m.getImageStyle(styleId);
            if (style != null) {
                break;
            }
        }

        // Search the global styles
        if (style == null) {
            for (ImageStyle s : styles) {
                if (s.getIdentifier().equals(styleId)) {
                    style = s;
                    break;
                }
            }
        }

        // The image style was not found
        if (style == null)
            throw new WebApplicationException(Status.BAD_REQUEST);

        // Load the input stream from the scaled image
        File scaledResourceFile = ImageStyleUtils.getScaledFile(resource, language, style);

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

        ResourceURI resourceURI = resource.getURI();
        final ContentRepository contentRepository = getContentRepository(site, false);

        // When there is no scaling required, just return the original
        if (ImageScalingMode.None.equals(style.getScalingMode())) {
            return getResourceContent(request, resource, language);
        }

        // Find a serializer
        ResourceSerializer<?, ?> serializer = serializerService.getSerializerByType(resourceURI.getType());
        if (serializer == null)
            throw new WebApplicationException(Status.PRECONDITION_FAILED);

        // Does the serializer come with a preview generator?
        PreviewGenerator previewGenerator = serializer.getPreviewGenerator(resource);
        if (previewGenerator == null)
            throw new WebApplicationException(Status.NOT_FOUND);

        // Load the resource contents from the repository
        InputStream resourceInputStream = null;
        long contentLength = -1;

        // Load the input stream from the scaled image
        InputStream contentRepositoryIs = null;
        FileOutputStream fos = null;
        try {
            long resourceLastModified = ResourceUtils.getModificationDate(resource, language).getTime();
            if (!scaledResourceFile.isFile() || scaledResourceFile.lastModified() < resourceLastModified) {
                if (!force)
                    throw new WebApplicationException(Response.Status.NOT_FOUND);

                contentRepositoryIs = contentRepository.getContent(resourceURI, language);
                scaledResourceFile = ImageStyleUtils.createScaledFile(resource, language, style);
                scaledResourceFile.setLastModified(Math.max(new Date().getTime(), resourceLastModified));
                fos = new FileOutputStream(scaledResourceFile);
                logger.debug("Creating scaled image '{}' at {}", resource, scaledResourceFile);

                previewGenerator.createPreview(resource, environment, language, style, DEFAULT_PREVIEW_FORMAT,
                        contentRepositoryIs, fos);
                if (scaledResourceFile.length() == 0) {
                    logger.debug("Error scaling '{}': file size is 0", resourceURI);
                    IOUtils.closeQuietly(resourceInputStream);
                    FileUtils.deleteQuietly(scaledResourceFile);
                }
            }

            // Did scaling work? If not, cleanup and tell the user
            if (scaledResourceFile.length() == 0)
                throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);

            // The scaled resource should now exist
            resourceInputStream = new FileInputStream(scaledResourceFile);
            contentLength = scaledResourceFile.length();

        } catch (WebApplicationException e) {
            IOUtils.closeQuietly(resourceInputStream);
            FileUtils.deleteQuietly(scaledResourceFile);
            if (scaledResourceFile != null)
                deleteIfEmpty(scaledResourceFile.getParentFile());
            throw e;
        } catch (ContentRepositoryException e) {
            logger.error("Error loading {} image '{}' from {}: {}",
                    new Object[] { language, resource, contentRepository, e.getMessage() });
            logger.error(e.getMessage(), e);
            IOUtils.closeQuietly(resourceInputStream);
            FileUtils.deleteQuietly(scaledResourceFile);
            if (scaledResourceFile != null)
                deleteIfEmpty(scaledResourceFile.getParentFile());
            throw new WebApplicationException();
        } catch (IOException e) {
            logger.error("Error scaling image '{}': {}", resourceURI, e.getMessage());
            IOUtils.closeQuietly(resourceInputStream);
            FileUtils.deleteQuietly(scaledResourceFile);
            if (scaledResourceFile != null)
                deleteIfEmpty(scaledResourceFile.getParentFile());
            throw new WebApplicationException();
        } catch (IllegalArgumentException e) {
            logger.error("Image '{}' is of unsupported format: {}", resourceURI, e.getMessage());
            IOUtils.closeQuietly(resourceInputStream);
            FileUtils.deleteQuietly(scaledResourceFile);
            if (scaledResourceFile != null)
                deleteIfEmpty(scaledResourceFile.getParentFile());
            throw new WebApplicationException();
        } catch (Throwable t) {
            logger.error("Error scaling image '{}': {}", resourceURI, t.getMessage());
            IOUtils.closeQuietly(resourceInputStream);
            FileUtils.deleteQuietly(scaledResourceFile);
            if (scaledResourceFile != null)
                deleteIfEmpty(scaledResourceFile.getParentFile());
            throw new WebApplicationException();
        } finally {
            IOUtils.closeQuietly(contentRepositoryIs);
            IOUtils.closeQuietly(fos);
        }

        // Create the response
        final InputStream is = resourceInputStream;
        ResponseBuilder response = Response.ok(new StreamingOutput() {
            public void write(OutputStream os) throws IOException, WebApplicationException {
                try {
                    IOUtils.copy(is, os);
                    os.flush();
                } catch (IOException e) {
                    if (!RequestUtils.isCausedByClient(e))
                        logger.error("Error writing preview to client", e);
                } finally {
                    IOUtils.closeQuietly(is);
                }
            }
        });

        // Add mime type header
        String mimetype = previewGenerator.getContentType(resource, language, style);
        if (mimetype == null)
            mimetype = MediaType.APPLICATION_OCTET_STREAM;
        response.type(mimetype);

        // Add last modified header
        response.lastModified(new Date(scaledResourceFile.lastModified()));

        // Add ETag header
        String eTag = ResourceUtils.getETagValue(scaledResourceFile);
        response.tag(eTag);

        // Add filename header
        String filename = null;
        ResourceContent resourceContent = resource.getContent(language);
        if (resourceContent != null)
            filename = resourceContent.getFilename();
        if (StringUtils.isBlank(filename))
            filename = scaledResourceFile.getName();
        response.header("Content-Disposition", "inline; filename=" + filename);

        // Content length
        response.header("Content-Length", Long.toString(contentLength));

        // Send the response
        return response.build();
    }

    /**
     * Deletes the preview images for the given resource and language.
     * 
     * @param request
     *          the request
     * @param resourceId
     *          the resource identifier
     * @param languageId
     *          the language identifier
     */
    @POST
    @Path("/")
    public Response createPreviews(@Context HttpServletRequest request) {
        Site site = super.getSite(request);
        final ContentRepository contentRepository = getContentRepository(site, false);
        new Thread(new Runnable() {
            public void run() {
                try {
                    contentRepository.createPreviews();
                } catch (ContentRepositoryException e) {
                    logger.warn("Preview generation returned with an error: {}", e.getMessage());
                }
            }
        }).start();
        return Response.ok().build();
    }

    /**
     * Deletes all preview images.
     * 
     * @param request
     *          the request
     */
    @DELETE
    @Path("/")
    public Response removePreviews(@Context HttpServletRequest request) {
        Site site = super.getSite(request);
        File previewsDir = ImageStyleUtils.getDirectory(site);
        if (FileUtils.deleteQuietly(previewsDir))
            return Response.ok().build();
        else
            return Response.serverError().build();
    }

    /**
     * Deletes all preview images for the given resource.
     * 
     * @param request
     *          the request
     * @param resourceId
     *          the resource identifier
     */
    @DELETE
    @Path("/{resource}")
    public Response removePreviewsByStyle(@Context HttpServletRequest request,
            @PathParam("resource") String styleId) {
        return removePreview(request, null, null, null);
    }

    /**
     * Deletes the preview images for the given style and language.
     * 
     * @param request
     *          the request
     * @param resourceId
     *          the resource identifier
     * @param languageId
     *          the language identifier
     */
    @DELETE
    @Path("/styles/{style}")
    public Response removePreview(@Context HttpServletRequest request, @PathParam("style") String styleId) {
        Site site = super.getSite(request);

        // Check the parameters
        if (styleId == null)
            throw new WebApplicationException(Status.BAD_REQUEST);

        // Search the site for the image style
        ImageStyle style = null;
        for (Module m : site.getModules()) {
            style = m.getImageStyle(styleId);
            if (style != null) {
                break;
            }
        }

        // Search the global styles
        if (style == null) {
            for (ImageStyle s : styles) {
                if (s.getIdentifier().equals(styleId)) {
                    style = s;
                    break;
                }
            }
        }

        // The image style was not found
        if (style == null)
            throw new WebApplicationException(Status.BAD_REQUEST);

        File previewsDir = ImageStyleUtils.getDirectory(site, style);
        if (FileUtils.deleteQuietly(previewsDir))
            return Response.ok().build();
        else
            return Response.serverError().build();
    }

    /**
     * Deletes the preview images for the given resource, the language and image
     * style.
     * 
     * @param request
     *          the request
     * @param resourceId
     *          the resource identifier
     * @param languageId
     *          the language identifier
     * @param styleId
     *          the image style identifier
     */
    @DELETE
    @Path("/{resource}/locales/{language}/styles/{style}")
    public Response removePreview(@Context HttpServletRequest request, @PathParam("resource") String resourceId,
            @PathParam("language") String languageId, @PathParam("style") String styleId) {

        // Check the parameters
        if (resourceId == null)
            throw new WebApplicationException(Status.BAD_REQUEST);

        // Get the resource
        final Site site = getSite(request);
        final Resource<?> resource = loadResource(request, resourceId, null);
        if (resource == null)
            throw new WebApplicationException(Status.NOT_FOUND);

        // Extract the language
        List<Language> languages = new ArrayList<Language>();
        if (languageId != null) {
            try {
                languages.add(LanguageUtils.getLanguage(languageId));
            } catch (UnknownLanguageException e) {
                throw new WebApplicationException(Status.BAD_REQUEST);
            }
        } else {
            languages.addAll(resource.languages());
        }

        // Search the site for the image style (if applicable)
        List<ImageStyle> removeStyles = new ArrayList<ImageStyle>();
        if (styleId != null) {
            for (Module m : site.getModules()) {
                ImageStyle s = m.getImageStyle(styleId);
                if (s != null) {
                    removeStyles.add(s);
                    break;
                }
            }

            // Search the global styles
            if (removeStyles.size() > 0) {
                for (ImageStyle s : this.styles) {
                    if (s.getIdentifier().equals(styleId)) {
                        removeStyles.add(s);
                        break;
                    }
                }
            }
        } else {
            removeStyles.addAll(this.styles);
            for (Module m : site.getModules()) {
                removeStyles.addAll(Arrays.asList(m.getImageStyles()));
            }
        }

        ResourceURI resourceURI = resource.getURI();
        final ContentRepository contentRepository = getContentRepository(site, false);

        // Load the resource versions from the repository
        ResourceURI[] versions = null;
        try {
            versions = contentRepository.getVersions(resourceURI);
        } catch (ContentRepositoryException e1) {
            throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
        }

        // Remove the preview for all versions in the specified styles and languages
        for (ResourceURI u : versions) {
            for (ImageStyle style : removeStyles) {
                for (Language language : languages) {
                    deletePreview(resource, u.getVersion(), style, language);
                }
            }
        }

        // Send the response
        return Response.status(Status.OK).build();
    }

    /**
     * Deletes a single preview image.
     * 
     * @param resource
     *          the resource
     * @param version
     *          the resource version
     * @param style
     *          the image style
     * @param language
     *          the language
     */
    private void deletePreview(Resource<?> resource, long version, ImageStyle style, Language language) {

        // Find a serializer
        ResourceSerializer<?, ?> serializer = serializerService.getSerializerByType(resource.getURI().getType());
        if (serializer == null)
            throw new WebApplicationException(Status.PRECONDITION_FAILED);

        // Does the serializer come with a preview generator?
        PreviewGenerator previewGenerator = serializer.getPreviewGenerator(resource);
        if (previewGenerator == null)
            throw new WebApplicationException(Status.NOT_FOUND);

        // Load the input stream from the scaled image
        File scaledResourceFile = null;
        try {
            scaledResourceFile = ImageStyleUtils.getScaledFile(resource, language, style);
            if (scaledResourceFile.exists()) {
                logger.debug("Deleting preview at {}", scaledResourceFile);
                FileUtils.deleteQuietly(scaledResourceFile);
                File parentDir = scaledResourceFile.getParentFile();
                while (parentDir.isDirectory() && parentDir.list().length == 0) {
                    logger.debug("Deleting empty preview directory {}", parentDir);
                    FileUtils.deleteQuietly(parentDir);
                    parentDir = parentDir.getParentFile();
                }
            }
        } catch (Throwable t) {
            logger.error("Error removing preview image '{}': {}", resource.getURI(), t.getMessage());
            throw new WebApplicationException();
        }
    }

    /**
     * Deletes the directory if it is empty and tries the same for the parent
     * directory.
     * 
     * @param dir
     *          the directory
     */
    private void deleteIfEmpty(File dir) {
        while (dir != null && dir.isDirectory() && (dir.listFiles() == null || dir.listFiles().length == 0)) {
            FileUtils.deleteQuietly(dir);
            dir = dir.getParentFile();
        }
    }

    /**
     * Returns the list of image styles that are registered for a site.
     * 
     * @param request
     *          the request
     * @return the list of image styles
     */
    @GET
    @Produces(MediaType.TEXT_XML)
    @Path("/styles")
    public Response getImagestyles(@Context HttpServletRequest request) {
        Site site = getSite(request);
        if (site == null)
            throw new WebApplicationException(Status.NOT_FOUND);

        StringBuffer buf = new StringBuffer("<styles>");

        // Add styles of current site
        for (Module m : site.getModules()) {
            ImageStyle[] styles = m.getImageStyles();
            for (ImageStyle style : styles) {
                buf.append(style.toXml());
            }
        }

        // Add global styles
        for (ImageStyle style : styles) {
            buf.append(style.toXml());
        }

        buf.append("</styles>");

        ResponseBuilder response = Response.ok(buf.toString());
        return response.build();
    }

    /**
     * Returns the image styles or a <code>404</code>.
     * 
     * @param request
     *          the request
     * @param styleId
     *          the image style identifier
     * @return the image
     */
    @GET
    @Produces(MediaType.TEXT_XML)
    @Path("/styles/{style}")
    public Response getImagestyle(@Context HttpServletRequest request, @PathParam("style") String styleId) {
        Site site = getSite(request);

        // Search styles of current site
        for (Module m : site.getModules()) {
            ImageStyle style = m.getImageStyle(styleId);
            if (style != null) {
                ResponseBuilder response = Response.ok(style.toXml());
                return response.build();
            }
        }

        // Search global styles
        for (ImageStyle style : styles) {
            if (style.getIdentifier().equals(styleId)) {
                ResponseBuilder response = Response.ok(style.toXml());
                return response.build();
            }
        }

        // The image style was not found
        throw new WebApplicationException(Status.NOT_FOUND);
    }

    /**
     * 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 = PreviewsEndpointDocs.createDocumentation(servicePath);
        }
        return docs;
    }

    /**
     * Callback from OSGi declarative services on registration of a new image
     * style in the service registry.
     * 
     * @param style
     *          the image style
     */
    void addImageStyle(ImageStyle style) {
        styles.add(style);
    }

    /**
     * Callback from OSGi declarative services on removal of an image style from
     * the service registry.
     * 
     * @param style
     *          the image style
     */
    void removeImageStyle(ImageStyle style) {
        styles.remove(style);
    }

    /**
     * Callback from the OSGi environment when the environment becomes published.
     * 
     * @param environment
     *          the environment
     */
    void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * OSGi callback that is setting the resource serializer.
     * 
     * @param serializer
     *          the resource serializer service
     */
    void setResourceSerializer(ResourceSerializerService serializer) {
        this.serializerService = serializer;
    }

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

}