001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.source.impl;
018:
019: import java.io.IOException;
020: import java.net.MalformedURLException;
021: import java.util.Iterator;
022: import java.util.Map;
023:
024: import org.apache.avalon.framework.activity.Disposable;
025: import org.apache.avalon.framework.configuration.Configurable;
026: import org.apache.avalon.framework.configuration.Configuration;
027: import org.apache.avalon.framework.configuration.ConfigurationException;
028: import org.apache.avalon.framework.container.ContainerUtil;
029: import org.apache.avalon.framework.logger.AbstractLogEnabled;
030: import org.apache.avalon.framework.parameters.Parameters;
031: import org.apache.avalon.framework.service.ServiceException;
032: import org.apache.avalon.framework.service.ServiceManager;
033: import org.apache.avalon.framework.service.Serviceable;
034: import org.apache.avalon.framework.thread.ThreadSafe;
035: import org.apache.excalibur.source.Source;
036: import org.apache.excalibur.source.SourceException;
037: import org.apache.excalibur.source.SourceFactory;
038: import org.apache.excalibur.source.SourceParameters;
039: import org.apache.excalibur.source.SourceResolver;
040: import org.apache.excalibur.source.SourceUtil;
041: import org.apache.excalibur.source.TraversableSource;
042: import org.apache.excalibur.source.URIAbsolutizer;
043:
044: import org.apache.cocoon.caching.Cache;
045: import org.apache.cocoon.components.source.InspectableSource;
046: import org.apache.cocoon.components.source.helpers.SourceRefresher;
047:
048: /**
049: * This class implements a proxy like source caches the contents of the source
050: * it wraps. This implementation can cache the content either for a given period
051: * of time or until an external event invalidates the cached response.
052: *
053: * <p>When using the timeout approach you have a choice between two separate
054: * revalidation strategies:</p>
055: *
056: * <ul>
057: * <li>Synchronously. This means that the cached contents are checked for validity
058: * and thrown out on the current thread.
059: * <li>Asynchronously. A runnable task is created to invalidate and update the
060: * cached response in the backgound.
061: * </ul>
062: *
063: * <h2>Protocol syntax</h2>
064: * <p>
065: * The URL needs to contain the URL of the cached source, an expiration
066: * period in seconds, and optionally a cache key:
067: * <code>cached:http://www.apache.org/[?cocoon:cache-expires=60][&cocoon:cache-name=main]</code>.
068: * </p>
069: * <p>
070: * The above examples shows how the real source <code>http://www.apache.org/</code>
071: * is wrapped and the cached contents is used for <code>60</code> seconds.
072: * The second querystring parameter instructs that the cache key be extended with the string
073: * <code>main</code>. This allows the use of multiple cache entries for the same source.
074: * </p>
075: * <p>
076: * This factory creates either instances of {@link org.apache.cocoon.components.source.impl.CachingSource}
077: * or {@link org.apache.cocoon.components.source.impl.TraversableCachingSource}
078: * depending on the whether the wrapped Source is an instance of TraversableSource.
079: * </p>
080: *
081: * <h2>Parameters</h2>
082: * <table><tbody>
083: * <tr>
084: * <th>cache-role (String)</th>
085: * <td>Role of component used as cache.</td>
086: * <td>opt</td>
087: * <td>String</td>
088: * <td><code>{@link Cache#ROLE}</code></td>
089: * </tr>
090: * <tr>
091: * <th>refresher-role (String)</th>
092: * <td>Role of component used for refreshing sources.</td>
093: * <td>opt</td>
094: * <td>String</td>
095: * <td><code>{@link org.apache.cocoon.components.source.helpers.SourceRefresher#ROLE}</code></td>
096: * </tr>
097: * <tr>
098: * <th>async (boolean)</th>
099: * <td>Indicated if the cached source should be refreshed asynchronously.</td>
100: * <td>opt</td>
101: * <td>String</td>
102: * <td><code>false</code></td>
103: * </tr>
104: * <tr>
105: * <th>event-aware (boolean)</th>
106: * <td>Whether to use event-based cache invalidation.</td>
107: * <td>opt</td>
108: * <td>String</td>
109: * <td><code>false</code></td>
110: * </tr>
111: * <tr>
112: * <th>default-expires (int)</th>
113: * <td>Default expiration value for if it is not specified on the Source itself.</td>
114: * <td>opt</td>
115: * <td>String</td>
116: * <td><code>-1</code></td>
117: * </tr>
118: * </tbody></table>
119: *
120: * @version $Id: CachingSourceFactory.java 485495 2006-12-11 04:44:23Z crossley $
121: * @since 2.1.1
122: */
123: public class CachingSourceFactory extends AbstractLogEnabled implements
124: Serviceable, Configurable, Disposable, ThreadSafe,
125: URIAbsolutizer, SourceFactory {
126:
127: // ---------------------------------------------------- Constants
128:
129: private static final String ASYNC_PARAM = "async";
130: private static final String EVENT_AWARE_PARAM = "event-aware";
131: private static final String CACHE_ROLE_PARAM = "cache-role";
132: private static final String REFRESHER_ROLE_PARAM = "refresher-role";
133: private static final String DEFAULT_EXPIRES_PARAM = "default-expires";
134:
135: // ---------------------------------------------------- Instance variables
136:
137: /** Protocol prefix / factory name */
138: private String scheme;
139:
140: /** Asynchronous ? */
141: private boolean async;
142:
143: /** Event aware ? */
144: private boolean eventAware;
145:
146: /** The role of the cache */
147: private String cacheRole;
148:
149: /** The role of the refresher */
150: private String refresherRole;
151:
152: /** Default expires value */
153: private int defaultExpires;
154:
155: /** Has the lazy initialization been done? */
156: private volatile boolean isInitialized;
157:
158: /** The <code>ServiceManager</code> */
159: protected ServiceManager manager;
160:
161: /** The {@link SourceResolver} */
162: protected SourceResolver resolver;
163:
164: /** The refresher */
165: protected SourceRefresher refresher;
166:
167: /** The cache */
168: protected Cache cache;
169:
170: // ---------------------------------------------------- Lifecycle
171:
172: public CachingSourceFactory() {
173: }
174:
175: public void service(ServiceManager manager) {
176: this .manager = manager;
177: // Due to cyclic dependencies we can't lookup the resolver,
178: // the refresher or the cache until after the factory is
179: // initialized.
180: }
181:
182: public void configure(Configuration configuration)
183: throws ConfigurationException {
184: this .scheme = configuration.getAttribute("name");
185: Parameters parameters = Parameters
186: .fromConfiguration(configuration);
187:
188: // 'async' parameter
189: this .async = parameters.getParameterAsBoolean(ASYNC_PARAM,
190: false);
191:
192: // 'event-aware' parameter
193: this .eventAware = parameters.getParameterAsBoolean(
194: EVENT_AWARE_PARAM, false);
195:
196: // 'cache-role' parameter
197: this .cacheRole = parameters.getParameter(CACHE_ROLE_PARAM,
198: Cache.ROLE);
199:
200: // 'refresher-role' parameter
201: if (this .async) {
202: this .refresherRole = parameters.getParameter(
203: REFRESHER_ROLE_PARAM, SourceRefresher.ROLE);
204: }
205:
206: this .defaultExpires = parameters.getParameterAsInteger(
207: DEFAULT_EXPIRES_PARAM, -1);
208:
209: if (getLogger().isDebugEnabled()) {
210: getLogger().debug("Using cache " + this .cacheRole);
211: if (this .async) {
212: getLogger().debug(
213: "Using refresher " + this .refresherRole);
214: }
215: }
216: }
217:
218: /**
219: * Lazy initialization of resolver and refresher because of
220: * cyclic dependencies.
221: *
222: * @throws SourceException
223: */
224: private synchronized void lazyInitialize() throws SourceException {
225: if (this .isInitialized) {
226: // another thread finished initialization for us while
227: // we were waiting
228: return;
229: }
230:
231: try {
232: this .resolver = (SourceResolver) this .manager
233: .lookup(SourceResolver.ROLE);
234: } catch (ServiceException se) {
235: throw new SourceException("Missing service dependency: "
236: + SourceResolver.ROLE, se);
237: }
238:
239: try {
240: this .cache = (Cache) this .manager.lookup(this .cacheRole);
241: } catch (ServiceException se) {
242: throw new SourceException("Missing service dependency: "
243: + this .cacheRole, se);
244: }
245:
246: if (this .async) {
247: try {
248: this .refresher = (SourceRefresher) this .manager
249: .lookup(this .refresherRole);
250: } catch (ServiceException se) {
251: throw new SourceException(
252: "Missing service dependency: "
253: + this .refresherRole, se);
254: }
255: }
256:
257: this .isInitialized = true;
258: }
259:
260: /* (non-Javadoc)
261: * @see Disposable#dispose()
262: */
263: public void dispose() {
264: if (this .refresher != null) {
265: this .manager.release(this .refresher);
266: this .refresher = null;
267: }
268: if (this .cache != null) {
269: this .manager.release(this .cache);
270: this .cache = null;
271: }
272: if (this .resolver != null) {
273: this .manager.release(this .resolver);
274: this .resolver = null;
275: }
276: this .manager = null;
277: }
278:
279: // ---------------------------------------------------- SourceFactory implementation
280:
281: protected String getScheme() {
282: return this .scheme;
283: }
284:
285: protected boolean isAsync() {
286: return this .async;
287: }
288:
289: /**
290: * Get a <code>Source</code> object.
291: * @param parameters This is optional.
292: */
293: public Source getSource(final String location, final Map parameters)
294: throws MalformedURLException, IOException {
295:
296: if (getLogger().isDebugEnabled()) {
297: getLogger().debug("Creating source " + location);
298: }
299:
300: // we must do lazy initialization because of cyclic dependencies
301: if (!this .isInitialized) {
302: lazyInitialize();
303: }
304:
305: // snip the cache protocol
306: int index = location.indexOf(':');
307: if (index == -1) {
308: throw new MalformedURLException(
309: "This Source requires a subprotocol to be specified.");
310: }
311:
312: String uri = location.substring(index + 1);
313:
314: // parse the query string
315: SourceParameters sp = null;
316: index = uri.indexOf('?');
317: if (index != -1) {
318: sp = new SourceParameters(uri.substring(index + 1));
319: uri = uri.substring(0, index);
320: }
321:
322: // put caching source specific query string parameters
323: // into a Parameters object
324: final Parameters params = new Parameters();
325: if (sp != null) {
326: SourceParameters remainingParameters = (SourceParameters) sp
327: .clone();
328: final Iterator names = sp.getParameterNames();
329: while (names.hasNext()) {
330: String name = (String) names.next();
331: if (name.startsWith("cocoon:cache")) {
332: params.setParameter(name.substring("cocoon:"
333: .length()), sp.getParameter(name));
334: remainingParameters.removeParameter(name);
335: }
336: }
337: String queryString = remainingParameters
338: .getEncodedQueryString();
339: if (queryString != null) {
340: uri += "?" + queryString;
341: }
342: }
343:
344: int expires = params.getParameterAsInteger(
345: CachingSource.CACHE_EXPIRES_PARAM, defaultExpires);
346: String cacheName = params.getParameter(
347: CachingSource.CACHE_NAME_PARAM, null);
348:
349: Source source = this .resolver.resolveURI(uri);
350: return createCachingSource(location, uri, source, expires,
351: cacheName);
352: }
353:
354: /**
355: * Actually creates a new CachingSource. Can be overriden in subclasses
356: */
357: protected CachingSource createCachingSource(String uri,
358: String wrappedUri, Source wrappedSource, int expires,
359: String cacheName) throws SourceException {
360:
361: CachingSource source;
362:
363: if (wrappedSource instanceof TraversableSource) {
364: if (wrappedSource instanceof InspectableSource) {
365: source = new InspectableTraversableCachingSource(this ,
366: getScheme(), uri, wrappedUri,
367: (InspectableSource) wrappedSource, expires,
368: cacheName, isAsync(), eventAware);
369: } else {
370: source = new TraversableCachingSource(this ,
371: getScheme(), uri, wrappedUri,
372: (TraversableSource) wrappedSource, expires,
373: cacheName, isAsync(), eventAware);
374: }
375: } else {
376: source = new CachingSource(getScheme(), uri, wrappedUri,
377: wrappedSource, expires, cacheName, isAsync(),
378: eventAware);
379: }
380:
381: // set the required components directly for speed
382: source.cache = this .cache;
383:
384: ContainerUtil.enableLogging(source, getLogger());
385: try {
386: // call selected avalon lifecycle interfaces. Mmmh.
387: ContainerUtil.service(source, this .manager);
388: ContainerUtil.initialize(source);
389: } catch (ServiceException e) {
390: throw new SourceException("Unable to initialize source.", e);
391: } catch (Exception e) {
392: throw new SourceException("Unable to initialize source.", e);
393: }
394:
395: if (this .async && expires > 0) {
396: // schedule it with the refresher
397: final Parameters params = new Parameters();
398: params.setParameter(SourceRefresher.PARAM_CACHE_INTERVAL,
399: String.valueOf(source.getExpiration()));
400: this .refresher.refresh(source.getCacheKey(), source
401: .getURI(), params);
402: }
403:
404: return source;
405: }
406:
407: /**
408: * Release a {@link Source} object.
409: */
410: public void release(Source source) {
411: if (source instanceof CachingSource) {
412: if (getLogger().isDebugEnabled()) {
413: getLogger()
414: .debug("Releasing source " + source.getURI());
415: }
416: CachingSource caching = (CachingSource) source;
417: resolver.release(caching.source);
418: caching.dispose();
419: }
420: }
421:
422: // ---------------------------------------------------- URIAbsolutizer implementation
423:
424: /*
425: * (non-Javadoc)
426: * @see org.apache.excalibur.source.URIAbsolutizer#absolutize(java.lang.String, java.lang.String)
427: */
428: public String absolutize(String baseURI, String location) {
429: return SourceUtil.absolutize(baseURI, location, true);
430: }
431:
432: }
|