Java tutorial
/* * 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.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 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 index.find(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 index.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 = index.find(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>/<int>/<int>/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>/<int>/<int>/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.isPreview() == style.isPreview(); 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 { if (s.isPreview()) { 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.isPreview()) { 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.isPreview()) { 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; } /** * 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; } } }