Package ch.entwine.weblounge.dispatcher.impl.handler

Source Code of ch.entwine.weblounge.dispatcher.impl.handler.PreviewRequestHandlerImpl

/*
*  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;
  }

}
TOP

Related Classes of ch.entwine.weblounge.dispatcher.impl.handler.PreviewRequestHandlerImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.