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