/*
* 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.bundle;
import ch.entwine.weblounge.common.content.MalformedResourceURIException;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceReader;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.impl.content.ResourceURIImpl;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.language.Language;
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.search.SearchIndex;
import ch.entwine.weblounge.common.site.Site;
import ch.entwine.weblounge.common.url.PathUtils;
import ch.entwine.weblounge.common.url.UrlUtils;
import ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository;
import ch.entwine.weblounge.contentrepository.impl.ResourceSelectorImpl;
import ch.entwine.weblounge.contentrepository.impl.index.ContentRepositoryIndex;
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.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.List;
/**
* Content repository that reads pages and resources from the site's
* <code>OSGi</code> bundle. Since a bundle is a read only archive, it does not
* implement the extended <code>WritableContentRepository</code> interface.
* <p>
* The implementation assumes a prefix to all page and resource uris of
* <code>/repository</code>, which means that pages are expected to live under
* <code>/repository/pages</code> while resources are expected under
* <code>/repository/resources</code>. You may want to change these assumptions
* using {@link #setBundlePathPrefix(String)}, {@link #setPagesURI(String)} or
* {@link #setResourcesURI()}.
*/
public class BundleContentRepository extends AbstractContentRepository implements ManagedService {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(BundleContentRepository.class);
/** The repository type */
public static final String TYPE = "ch.entwine.weblounge.contentrepository.bundle";
/** Prefix for repository configuration keys */
public static final String CONF_PREFIX = "contentrepository.bundle.";
/** Option specifying the root directory */
public static final String OPT_ROOT_DIR = CONF_PREFIX + "root";
/** Option to cleanup temporary bundle index on shutdown */
private static final String OPT_CLEANUP = CONF_PREFIX + "cleanup";
/** The site bundle context */
protected Bundle bundle = null;
/** Prefix into the bundle */
protected String bundlePathPrefix = "/repository";
/** Path to the storage root directory */
protected String rootDirPath = null;
/** Flag to indicate whether temporary indices should be removed on shutdown */
protected boolean cleanupTemporaryIndex = false;
/**
* Creates a new instance of the bundle content repository.
*/
public BundleContentRepository() {
super(TYPE);
rootDirPath = PathUtils.concat(System.getProperty("java.io.tmpdir"), "repository");
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#connect(ch.entwine.weblounge.common.site.Site)
*/
@Override
public void connect(Site site) throws ContentRepositoryException {
// Find the site's bundle
bundle = loadBundle(site);
if (bundle == null)
throw new ContentRepositoryException("Unable to locate bundle for site '" + site + "'");
super.connect(site);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#disconnect()
*/
@Override
public void disconnect() throws ContentRepositoryException {
super.disconnect();
}
/**
* {@inheritDoc}
*
* @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
*/
public void updated(Dictionary properties) throws ConfigurationException {
if (properties == null)
return;
// Path to the root directory
String rootDirPath = (String) properties.get(OPT_ROOT_DIR);
if (StringUtils.isNotBlank(rootDirPath)) {
this.rootDirPath = PathUtils.trim(rootDirPath);
logger.info("Bundle content repository index data will be stored at {}", rootDirPath);
}
// Cleanup after shutdown?
if (StringUtils.isNotBlank((String) properties.get(OPT_CLEANUP))) {
cleanupTemporaryIndex = ConfigurationUtils.isTrue((String) properties.get(OPT_CLEANUP));
logger.info("Bundle content repository indicex will {} removed on shutdown", (cleanupTemporaryIndex ? "be" : "not be"));
}
}
/**
* Returns the bundle that contains the site.
*
* @return the site bundle
*/
protected Bundle getBundle() {
return bundle;
}
/**
* 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
*/
@Override
protected Bundle loadBundle(Site site) {
BundleContext bundleCtx = FrameworkUtil.getBundle(site.getClass()).getBundleContext();
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.ch.entwine.weblounge.common.content.repository.ContentRepository#get(ch.entwine.weblounge.common.content.ResourceURI)
*/
@Override
public ResourceURI[] getVersions(ResourceURI uri)
throws ContentRepositoryException {
if (uri == null)
throw new IllegalArgumentException("Page uri cannot be null");
List<ResourceURI> uris = new ArrayList<ResourceURI>();
long[] versions = index.getRevisions(uri);
for (long version : versions) {
uris.add(new ResourceURIImpl(uri, version));
}
return uris.toArray(new ResourceURI[uris.size()]);
}
/**
* Sets the path that identifies the repository root in the bundle. If nothing
* is specified, the default value of <code>/repository</code> will be used.
*
* @param repositoryURI
* the path to the repository
* @throws IllegalArgumentException
* if the uri is <code>null</code>
* @throws IllegalArgumentException
* if the uri is not absolute
*/
public void setBundlePathPrefix(String repositoryURI) {
if (repositoryURI == null)
throw new IllegalArgumentException("Repository uri cannot be null");
if (repositoryURI.startsWith("/"))
throw new IllegalArgumentException("Repository uri Must be absolute");
this.bundlePathPrefix = repositoryURI;
}
/**
* Returns the path that identifies the repository root in the bundle.
*
* @return the path to the repository
*/
public String getBundlePathPrefix() {
return bundlePathPrefix;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#listResources()
*/
@Override
protected Collection<ResourceURI> listResources() throws IOException {
List<ResourceURI> uris = new ArrayList<ResourceURI>();
// Add all known resource types to the index
for (ResourceSerializer<?, ?> serializer : getSerializers()) {
// Construct this resource type's entry point into the bundle
String resourcePath = "/" + serializer.getType() + "s";
String prefix = UrlUtils.concat(bundlePathPrefix, resourcePath);
// List all relevant site resources in the bundle
Enumeration<URL> entries = bundle.findEntries(prefix, "*.xml", true);
if (entries != null) {
while (entries.hasMoreElements()) {
URL entry = entries.nextElement();
ResourceURI uri = loadResourceURI(getSite(), entry);
if (uri == null)
throw new IllegalStateException("Resource " + entry + " has no uri");
uris.add(uri);
}
}
}
return uris;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#loadIndex()
*/
@Override
protected ContentRepositoryIndex loadIndex() throws IOException,
ContentRepositoryException {
BundleContentRepositoryIndex index = null;
index = new BundleContentRepositoryIndex(site, resourceSerializer);
boolean success = false;
// Make sure the version matches the implementation
if (index.getIndexVersion() != SearchIndex.INDEX_VERSION) {
logger.warn("Index version does not match implementation, triggering reindex");
index = new BundleContentRepositoryIndex(site, resourceSerializer);
}
// Is there an existing index?
else if (index.getResourceCount() > 0) {
long resourceCount = index.getResourceCount();
long revisionCount = index.getRevisionCount();
logger.info("Index contains {} resources and {} revisions", resourceCount, revisionCount - resourceCount);
return index;
}
try {
logger.info("Populating temporary site index '{}'", site);
long time = System.currentTimeMillis();
long resourceCount = 0;
long revisionCount = 0;
ResourceURI previousURI = null;
// Add all known resource types to the index
for (ResourceSerializer<?, ?> serializer : getSerializers()) {
ResourceSelector selector = new ResourceSelectorImpl(site).withTypes(serializer.getType());
for (ResourceURI uri : list(selector)) {
// Load the resource
Resource<?> resource = null;
InputStream is = null;
try {
ResourceReader<?, ?> reader = serializer.getReader();
is = loadResource(uri);
resource = reader.read(is, site);
if (resource == null) {
logger.warn("Unkown error loading resource {}", uri);
continue;
}
} catch (Throwable t) {
logger.error("Error loading resource '{}' from bundle: {}", uri, t.getMessage());
continue;
} finally {
IOUtils.closeQuietly(is);
}
// Add it to the index
index.add(resource);
revisionCount++;
if (previousURI != null && !previousURI.getPath().equals(uri.getPath())) {
logger.info("Adding {}:{} to site index", site, uri.getPath());
resourceCount++;
}
previousURI = uri;
}
}
success = true;
if (resourceCount > 0) {
time = System.currentTimeMillis() - time;
logger.info("Site index populated in {} ms", ConfigurationUtils.toHumanReadableDuration(time));
logger.info("{} resources and {} revisions added to index", resourceCount, revisionCount - resourceCount);
}
} catch (MalformedResourceURIException e) {
throw new ContentRepositoryException("Error while reading resource uri for index", e);
} finally {
if (!success) {
try {
index.clear();
} catch (IOException e) {
logger.error("Error while trying to cleanup after failed indexing operation", e);
}
}
}
return index;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#loadResource(ch.entwine.weblounge.common.content.ResourceURI)
*/
@Override
protected InputStream loadResource(ResourceURI uri)
throws ContentRepositoryException, IOException {
String uriPath = uri.getPath();
// This repository is path based, so let's make sure we have a path
// or get one, if that's not the case.
if (uriPath == null) {
uriPath = index.getPath(uri);
if (uriPath == null)
return null;
}
String typePathPrefix = "/" + uri.getType() + "s";
String entryPath = UrlUtils.concat(bundlePathPrefix, typePathPrefix, uriPath, ResourceUtils.getDocument(uri.getVersion()));
URL url = bundle.getEntry(entryPath);
if (url == null)
return null;
try {
return url.openStream();
} catch (IOException e) {
throw new IOException("I/O error while reading page '" + uri + "'", e);
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.contentrepository.impl.AbstractContentRepository#loadResourceContent(ch.entwine.weblounge.common.content.ResourceURI,
* ch.entwine.weblounge.common.language.Language)
*/
@Override
protected InputStream loadResourceContent(ResourceURI uri, Language language)
throws ContentRepositoryException, IOException {
String uriPath = uri.getPath();
// This repository is path based, so let's make sure we have a path
// or get one, if that's not the case.
if (uriPath == null) {
uriPath = index.getPath(uri);
if (uriPath == null)
return null;
}
String typePathPrefix = "/" + uri.getType() + "s";
String entryPath = UrlUtils.concat(bundlePathPrefix, typePathPrefix, uriPath);
String fileFilter = language.getIdentifier() + ".*";
Enumeration<URL> entries = bundle.findEntries(entryPath, fileFilter, false);
if (entries != null && entries.hasMoreElements()) {
URL url = entries.nextElement();
try {
return url.openStream();
} catch (IOException e) {
throw new IOException("I/O error while reading page '" + uri + "'", e);
} catch (Throwable t) {
throw new IllegalStateException(t);
}
}
return null;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
if (bundle != null)
return bundle.getSymbolicName().hashCode();
else
return super.hashCode();
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof BundleContentRepository) {
BundleContentRepository repo = (BundleContentRepository) obj;
if (bundle != null) {
return bundle.equals(repo.getBundle());
} else {
return super.equals(obj);
}
}
return false;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return bundle.toString();
}
}