Package ch.entwine.weblounge.contentrepository.impl

Source Code of ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository$PreviewOperation

/*
*  Weblounge: Web Content Management System
*  Copyright (c) 2003 - 2011 The Weblounge Team
*  http://entwinemedia.com/weblounge
*
*  This program is free software; you can redistribute it and/or
*  modify it under the terms of the GNU Lesser General Public License
*  as published by the Free Software Foundation; either version 2
*  of the License, or (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public License
*  along with this program; if not, write to the Free Software Foundation
*  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package ch.entwine.weblounge.contentrepository.impl;

import static ch.entwine.weblounge.common.site.Environment.Development;

import ch.entwine.weblounge.common.content.PreviewGenerator;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceReader;
import ch.entwine.weblounge.common.content.ResourceSearchResultItem;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.content.SearchQuery;
import ch.entwine.weblounge.common.content.SearchResult;
import ch.entwine.weblounge.common.content.image.ImagePreviewGenerator;
import ch.entwine.weblounge.common.content.image.ImageStyle;
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.image.ImageStyleImpl;
import ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
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.repository.ResourceSelector;
import ch.entwine.weblounge.common.repository.ResourceSerializer;
import ch.entwine.weblounge.common.repository.ResourceSerializerService;
import ch.entwine.weblounge.common.repository.WritableContentRepository;
import ch.entwine.weblounge.common.search.SearchIndex;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.contentrepository.impl.index.ContentRepositoryIndex;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerFactory;

/**
* Abstract implementation for read-only content repositories.
*/
public abstract class AbstractContentRepository implements ContentRepository {

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

  /** The repository type */
  protected String type = null;

  /** Index into this repository */
  protected ContentRepositoryIndex index = null;

  /** The site */
  protected Site site = null;

  /** Flag indicating the connected state */
  protected boolean connected = false;

  /** Flag indicating the initializing state */
  protected boolean initializing = false;

  /** Flag indicating the write access */
  protected boolean readOnly = false;

  /** Flag indicating the indexing state */
  protected boolean indexing = false;

  /** The document builder factory */
  protected final DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();

  /** The xml transformer factory */
  protected final TransformerFactory transformerFactory = TransformerFactory.newInstance();

  /** Regular expression to match the resource id, path and version */
  protected static final Pattern resourceHeaderRegex = Pattern.compile(".*<\\s*([\\w]*) .*id=\"([a-z0-9-]*)\".*path=\"([^\"]*)\".*version=\"([^\"]*)\".*");

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

  /** The resource serializer service */
  protected ResourceSerializerService resourceSerializer = null;

  /** The search index service */
  protected SearchIndex searchIndex = null;

  /** The image style tracker */
  private ImageStyleTracker imageStyleTracker = null;

  /** The image preview generators */
  protected List<ImagePreviewGenerator> imagePreviewGenerators = new ArrayList<ImagePreviewGenerator>();

  /** The resources for which preview generation is due */
  private final Map<ResourceURI, PreviewOperation> previews = new HashMap<ResourceURI, PreviewOperation>();

  /** Prioritized list of preview rendering operations */
  private final Queue<PreviewOperation> previewOperations = new LinkedBlockingQueue<PreviewOperation>();

  /** The preview operations that are being worked on at the moment */
  private final List<PreviewOperation> currentPreviewOperations = new ArrayList<PreviewOperation>();

  /** The maximum number of concurrent preview operations */
  private final int maxPreviewOperations = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);

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

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getType()
   */
  public String getType() {
    return type;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#isReadOnly()
   */
  public boolean isReadOnly() {
    return readOnly || !(this instanceof WritableContentRepository);
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#connect(ch.entwine.weblounge.common.site.Site)
   */
  public void connect(Site site) throws ContentRepositoryException {
    if (connected)
      throw new IllegalStateException("Content repository has already been started");
    if (site == null)
      throw new ContentRepositoryException("Site must not be null");
    this.site = site;

    try {
      index = loadIndex();
    } catch (IOException e) {
      throw new ContentRepositoryException("Error loading repository index", e);
    }

    Bundle bundle = loadBundle(site);
    if (bundle != null) {
      imageStyleTracker = new ImageStyleTracker(bundle.getBundleContext());
      imageStyleTracker.open();
    }

    connected = true;

    // Make sure previews are available as defined
    updatePreviews();
  }

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

    // Stop ongoing image preview generation
    synchronized (currentPreviewOperations) {
      logger.info("Stopping preview generation");
      previewOperations.clear();
      previews.clear();
    }

    // Close the image style tracker
    if (imageStyleTracker != null) {
      imageStyleTracker.close();
      imageStyleTracker = null;
    }

    // Close the index and mark the content repository as offline
    try {
      connected = false;
      if (index != null)
        index.close();
    } catch (IOException e) {
      throw new ContentRepositoryException("Error closing repository index", e);
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#isIndexing()
   */
  public boolean isIndexing() {
    return indexing;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#exists(ch.entwine.weblounge.common.content.ResourceURI)
   */
  public boolean exists(ResourceURI uri) throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");
    return index.exists(uri);
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#existsInAnyVersion(ch.entwine.weblounge.common.content.ResourceURI)
   */
  public boolean existsInAnyVersion(ResourceURI uri)
      throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");
    return index.existsInAnyVersion(uri);
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getResourceURI(java.lang.String)
   */
  public ResourceURI getResourceURI(String resourceId)
      throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");
    ResourceURI uri = new GeneralResourceURIImpl(getSite(), null, resourceId);
    if (!index.exists(uri))
      return null;
    uri.setType(index.getType(uri));
    uri.setPath(index.getPath(uri));
    return uri;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#find(ch.entwine.weblounge.common.content.SearchQuery)
   */
  public SearchResult find(SearchQuery query) throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");
    return searchIndex.getByQuery(query);
  }

  /**
   * {@inheritDoc}
   *
   * @throws ContentRepositoryException
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#suggest(java.lang.String,
   *      java.lang.String, int)
   */
  public List<String> suggest(String dictionary, String seed, int count)
      throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");
    return searchIndex.suggest(dictionary, seed, false, count, false);
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#get(ch.entwine.weblounge.common.content.ResourceURI)
   */
  @SuppressWarnings("unchecked")
  public <R extends Resource<?>> R get(ResourceURI uri)
      throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");

    // Check if the resource is available
    if (!index.exists(uri))
      return null;

    // Make sure we have the correct resource type
    if (uri.getType() == null) {
      uri.setType(index.getType(uri));
    } else if (!uri.getType().equals(index.getType(uri))) {
      return null;
    }
    if (uri.getIdentifier() == null && StringUtils.isNotBlank(uri.getPath())) {
      uri.setIdentifier(index.getIdentifier(uri));
    }

    // Load the resource
    SearchQuery q = new SearchQueryImpl(site).withVersion(uri.getVersion()).withIdentifier(uri.getIdentifier());
    SearchResult result = searchIndex.getByQuery(q);

    if (result.getDocumentCount() > 0) {
      ResourceSearchResultItem searchResultItem = (ResourceSearchResultItem) result.getItems()[0];
      InputStream is = null;
      try {
        ResourceSerializer<?, ?> serializer = getSerializerByType(uri.getType());
        if (serializer == null) {
          logger.warn("No resource serializer for type '{}' found", uri.getType());
          throw new ContentRepositoryException("No resource serializer for type '" + uri.getType() + "' found");
        }
        ResourceReader<?, ?> reader = serializer.getReader();
        is = IOUtils.toInputStream(searchResultItem.getResourceXml(), "utf-8");
        return (R) reader.read(is, site);
      } catch (Throwable t) {
        logger.error("Error loading {}: {}", uri, t.getMessage());
        throw new ContentRepositoryException(t);
      } finally {
        IOUtils.closeQuietly(is);
      }

    } else {

      try {
        Resource<?> resource = null;
        InputStream is = null;
        try {
          InputStream resourceStream = loadResource(uri);
          if (resourceStream == null) {
            return null;
          }
          is = new BufferedInputStream(resourceStream);
          ResourceSerializer<?, ?> serializer = getSerializerByType(uri.getType());
          ResourceReader<?, ?> reader = serializer.getReader();
          resource = reader.read(is, site);
        } catch (Throwable t) {
          String version = ResourceUtils.getVersionString(uri.getVersion());
          throw new IOException("Error reading " + version + " version of " + uri + " (" + uri.getIdentifier() + ")", t);
        } finally {
          IOUtils.closeQuietly(is);
        }

        if (resource == null) {
          logger.error("Index inconsistency detected: version '{}' of {} does not exist on disk", ResourceUtils.getVersionString(uri.getVersion()), uri);
          return null;
        }

        return (R) resource;
      } catch (IOException e) {
        logger.error("Error loading {}: {}", uri, e.getMessage());
        throw new ContentRepositoryException(e);
      }
    }

  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getContent(ch.entwine.weblounge.common.content.ResourceURI,
   *      ch.entwine.weblounge.common.language.Language)
   */
  public InputStream getContent(ResourceURI uri, Language language)
      throws ContentRepositoryException, IOException {
    return loadResourceContent(uri, language);
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getVersions(ch.entwine.weblounge.common.content.ResourceURI)
   */
  public ResourceURI[] getVersions(ResourceURI uri)
      throws ContentRepositoryException {
    if (!isStarted())
      throw new IllegalStateException("Content repository is not connected");

    long[] revisions = index.getRevisions(uri);
    ResourceURI[] uris = new ResourceURI[revisions.length];
    int i = 0;
    for (long r : revisions) {
      uris[i++] = new ResourceURIImpl(uri, r);
    }
    return uris;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getResourceCount()
   */
  public long getResourceCount() throws ContentRepositoryException {
    return index != null ? index.getResourceCount() : -1;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#getVersionCount()
   */
  public long getVersionCount() throws ContentRepositoryException {
    return index != null ? index.getRevisionCount() : -1;
  }

  /**
   * Appends the identifier of the form <code>x-y-z-u-v</code> to
   * <code>path</code> as in <code>/&lt;int&gt;/&lt;int&gt;/id</code>, with the
   * "/" being the platform's file separator.
   *
   * @param id
   *          the identifier
   * @param path
   *          the root path
   * @return the path
   */
  protected StringBuffer appendIdToPath(String id, StringBuffer path) {
    if (id == null)
      throw new IllegalArgumentException("Identifier must not be null");
    path.append(idToDirectory(id));
    return path;
  }

  /**
   * Returns the identifier of the form <code>x-y-z-u-v</code> as a path as in
   * <code>/&lt;int&gt;/&lt;int&gt;/id</code>, with the "/" being the platform's
   * file separator.
   *
   * @param id
   *          the identifier
   * @return the path
   */
  protected String idToDirectory(String id) {
    if (id == null)
      throw new IllegalArgumentException("Identifier must not be null");
    String[] elements = id.split("-");
    StringBuffer path = new StringBuffer();

    // convert first part of uuid to long and apply modulo 100
    path.append(File.separatorChar);
    path.append(String.valueOf(Long.parseLong(elements[0], 16) % 100));

    // convert second part of uuid to long and apply modulo 10
    path.append(File.separatorChar);
    path.append(String.valueOf(Long.parseLong(elements[1], 16) % 10));

    // append the full uuid as the actual directory
    path.append(File.separatorChar);
    path.append(id);

    return path.toString();
  }

  /**
   * Returns the site that is associated with this repository.
   *
   * @return the site
   */
  protected Site getSite() {
    return site;
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.ContentRepository#list(ch.entwine.weblounge.common.repository.ResourceSelector)
   */
  public Collection<ResourceURI> list(ResourceSelector selector)
      throws ContentRepositoryException {

    int index = -1;
    int selected = 0;

    Collection<ResourceURI> uris = null;
    Collection<ResourceURI> result = new ArrayList<ResourceURI>();

    List<?> selectedTypes = Arrays.asList(selector.getTypes());
    List<?> forbiddenTypes = Arrays.asList(selector.getWithoutTypes());
    List<?> selectedIds = Arrays.asList(selector.getIdentifiers());
    List<?> selectedVersions = Arrays.asList(selector.getVersions());

    try {
      uris = listResources();
    } catch (IOException e) {
      logger.error("Error reading available uris: {}", e.getMessage());
      throw new ContentRepositoryException(e);
    }

    for (ResourceURI uri : uris) {

      // Rule out types that we don't need
      if (!selectedTypes.isEmpty() && !selectedTypes.contains(uri.getType()))
        continue;
      if (!forbiddenTypes.isEmpty() && forbiddenTypes.contains(uri.getType()))
        continue;

      // Rule out resources we are not interested in
      if (!selectedIds.isEmpty() && !selectedIds.contains(uri.getIdentifier()))
        continue;
      if (!selectedVersions.isEmpty() && !selectedVersions.contains(uri.getVersion()))
        continue;

      index++;

      // Skip everything below the offset
      if (index < selector.getOffset())
        continue;

      result.add(uri);
      selected++;

      // Only collect as many items as we need
      if (selector.getLimit() > 0 && selected == selector.getLimit())
        break;
    }

    return result;
  }

  /**
   * Returns <code>true</code> if the repository is connected and started.
   *
   * @return <code>true</code> if the repository is started
   */
  protected boolean isStarted() {
    return connected;
  }

  /**
   * Lists the resources in the content repository.
   *
   * @return the list of resources
   * @throws ContentRepositoryException
   *           if loading metadata from the repository fails
   * @throws IOException
   *           if listing the resources fails
   */
  protected abstract Collection<ResourceURI> listResources()
      throws ContentRepositoryException, IOException;

  /**
   * Loads and returns the resource from the repository.
   *
   * @param uri
   *          the resource uri
   * @return the resource
   * @throws ContentRepositoryException
   *           if loading metadata from the repository fails
   * @throws IOException
   *           if the resource could not be loaded
   */
  protected abstract InputStream loadResource(ResourceURI uri)
      throws ContentRepositoryException, IOException;

  /**
   * Returns the input stream to the resource content identified by
   * <code>uri</code> and <code>language</code> or <code>null</code> if no such
   * resource exists.
   *
   * @param uri
   *          the resource uri
   * @param language
   *          the content language
   * @return the resource contents
   * @throws ContentRepositoryException
   *           if loading metadata from the repository fails
   * @throws IOException
   *           if opening the stream to the resource failed
   */
  protected abstract InputStream loadResourceContent(ResourceURI uri,
      Language language) throws ContentRepositoryException, IOException;

  /**
   * Loads the repository index. Depending on the concrete implementation, the
   * index might be located in the repository itself or at any other storage
   * location. It might even be an in-memory index, in which case the repository
   * implementation is in charge of populating the index.
   *
   * @return the index
   * @throws IOException
   *           if reading or creating the index fails
   * @throws ContentRepositoryException
   *           if populating the index fails
   */
  protected abstract ContentRepositoryIndex loadIndex() throws IOException,
      ContentRepositoryException;

  /**
   * {@inheritDoc}
   *
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode() {
    if (site != null)
      return site.hashCode();
    else
      return super.hashCode();
  }

  /**
   * {@inheritDoc}
   *
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof AbstractContentRepository) {
      AbstractContentRepository repo = (AbstractContentRepository) obj;
      if (site != null) {
        return site.equals(repo.getSite());
      } else {
        return super.equals(obj);
      }
    }
    return false;
  }

  /**
   * Returns the resource that is located at the indicated url.
   *
   * @param uri
   *          the resource uri
   * @param contentUrl
   *          location of the resource file
   * @return the resource
   */
  protected Resource<?> loadResource(ResourceURI uri, URL contentUrl)
      throws IOException {
    InputStream is = null;
    try {
      is = new BufferedInputStream(contentUrl.openStream());
      ResourceSerializer<?, ?> serializer = getSerializerByType(uri.getType());
      ResourceReader<?, ?> reader = serializer.getReader();
      return reader.read(is, site);
    } catch (Throwable t) {
      throw new IOException("Error reading resource from " + contentUrl);
    } finally {
      IOUtils.closeQuietly(is);
    }
  }

  /**
   * Returns the resource uri or <code>null</code> if no resource id and/or path
   * could be found on the specified document. This method is intended to serve
   * as a utility method when importing resources.
   *
   * @param site
   *          the resource uri
   * @param contentUrl
   *          location of the resource file
   * @return the resource uri
   */
  protected ResourceURI loadResourceURI(Site site, URL contentUrl)
      throws IOException {

    InputStream is = null;
    InputStreamReader reader = null;
    try {
      is = new BufferedInputStream(contentUrl.openStream());
      reader = new InputStreamReader(is);
      CharBuffer buf = CharBuffer.allocate(1024);
      reader.read(buf);
      String s = new String(buf.array());
      s = s.replace('\n', ' ');
      Matcher m = resourceHeaderRegex.matcher(s);
      if (m.matches()) {
        long version = ResourceUtils.getVersion(m.group(4));
        return new ResourceURIImpl(m.group(1), site, m.group(3), m.group(2), version);
      }
      return null;
    } finally {
      if (reader != null)
        reader.close();
      IOUtils.closeQuietly(is);
    }
  }

  /**
   * Replaces templates inside the property value with their corresponding value
   * from the system properties and environment.
   *
   * @param v
   *          the original property value
   * @return the processed value
   */
  protected Object processPropertyTemplates(Object v) {
    if (v == null || !(v instanceof String))
      return v;

    String value = (String) v;

    // Do variable replacement using the system properties
    for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
      StringBuffer envKey = new StringBuffer("\\$\\{").append(entry.getKey()).append("\\}");
      value = value.replaceAll(envKey.toString(), entry.getValue().toString());
    }

    // Do variable replacement using the system environment
    for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
      StringBuffer envKey = new StringBuffer("\\$\\{").append(entry.getKey()).append("\\}");
      value = value.replaceAll(envKey.toString(), entry.getValue());
    }

    return value;
  }

  /**
   * Tries to find the site's bundle in the OSGi service registry and returns
   * it, <code>null</code> otherwise.
   *
   * @param site
   *          the site
   * @return the bundle
   */
  protected Bundle loadBundle(Site site) {
    Bundle bundle = FrameworkUtil.getBundle(this.getClass());
    if (bundle == null)
      return null;
    BundleContext bundleCtx = bundle.getBundleContext();
    if (bundleCtx == null) {
      logger.debug("Bundle {} does not have a bundle context associated", bundle);
      return null;
    }
    String siteClass = Site.class.getName();
    try {
      ServiceReference[] refs = bundleCtx.getServiceReferences(siteClass, null);
      if (refs == null || refs.length == 0)
        return null;
      for (ServiceReference ref : refs) {
        Site s = (Site) bundleCtx.getService(ref);
        if (s == site)
          return ref.getBundle();
      }
      return null;
    } catch (InvalidSyntaxException e) {
      // Can't happen
      logger.error("Error trying to locate the site's bundle", e);
      return null;
    }
  }

  /**
   * {@inheritDoc}
   *
   * @see ch.entwine.weblounge.common.repository.WritableContentRepository#createPreviews()
   */
  @Override
  public void createPreviews() throws ContentRepositoryException {
    Collection<ResourceURI> uris = null;
    logger.debug("Starting preview generation");

    // Load the uris
    try {
      uris = listResources();
    } catch (IOException e) {
      logger.warn("Error retrieving list of resources: {}", e.getMessage());
      return;
    }

    // Initiate preview generation
    for (ResourceURI uri : uris) {
      Resource<?> resource = get(uri);
      if (resource == null) {
        logger.warn("Skipping missing {} for preview generation", uri);
        continue;
      }
      createPreviews(resource, site.getLanguages());
    }

  }

  /**
   * Iterates over the existing image styles and determines whether at least one
   * style has changed or is missing the previews.
   *
   * @throws ContentRepositoryException
   *           if preview generation fails
   */
  protected void updatePreviews() throws ContentRepositoryException {

    // Compile the full list of image styles
    if (imageStyleTracker == null) {
      logger.info("Skipping preview generation: image styles are unavailable");
      return;
    }

    final List<ImageStyle> allStyles = new ArrayList<ImageStyle>();

    // Add the global image styles that have the preview flag turned on
    for (ImageStyle s : imageStyleTracker.getImageStyles()) {
      allStyles.add(s);
    }

    // Add the site's preview image styles as well as
    for (Module m : getSite().getModules()) {
      for (ImageStyle s : m.getImageStyles()) {
        allStyles.add(s);
      }
    }

    // Check whether the image styles still match the current definition. If
    // not, remove the produced previews and recreate them.
    boolean styleHasChanged = false;
    boolean styleIsMissing = false;

    for (ImageStyle s : allStyles) {
      File baseDir = ImageStyleUtils.getDirectory(site, s);
      File definitionFile = new File(baseDir, "style.xml");

      // Try and read the file on disk
      if (definitionFile.isFile()) {
        DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder;
        Document doc;
        ImageStyle style;
        try {
          docBuilder = docBuilderFactory.newDocumentBuilder();
          doc = docBuilder.parse(definitionFile);
          style = ImageStyleImpl.fromXml(doc.getFirstChild());

          // Is the style still the same?
          boolean stylesMatch = s.getWidth() == style.getWidth();
          stylesMatch = stylesMatch && s.getHeight() == style.getHeight();
          stylesMatch = stylesMatch && s.getScalingMode().equals(style.getScalingMode());
          stylesMatch = stylesMatch && s.getContexts().size() == style.getContexts().size();
          for (String ctx : s.getContexts()) {
            stylesMatch = stylesMatch && s.createPreview(ctx) == style.createPreview(ctx);
          }
          styleHasChanged = styleHasChanged || !stylesMatch;
        } catch (ParserConfigurationException e) {
          logger.error("Error setting up image style parser: {}", e.getMessage());
        } catch (SAXException e) {
          logger.error("Error parsing image style {}: {}", definitionFile, e.getMessage());
        } catch (IOException e) {
          logger.error("Error reading image style {}: {}", definitionFile, e.getMessage());
        }
      } else {
        boolean previewsForAnyContext = false;
        for (String ctx : s.getContexts()) {
          previewsForAnyContext |= s.createPreview(ctx);
        }
        if (previewsForAnyContext) {
          logger.debug("No previews found for image style '{}'", s.getIdentifier());
          styleIsMissing = true;
        }
      }

      // The current definition is no longer valid
      if (styleHasChanged) {
        logger.info("Image style '{}' has changed, removing existing previews from {}", s.getIdentifier(), baseDir);
        FileUtils.deleteQuietly(baseDir);
        if (!baseDir.mkdirs()) {
          logger.error("Error creating image style directory {}", baseDir);
          continue;
        }
      }

      // Store the new definition
      if (!definitionFile.isFile() || styleHasChanged) {
        try {
          definitionFile.getParentFile().mkdirs();
          definitionFile.createNewFile();
          FileUtils.copyInputStreamToFile(IOUtils.toInputStream(s.toXml(), "UTF-8"), definitionFile);
        } catch (IOException e) {
          logger.error("Error creating image style defintion file at {}", definitionFile, e.getMessage());
          continue;
        }
      } else {
        logger.debug("Image style {} still matching the current definition", s.getIdentifier());
      }
    }

    if (styleHasChanged || styleIsMissing) {
      if (environment.equals(Development)) {
        logger.info("Missing or outdated previews found. Skipping preview generation for current environment 'development'");
        return;
      }
      logger.info("Triggering creation of missing and outdated previews");
      createPreviews();
    } else {
      logger.debug("Preview images for {} are still up to date", site.getIdentifier());
    }
  }

  /**
   * Creates the previews for this resource in all languages and for all known
   * image styles. The implementation ensures that there is only one preview
   * renderer running per resource.
   *
   * @param resource
   *          the resource
   * @param languages
   *          the languages to build the previews for
   */
  protected void createPreviews(final Resource<?> resource,
      Language... languages) {

    ResourceURI uri = resource.getURI();

    // Compile the full list of image styles
    if (imageStyleTracker == null) {
      logger.info("Skipping preview generation for {}: image styles are unavailable", uri);
      return;
    }

    final List<ImageStyle> previewStyles = new ArrayList<ImageStyle>();

    // Add the global image styles that have the preview flag turned on
    for (ImageStyle s : imageStyleTracker.getImageStyles()) {
      if (s.createPreview(resource.getURI().getType())) {
        previewStyles.add(s);
        logger.debug("Preview images will be generated for {}", s);
      } else {
        logger.debug("Preview image generation will be skipped for {}", s);
      }
    }

    // Add the site's preview image styles as well as
    for (Module m : getSite().getModules()) {
      for (ImageStyle s : m.getImageStyles()) {
        if (s.createPreview(resource.getURI().getType())) {
          previewStyles.add(s);
          logger.debug("Preview images will be generated for {}", s);
        } else {
          logger.debug("Preview image generation will be skipped for {}", s);
        }
      }
    }

    // If no language has been specified, we create the preview for all
    // languages
    if (languages == null || languages.length == 0) {
      languages = uri.getSite().getLanguages();
    }

    // Create the previews
    PreviewOperation previewOp = null;
    synchronized (currentPreviewOperations) {

      // is there an existing operation for this resource? If so, simply update
      // it and be done.
      previewOp = previews.get(uri);
      if (previewOp != null) {
        PreviewGeneratorWorker worker = previewOp.getWorker();
        if (worker != null) {
          logger.info("Canceling current preview generation for {} in favor of more recent data", uri);
          worker.cancel();
        }
      }

      // Otherwise, a new preview generator needs to be started.
      previewOp = new PreviewOperation(resource, Arrays.asList(languages), previewStyles, ImageStyleUtils.DEFAULT_PREVIEW_FORMAT);

      // Make sure nobody is working on the same resource at the moment
      if (currentPreviewOperations.contains(previewOp)) {
        logger.debug("Queing concurring creation of preview for {}", uri);
        previews.put(uri, previewOp);
        previewOperations.add(previewOp);
        return;
      }

      // If there is enough being worked on already, there is nothing we can do
      // right now, the work will be picked up later on
      if (currentPreviewOperations.size() >= maxPreviewOperations) {
        logger.debug("Queing creation of preview for {}", uri);
        previews.put(uri, previewOp);
        previewOperations.add(previewOp);
        logger.debug("Preview generation queue now contains {} resources", previews.size());
        return;
      }

      // It seems like it is safe to start the preview generation
      currentPreviewOperations.add(previewOp);
      PreviewGeneratorWorker previewWorker = new PreviewGeneratorWorker(this, previewOp.getResource(), environment, previewOp.getLanguages(), previewOp.getStyles(), previewOp.getFormat());
      previewOp.setWorker(previewWorker);
      Thread t = new Thread(previewWorker);
      t.setPriority(Thread.MIN_PRIORITY);
      t.setDaemon(true);

      logger.debug("Creating preview of {}", uri);
      t.start();
    }
  }

  /**
   * Callback for the preview renderer to indicate a finished rendering
   * operation.
   *
   * @param resource
   *          the resource
   */
  void previewCreated(Resource<?> resource) {
    synchronized (currentPreviewOperations) {

      // Do the cleanup
      for (Iterator<PreviewOperation> i = currentPreviewOperations.iterator(); i.hasNext();) {
        PreviewOperation op = i.next();
        Resource<?> r = op.getResource();
        if (r.equals(resource)) {
          logger.debug("Preview creation of {} finished", r.getURI());
          i.remove();
          PreviewOperation o = previews.get(r.getURI());
          // In the meantime, someone may have canceled this operation and
          // created a new one
          if (op == o)
            previews.remove(r.getURI());
          break;
        }
      }

      // Is there more work to do?
      if (!previewOperations.isEmpty() && currentPreviewOperations.size() < maxPreviewOperations) {

        // Get the next operation and do the bookkeeping
        PreviewOperation op = previewOperations.remove();
        Resource<?> r = op.getResource();
        currentPreviewOperations.add(op);

        // Finally start the generation
        PreviewGeneratorWorker previewWorker = new PreviewGeneratorWorker(this, r, environment, op.getLanguages(), op.getStyles(), op.getFormat());
        op.setWorker(previewWorker);
        Thread t = new Thread(previewWorker);
        t.setPriority(Thread.MIN_PRIORITY);
        t.setDaemon(true);

        logger.debug("Starting creation of preview of {}", r.getURI());
        logger.trace("There are {} more preview operations waiting", previewOperations.size());
        logger.trace("Currently using {} out of {} preview creation slots", currentPreviewOperations.size(), maxPreviewOperations);
        t.start();
      } else {
        logger.debug("No more resources queued for preview creation");
      }
    }
  }

  /**
   * Deletes the previews for this resource in all languages and for all known
   * image styles.
   *
   * @param resource
   *          the resource
   */
  protected void deletePreviews(Resource<?> resource) {
    deletePreviews(resource, null);
  }

  /**
   * Deletes the previews for this resource in the given languages and for all
   * known image styles.
   *
   * @param resource
   *          the resource
   * @param language
   *          the language
   */
  protected void deletePreviews(Resource<?> resource, Language language) {
    // Compile the full list of image styles
    List<ImageStyle> styles = new ArrayList<ImageStyle>();
    if (imageStyleTracker != null)
      styles.addAll(imageStyleTracker.getImageStyles());
    for (Module m : getSite().getModules()) {
      styles.addAll(Arrays.asList(m.getImageStyles()));
    }

    for (ImageStyle style : styles) {
      File styledImage = null;

      // Create the path to a sample image
      if (language != null) {
        styledImage = ImageStyleUtils.getScaledFile(resource, language, style);
      } else {
        styledImage = ImageStyleUtils.getScaledFile(resource, LanguageUtils.getLanguage("en"), style);
        styledImage = styledImage.getParentFile();
      }

      // Remove the parent's directory, which will include the specified
      // previews
      File dir = styledImage.getParentFile();
      logger.debug("Deleting previews in {}", dir.getAbsolutePath());
      FileUtils.deleteQuietly(dir);
    }
  }

  /**
   * Returns the current environment.
   *
   * @return the environment
   */
  protected Environment getEnvironment() {
    return environment;
  }

  /**
   * This method is called right after initialization of the content repository
   * and sets the environment.
   *
   * @param environment
   *          the environment
   */
  public void setEnvironment(Environment environment) {
    if (environment == null)
      throw new IllegalStateException("Environment has not been set");
    this.environment = environment;
  }

  /**
   * Returns the resource serializer for the given type or <code>null</code> if
   * no such serializer is registered.
   *
   * @param type
   *          the resource type
   * @return the serializer
   */
  protected ResourceSerializer<?, ?> getSerializerByType(String type) {
    if (resourceSerializer == null)
      throw new IllegalStateException("Serializer service has not been set");
    return resourceSerializer.getSerializerByType(type);
  }

  /**
   * Returns the resource serializer for the given mime type or
   * <code>null</code> if no such serializer is registered.
   *
   * @param mimeType
   *          the mime type
   * @return the serializer
   */
  protected ResourceSerializer<?, ?> getSerializerByMimeType(String mimeType) {
    if (resourceSerializer == null)
      throw new IllegalStateException("Serializer service has not been set");
    return resourceSerializer.getSerializerByMimeType(mimeType);
  }

  /**
   * Returns the set of available resource serializers.
   *
   * @return the set serializer
   */
  protected Set<ResourceSerializer<?, ?>> getSerializers() {
    return resourceSerializer.getSerializers();
  }

  /**
   * This method is called right after initialization of the content repository
   * and is used to register the factory with a backing service implementation.
   *
   * @param service
   *          the resource serializer service
   */
  public void setSerializer(ResourceSerializerService service) {
    resourceSerializer = service;
  }

  public void setSearchIndex(SearchIndex index) {
    searchIndex = index;
  }

  /**
   * Adds the preview generator to the list of registered preview generators.
   *
   * @param generator
   *          the generator
   */
  void addPreviewGenerator(ImagePreviewGenerator generator) {
    synchronized (imagePreviewGenerators) {
      imagePreviewGenerators.add(generator);
      Collections.sort(imagePreviewGenerators, 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(ImagePreviewGenerator generator) {
    synchronized (imagePreviewGenerators) {
      imagePreviewGenerators.remove(generator);
    }
  }

  /**
   * Data structure that is used to hold all relevant information for preview
   * generation of a given resource.
   */
  private static final class PreviewOperation {

    /** The resource to be rendered */
    private Resource<?> resource = null;

    /** List of languages that need to be rendered */
    private final List<Language> languages = new ArrayList<Language>();

    /** List of image styles that need to be rendered */
    private final List<ImageStyle> styles = new ArrayList<ImageStyle>();

    /** Name of the preview image format */
    private String format = null;

    /** Worker that is in charge of conducting this operation */
    private PreviewGeneratorWorker worker = null;

    /**
     * Creates a new representation of a preview generation.
     */
    public PreviewOperation(Resource<?> resource, List<Language> languages,
        List<ImageStyle> styles, String format) {
      this.resource = resource;
      this.languages.addAll(languages);
      this.styles.addAll(styles);
      this.format = format;
    }

    /**
     * Sets the worker that is in charge of conducting this operation.
     *
     * @param worker
     *          the worker
     */
    void setWorker(PreviewGeneratorWorker worker) {
      this.worker = worker;
    }

    /**
     * Returns the worker that is in charge of this operation.
     *
     * @return the worker
     */
    PreviewGeneratorWorker getWorker() {
      return this.worker;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      return resource.hashCode();
    }

    /**
     * {@inheritDoc}
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object op) {
      return resource.equals(((PreviewOperation) op).getResource());
    }

    /**
     * Returns the resource that is to be rendered.
     *
     * @return the resource
     */
    public Resource<?> getResource() {
      return resource;
    }

    /**
     * Returns the languages that need preview generation.
     *
     * @return the language
     */
    public List<Language> getLanguages() {
      return languages;
    }

    /**
     * Returns the image styles.
     *
     * @return the styles
     */
    public List<ImageStyle> getStyles() {
      return styles;
    }

    /**
     * @return the format
     */
    public String getFormat() {
      return format;
    }

  }

}
TOP

Related Classes of ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository$PreviewOperation

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.