ch.entwine.weblounge.dispatcher.impl.handler.PreviewRequestHandlerImpl.java Source code

Java tutorial

Introduction

Here is the source code for ch.entwine.weblounge.dispatcher.impl.handler.PreviewRequestHandlerImpl.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.dispatcher.impl.handler;

import static ch.entwine.weblounge.common.Times.MS_PER_DAY;

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.ResourceURIImpl;
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.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.security.User;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.WebUrl;
import ch.entwine.weblounge.dispatcher.RequestHandler;
import ch.entwine.weblounge.dispatcher.impl.DispatchUtils;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;

/**
 * This request handler is used to handle requests to scaled images in the
 * repository.
 */
public final class PreviewRequestHandlerImpl implements RequestHandler {

    /** Alternate uri prefix */
    protected static final String URI_PREFIX = "/weblounge-previews/";

    /** Name of the image style parameter */
    protected static final String OPT_IMAGE_STYLE = "style";

    /** Length of a UUID */
    protected static final int UUID_LENGTH = 36;

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

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

    /** The preview generators */
    private final List<PreviewGenerator> previewGenerators = new ArrayList<PreviewGenerator>();

    /** The list of previews that are being created at the moment */
    private final List<String> previews = new ArrayList<String>();

    /**
     * Handles the request for an image resource that is believed to be in the
     * content repository. The handler scales the image as requested, sets the
     * response headers and the writes the image contents to the response.
     * <p>
     * This method returns <code>true</code> if the handler is decided to handle
     * the request, <code>false</code> otherwise.
     * 
     * @param request
     *          the weblounge request
     * @param response
     *          the weblounge response
     */
    public boolean service(WebloungeRequest request, WebloungeResponse response) {

        WebUrl url = request.getUrl();
        Site site = request.getSite();
        String path = url.getPath();
        String fileName = null;

        // This request handler can only be used with the prefix
        if (!path.startsWith(URI_PREFIX))
            return false;

        // Get hold of the content repository
        ContentRepository contentRepository = site.getContentRepository();
        if (contentRepository == null) {
            logger.warn("No content repository found for site '{}'", site);
            return false;
        } else if (contentRepository.isIndexing()) {
            logger.debug("Content repository of site '{}' is currently being indexed", site);
            DispatchUtils.sendServiceUnavailable(request, response);
            return true;
        }

        // Check if the request uri matches the special uri for previews. If so, try
        // to extract the id from the last part of the path. If not, check if there
        // is an image with the current path.
        ResourceURI resourceURI = null;
        Resource<?> resource = null;
        try {
            String id = null;
            String imagePath = null;

            String uriSuffix = StringUtils.chomp(path.substring(URI_PREFIX.length()), "/");
            uriSuffix = URLDecoder.decode(uriSuffix, "utf-8");

            // Check whether we are looking at a uuid or a url path
            if (uriSuffix.length() == UUID_LENGTH) {
                id = uriSuffix;
            } else if (uriSuffix.length() >= UUID_LENGTH) {
                int lastSeparator = uriSuffix.indexOf('/');
                if (lastSeparator == UUID_LENGTH && uriSuffix.indexOf('/', lastSeparator + 1) < 0) {
                    id = uriSuffix.substring(0, lastSeparator);
                    fileName = uriSuffix.substring(lastSeparator + 1);
                } else {
                    imagePath = uriSuffix;
                    fileName = FilenameUtils.getName(imagePath);
                }
            } else {
                imagePath = "/" + uriSuffix;
                fileName = FilenameUtils.getName(imagePath);
            }

            // Try to load the resource
            resourceURI = new ResourceURIImpl(null, site, imagePath, id);
            resource = contentRepository.get(resourceURI);
            if (resource == null) {
                logger.debug("No resource found at {}", resourceURI);
                return false;
            }
        } catch (ContentRepositoryException e) {
            logger.error("Error loading resource from {}: {}", contentRepository, e.getMessage());
            DispatchUtils.sendInternalError(request, response);
            return true;
        } catch (UnsupportedEncodingException e) {
            logger.error("Error decoding resource url {} using utf-8: {}", path, e.getMessage());
            DispatchUtils.sendInternalError(request, response);
            return true;
        }

        // Agree to serve the preview
        logger.debug("Preview handler agrees to handle {}", path);

        // Check the request method. Only GET is supported right now.
        String requestMethod = request.getMethod().toUpperCase();
        if ("OPTIONS".equals(requestMethod)) {
            String verbs = "OPTIONS,GET";
            logger.trace("Answering options request to {} with {}", url, verbs);
            response.setHeader("Allow", verbs);
            response.setContentLength(0);
            return true;
        } else if (!"GET".equals(requestMethod)) {
            logger.debug("Image request handler does not support {} requests", requestMethod);
            DispatchUtils.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, request, response);
            return true;
        }

        // Is it published?
        // TODO: Fix this. imageResource.isPublished() currently returns false,
        // as both from and to dates are null (see PublishingCtx)
        // if (!imageResource.isPublished()) {
        // logger.debug("Access to unpublished image {}", imageURI);
        // DispatchUtils.sendNotFound(request, response);
        // return true;
        // }

        // Can the resource be accessed by the current user?
        User user = request.getUser();
        try {
            // TODO: Check permission
            // PagePermission p = new PagePermission(page, user);
            // AccessController.checkPermission(p);
        } catch (SecurityException e) {
            logger.warn("Access to resource {} denied for user {}", resourceURI, user);
            DispatchUtils.sendAccessDenied(request, response);
            return true;
        }

        // Determine the response language by filename
        Language language = null;
        if (StringUtils.isNotBlank(fileName)) {
            for (ResourceContent c : resource.contents()) {
                if (c.getFilename().equalsIgnoreCase(fileName)) {
                    if (language != null) {
                        logger.debug("Unable to determine language from ambiguous filename");
                        language = LanguageUtils.getPreferredContentLanguage(resource, request, site);
                        break;
                    }
                    language = c.getLanguage();
                }
            }
            if (language == null)
                language = LanguageUtils.getPreferredContentLanguage(resource, request, site);
        } else {
            language = LanguageUtils.getPreferredContentLanguage(resource, request, site);
        }

        // If the filename did not lead to a language, apply language resolution
        if (language == null) {
            logger.warn("Resource {} does not exist in any supported language", resourceURI);
            DispatchUtils.sendNotFound(request, response);
            return true;
        }

        // Find a resource preview generator
        PreviewGenerator previewGenerator = null;
        synchronized (previewGenerators) {
            for (PreviewGenerator generator : previewGenerators) {
                if (generator.supports(resource)) {
                    previewGenerator = generator;
                    break;
                }
            }
        }

        // If we did not find a preview generator, we need to let go
        if (previewGenerator == null) {
            logger.debug("Unable to generate preview for {} since no suitable preview generator is available",
                    resource);
            DispatchUtils.sendServiceUnavailable(request, response);
            return true;
        }

        // Extract the image style
        ImageStyle style = null;
        String styleId = StringUtils.trimToNull(request.getParameter(OPT_IMAGE_STYLE));
        if (styleId != null) {
            style = ImageStyleUtils.findStyle(styleId, site);
            if (style == null) {
                DispatchUtils.sendBadRequest("Image style '" + styleId + "' not found", request, response);
                return true;
            }
        }

        // Get the path to the preview image
        File previewFile = ImageStyleUtils.getScaledFile(resource, language, style);

        // Check the modified headers
        long revalidationTime = MS_PER_DAY;
        long expirationDate = System.currentTimeMillis() + revalidationTime;
        if (!ResourceUtils.hasChanged(request, previewFile)) {
            logger.debug("Scaled preview {} was not modified", resourceURI);
            response.setDateHeader("Expires", expirationDate);
            DispatchUtils.sendNotModified(request, response);
            return true;
        }

        // Load the image contents from the repository
        ResourceContent resourceContents = resource.getContent(language);

        // Add mime type header
        String contentType = resourceContents.getMimetype();
        if (contentType == null)
            contentType = MediaType.APPLICATION_OCTET_STREAM;

        // Set the content type
        String characterEncoding = response.getCharacterEncoding();
        if (StringUtils.isNotBlank(characterEncoding))
            response.setContentType(contentType + "; charset=" + characterEncoding.toLowerCase());
        else
            response.setContentType(contentType);

        // Browser caches and proxies are allowed to keep a copy
        response.setHeader("Cache-Control", "public, max-age=" + revalidationTime);

        // Set Expires header
        response.setDateHeader("Expires", expirationDate);

        // Write the image back to the client
        InputStream previewInputStream = null;
        try {
            if (previewFile.isFile()
                    && previewFile.lastModified() >= resourceContents.getCreationDate().getTime()) {
                previewInputStream = new FileInputStream(previewFile);
            } else {
                previewInputStream = createPreview(request, response, resource, language, style, previewGenerator,
                        previewFile, contentRepository);
            }

            if (previewInputStream == null) {
                // Assuming that createPreview() is setting the response header in the
                // case of failure
                return true;
            }

            // Add last modified header
            response.setDateHeader("Last-Modified", previewFile.lastModified());
            response.setHeader("ETag", ResourceUtils.getETagValue(previewFile.lastModified()));
            response.setHeader("Content-Disposition", "inline; filename=" + previewFile.getName());
            response.setHeader("Content-Length", Long.toString(previewFile.length()));
            previewInputStream = new FileInputStream(previewFile);
            IOUtils.copy(previewInputStream, response.getOutputStream());
            response.getOutputStream().flush();
            return true;
        } catch (EOFException e) {
            logger.debug("Error writing image '{}' back to client: connection closed by client", resource);
            return true;
        } catch (IOException e) {
            DispatchUtils.sendInternalError(request, response);
            if (RequestUtils.isCausedByClient(e))
                return true;
            logger.error("Error sending image '{}' to the client: {}", resourceURI, e.getMessage());
            return true;
        } catch (Throwable t) {
            logger.error("Error creating scaled image '{}': {}", resourceURI, t.getMessage());
            DispatchUtils.sendInternalError(request, response);
            return true;
        } finally {
            IOUtils.closeQuietly(previewInputStream);
        }
    }

    /**
     * Creates the preview image for the given resource and returns an input
     * stream to the preview or <code>null</code> if the preview could not be
     * created.
     */
    private InputStream createPreview(WebloungeRequest request, WebloungeResponse response, Resource<?> resource,
            Language language, ImageStyle style, PreviewGenerator previewGenerator, File previewFile,
            ContentRepository contentRepository) {

        String pathToImageFile = previewFile.getAbsolutePath();
        boolean firstOne = true;

        // Make sure the preview is not already being generated by another thread
        synchronized (previews) {
            while (previews.contains(pathToImageFile)) {
                logger.debug("Preview at {} is being created, waiting for it to be generated", pathToImageFile);
                firstOne = false;
                try {
                    previews.wait(500);
                    if (previews.contains(pathToImageFile)) {
                        logger.trace("After waiting 500ms, preview at {} is still being worked on",
                                pathToImageFile);
                        DispatchUtils.sendServiceUnavailable(request, response);
                        return null;
                    }
                } catch (InterruptedException e) {
                    DispatchUtils.sendServiceUnavailable(request, response);
                    return null;
                }
            }

            // Make sure others are waiting until we are done
            if (firstOne) {
                previews.add(pathToImageFile);
            }
        }

        // Determine the resource's modification date
        long resourceLastModified = ResourceUtils.getModificationDate(resource, language).getTime();

        // Create the preview if this is the first request
        if (firstOne) {

            ResourceURI resourceURI = resource.getURI();

            if (style != null)
                logger.info("Creating preview of {} with style '{}' at {}",
                        new String[] { resource.getIdentifier(), style.getIdentifier(), pathToImageFile });
            else
                logger.info("Creating original preview of {} at {}",
                        new String[] { resource.getIdentifier(), pathToImageFile });

            // Get hold of the content
            ResourceContent resourceContents = resource.getContent(language);

            // Get the mime type
            final String mimetype = resourceContents.getMimetype();
            final String format = mimetype.substring(mimetype.indexOf("/") + 1);

            boolean scalingFailed = false;

            InputStream is = null;
            FileOutputStream fos = null;
            try {
                is = contentRepository.getContent(resourceURI, language);

                // Remove the original image
                FileUtils.deleteQuietly(previewFile);

                // Create a work file
                File imageDirectory = previewFile.getParentFile();
                String workFileName = "." + UUID.randomUUID() + "-" + previewFile.getName();
                FileUtils.forceMkdir(imageDirectory);
                File workImageFile = new File(imageDirectory, workFileName);

                // Create the scaled image
                fos = new FileOutputStream(workImageFile);
                logger.debug("Creating scaled image '{}' at {}", resource, previewFile);
                previewGenerator.createPreview(resource, environment, language, style, format, is, fos);

                // Move the work image in place
                try {
                    FileUtils.moveFile(workImageFile, previewFile);
                } catch (IOException e) {
                    logger.warn("Concurrent creation of preview {} resolved by copy instead of rename",
                            previewFile.getAbsolutePath());
                    FileUtils.copyFile(workImageFile, previewFile);
                    FileUtils.deleteQuietly(workImageFile);
                } finally {
                    previewFile.setLastModified(Math.max(new Date().getTime(), resourceLastModified));
                }

                // Make sure preview generation was successful
                if (!previewFile.isFile()) {
                    logger.warn("The file at {} is not a regular file", pathToImageFile);
                    scalingFailed = true;
                } else if (previewFile.length() == 0) {
                    logger.warn("The scaled file at {} has zero length", pathToImageFile);
                    scalingFailed = true;
                }

            } catch (ContentRepositoryException e) {
                logger.error("Unable to load image {}: {}", new Object[] { resourceURI, e.getMessage(), e });
                scalingFailed = true;
                DispatchUtils.sendInternalError(request, response);
            } catch (IOException e) {
                logger.error("Error sending image '{}' to the client: {}", resourceURI, e.getMessage());
                scalingFailed = true;
                DispatchUtils.sendInternalError(request, response);
            } catch (Throwable t) {
                logger.error("Error creating scaled image '{}': {}", resourceURI, t.getMessage());
                scalingFailed = true;
                DispatchUtils.sendInternalError(request, response);
            } finally {
                IOUtils.closeQuietly(is);
                IOUtils.closeQuietly(fos);

                try {
                    if (scalingFailed && previewFile != null) {
                        logger.info("Cleaning up after failed scaling of {}", pathToImageFile);
                        File f = previewFile;
                        FileUtils.deleteQuietly(previewFile);
                        f = previewFile.getParentFile();
                        while (f != null && f.isDirectory()
                                && (f.listFiles() == null || f.listFiles().length == 0)) {
                            FileUtils.deleteQuietly(f);
                            f = f.getParentFile();
                        }
                    }
                } catch (Throwable t) {
                    logger.warn("Error cleaning up after failed scaling of {}", pathToImageFile);
                }

                synchronized (previews) {
                    previews.remove(pathToImageFile);
                    previews.notifyAll();
                }

            }

        }

        // Make sure whoever was in charge of creating the preview, was
        // successful
        boolean scaledImageExists = previewFile.isFile();
        boolean scaledImageIsOutdated = previewFile.lastModified() < resourceLastModified;
        if (!scaledImageExists || scaledImageIsOutdated) {
            logger.debug("Apparently, preview rendering for {} failed", previewFile.getAbsolutePath());
            DispatchUtils.sendServiceUnavailable(request, response);
            return null;
        } else {
            try {
                return new FileInputStream(previewFile);
            } catch (Throwable t) {
                logger.error("Error reading content from preview at {}: {}", previewFile.getAbsolutePath(),
                        t.getMessage());
                DispatchUtils.sendServiceUnavailable(request, response);
                return null;
            }
        }
    }

    /**
     * Sets the server environment.
     * 
     * @param environment
     *          the server environment
     */
    void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * Adds the preview generator to the list of registered preview generators.
     * 
     * @param generator
     *          the generator
     */
    void addPreviewGenerator(PreviewGenerator generator) {
        synchronized (previewGenerators) {
            previewGenerators.add(generator);
            Collections.sort(previewGenerators, new Comparator<PreviewGenerator>() {
                public int compare(PreviewGenerator a, PreviewGenerator b) {
                    return Integer.valueOf(b.getPriority()).compareTo(a.getPriority());
                }
            });
        }
    }

    /**
     * Removes the preview generator from the list of registered preview
     * generators.
     * 
     * @param generator
     *          the generator
     */
    void removePreviewGenerator(PreviewGenerator generator) {
        synchronized (previewGenerators) {
            previewGenerators.remove(generator);
        }
    }

    /**
     * @see ch.entwine.weblounge.dispatcher.api.request.RequestHandler#getName()
     */
    public String getName() {
        return "preview request handler";
    }

    /**
     * Returns a string representation of this request handler.
     * 
     * @return the handler name
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return getName();
    }

    /**
     * {@inheritDoc}
     * 
     * @see ch.entwine.weblounge.dispatcher.RequestHandler#getPriority()
     */
    public int getPriority() {
        return 0;
    }

}