blob: 8bb613249de7edcbe50152ed9d509ef38feb39c3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.components.source.impl;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.Map;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.container.ContainerUtil;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.avalon.framework.thread.ThreadSafe;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.SourceFactory;
import org.apache.excalibur.source.SourceParameters;
import org.apache.excalibur.source.SourceResolver;
import org.apache.excalibur.source.SourceUtil;
import org.apache.excalibur.source.TraversableSource;
import org.apache.excalibur.source.URIAbsolutizer;
import org.apache.cocoon.caching.Cache;
import org.apache.cocoon.components.source.InspectableSource;
import org.apache.cocoon.components.source.helpers.SourceRefresher;
/**
* This class implements a proxy like source caches the contents of the source
* it wraps. This implementation can cache the content either for a given period
* of time or until an external event invalidates the cached response.
*
* <p>When using the timeout approach you have a choice between two separate
* revalidation strategies:</p>
*
* <ul>
* <li>Synchronously. This means that the cached contents are checked for validity
* and thrown out on the current thread.
* <li>Asynchronously. A runnable task is created to invalidate and update the
* cached response in the backgound.
* </ul>
*
* <h2>Protocol syntax</h2>
* <p>
* The URL needs to contain the URL of the cached source, an expiration
* period in seconds, and optionally a cache key:
* <code>cached:http://www.apache.org/[?cocoon:cache-expires=60][&cocoon:cache-name=main]</code>.
* </p>
* <p>
* The above examples shows how the real source <code>http://www.apache.org/</code>
* is wrapped and the cached contents is used for <code>60</code> seconds.
* The second querystring parameter instructs that the cache key be extended with the string
* <code>main</code>. This allows the use of multiple cache entries for the same source.
* </p>
* <p>
* This factory creates either instances of {@link org.apache.cocoon.components.source.impl.CachingSource}
* or {@link org.apache.cocoon.components.source.impl.TraversableCachingSource}
* depending on the whether the wrapped Source is an instance of TraversableSource.
* </p>
*
* <h2>Parameters</h2>
* <table><tbody>
* <tr>
* <th>cache-role (String)</th>
* <td>Role of component used as cache.</td>
* <td>opt</td>
* <td>String</td>
* <td><code>{@link Cache#ROLE}</code></td>
* </tr>
* <tr>
* <th>refresher-role (String)</th>
* <td>Role of component used for refreshing sources.</td>
* <td>opt</td>
* <td>String</td>
* <td><code>{@link org.apache.cocoon.components.source.helpers.SourceRefresher#ROLE}</code></td>
* </tr>
* <tr>
* <th>async (boolean)</th>
* <td>Indicated if the cached source should be refreshed asynchronously.</td>
* <td>opt</td>
* <td>String</td>
* <td><code>false</code></td>
* </tr>
* <tr>
* <th>event-aware (boolean)</th>
* <td>Whether to use event-based cache invalidation.</td>
* <td>opt</td>
* <td>String</td>
* <td><code>false</code></td>
* </tr>
* <tr>
* <th>default-expires (int)</th>
* <td>Default expiration value for if it is not specified on the Source itself.</td>
* <td>opt</td>
* <td>String</td>
* <td><code>-1</code></td>
* </tr>
* </tbody></table>
*
* @version $Id$
* @since 2.1.1
*/
public class CachingSourceFactory extends AbstractLogEnabled
implements Serviceable, Configurable, Disposable,
ThreadSafe, URIAbsolutizer, SourceFactory {
// ---------------------------------------------------- Constants
private static final String ASYNC_PARAM = "async";
private static final String EVENT_AWARE_PARAM = "event-aware";
private static final String CACHE_ROLE_PARAM = "cache-role";
private static final String REFRESHER_ROLE_PARAM = "refresher-role";
private static final String DEFAULT_EXPIRES_PARAM = "default-expires";
// ---------------------------------------------------- Instance variables
/** Protocol prefix / factory name */
private String scheme;
/** Asynchronous ? */
private boolean async;
/** Event aware ? */
private boolean eventAware;
/** The role of the cache */
private String cacheRole;
/** The role of the refresher */
private String refresherRole;
/** Default expires value */
private int defaultExpires;
/** Has the lazy initialization been done? */
private volatile boolean isInitialized;
/** The <code>ServiceManager</code> */
protected ServiceManager manager;
/** The {@link SourceResolver} */
protected SourceResolver resolver;
/** The refresher */
protected SourceRefresher refresher;
/** The cache */
protected Cache cache;
// ---------------------------------------------------- Lifecycle
public CachingSourceFactory() {
}
public void service(ServiceManager manager) {
this.manager = manager;
// Due to cyclic dependencies we can't lookup the resolver,
// the refresher or the cache until after the factory is
// initialized.
}
public void configure(Configuration configuration) throws ConfigurationException {
this.scheme = configuration.getAttribute("name");
Parameters parameters = Parameters.fromConfiguration(configuration);
// 'async' parameter
this.async = parameters.getParameterAsBoolean(ASYNC_PARAM, false);
// 'event-aware' parameter
this.eventAware = parameters.getParameterAsBoolean(EVENT_AWARE_PARAM, false);
// 'cache-role' parameter
this.cacheRole = parameters.getParameter(CACHE_ROLE_PARAM, Cache.ROLE);
// 'refresher-role' parameter
if (this.async) {
this.refresherRole = parameters.getParameter(REFRESHER_ROLE_PARAM, SourceRefresher.ROLE);
}
this.defaultExpires = parameters.getParameterAsInteger(DEFAULT_EXPIRES_PARAM, -1);
if (getLogger().isDebugEnabled()) {
getLogger().debug("Using cache " + this.cacheRole);
if (this.async) {
getLogger().debug("Using refresher " + this.refresherRole);
}
}
}
/**
* Lazy initialization of resolver and refresher because of
* cyclic dependencies.
*
* @throws SourceException
*/
private synchronized void lazyInitialize() throws SourceException {
if (this.isInitialized) {
// another thread finished initialization for us while
// we were waiting
return;
}
try {
this.resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE);
} catch (ServiceException se) {
throw new SourceException("Missing service dependency: " + SourceResolver.ROLE, se);
}
try {
this.cache = (Cache) this.manager.lookup(this.cacheRole);
} catch (ServiceException se) {
throw new SourceException("Missing service dependency: " + this.cacheRole, se);
}
if (this.async) {
try {
this.refresher = (SourceRefresher) this.manager.lookup(this.refresherRole);
} catch (ServiceException se) {
throw new SourceException("Missing service dependency: " + this.refresherRole, se);
}
}
this.isInitialized = true;
}
/* (non-Javadoc)
* @see Disposable#dispose()
*/
public void dispose() {
if (this.refresher != null) {
this.manager.release(this.refresher);
this.refresher = null;
}
if (this.cache != null) {
this.manager.release(this.cache);
this.cache = null;
}
if (this.resolver != null) {
this.manager.release(this.resolver);
this.resolver = null;
}
this.manager = null;
}
// ---------------------------------------------------- SourceFactory implementation
protected String getScheme() {
return this.scheme;
}
protected boolean isAsync() {
return this.async;
}
/**
* Get a <code>Source</code> object.
* @param parameters This is optional.
*/
public Source getSource(final String location, final Map parameters)
throws MalformedURLException, IOException {
if (getLogger().isDebugEnabled() ) {
getLogger().debug("Creating source " + location);
}
// we must do lazy initialization because of cyclic dependencies
if (!this.isInitialized) {
lazyInitialize();
}
// snip the cache protocol
int index = location.indexOf(':');
if (index == -1) {
throw new MalformedURLException("This Source requires a subprotocol to be specified.");
}
String uri = location.substring(index + 1);
// parse the query string
SourceParameters sp = null;
index = uri.indexOf('?');
if (index != -1) {
sp = new SourceParameters(uri.substring(index + 1));
uri = uri.substring(0, index);
}
// put caching source specific query string parameters
// into a Parameters object
final Parameters params = new Parameters();
if (sp != null) {
SourceParameters remainingParameters = (SourceParameters) sp.clone();
final Iterator names = sp.getParameterNames();
while (names.hasNext()) {
String name = (String) names.next();
if (name.startsWith("cocoon:cache")) {
params.setParameter(name.substring("cocoon:".length()), sp.getParameter(name));
remainingParameters.removeParameter(name);
}
}
String queryString = remainingParameters.getEncodedQueryString();
if (queryString != null) {
uri += "?" + queryString;
}
}
int expires = params.getParameterAsInteger(CachingSource.CACHE_EXPIRES_PARAM, defaultExpires);
String cacheName = params.getParameter(CachingSource.CACHE_NAME_PARAM, null);
Source source = this.resolver.resolveURI(uri);
return createCachingSource(location, uri, source, expires, cacheName);
}
/**
* Actually creates a new CachingSource. Can be overriden in subclasses
*/
protected CachingSource createCachingSource(String uri,
String wrappedUri,
Source wrappedSource,
int expires,
String cacheName)
throws SourceException {
CachingSource source;
if (wrappedSource instanceof TraversableSource) {
if (wrappedSource instanceof InspectableSource) {
source = new InspectableTraversableCachingSource(this,
getScheme(),
uri,
wrappedUri,
(InspectableSource) wrappedSource,
expires,
cacheName,
isAsync(),
eventAware);
} else {
source = new TraversableCachingSource(this,
getScheme(),
uri,
wrappedUri,
(TraversableSource) wrappedSource,
expires,
cacheName,
isAsync(),
eventAware);
}
} else {
source = new CachingSource(getScheme(),
uri,
wrappedUri,
wrappedSource,
expires,
cacheName,
isAsync(),
eventAware);
}
// set the required components directly for speed
source.cache = this.cache;
ContainerUtil.enableLogging(source, getLogger());
try {
// call selected avalon lifecycle interfaces. Mmmh.
ContainerUtil.service(source, this.manager);
ContainerUtil.initialize(source);
} catch (ServiceException e) {
throw new SourceException("Unable to initialize source.", e);
} catch (Exception e) {
throw new SourceException("Unable to initialize source.", e);
}
if (this.async && expires > 0) {
// schedule it with the refresher
final Parameters params = new Parameters();
params.setParameter(SourceRefresher.PARAM_CACHE_INTERVAL,
String.valueOf(source.getExpiration()));
this.refresher.refresh(source.getCacheKey(), source.getURI(), params);
}
return source;
}
/**
* Release a {@link Source} object.
*/
public void release(Source source) {
if (source instanceof CachingSource) {
if (getLogger().isDebugEnabled() ) {
getLogger().debug("Releasing source " + source.getURI());
}
CachingSource caching = (CachingSource) source;
resolver.release(caching.source);
caching.dispose();
}
}
// ---------------------------------------------------- URIAbsolutizer implementation
/*
* (non-Javadoc)
* @see org.apache.excalibur.source.URIAbsolutizer#absolutize(java.lang.String, java.lang.String)
*/
public String absolutize(String baseURI, String location) {
return SourceUtil.absolutize(baseURI, location, true);
}
}