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.ByteArrayInputStream;
020: import java.io.ByteArrayOutputStream;
021: import java.io.IOException;
022: import java.io.InputStream;
023: import java.io.Serializable;
024:
025: import org.apache.avalon.framework.activity.Initializable;
026: import org.apache.avalon.framework.logger.AbstractLogEnabled;
027: import org.apache.avalon.framework.service.ServiceException;
028: import org.apache.avalon.framework.service.ServiceManager;
029: import org.apache.avalon.framework.service.Serviceable;
030: import org.apache.excalibur.source.Source;
031: import org.apache.excalibur.source.SourceException;
032: import org.apache.excalibur.source.SourceNotFoundException;
033: import org.apache.excalibur.source.SourceValidity;
034: import org.apache.excalibur.source.impl.validity.ExpiresValidity;
035: import org.apache.excalibur.source.impl.validity.TimeStampValidity;
036: import org.apache.excalibur.xml.sax.XMLizable;
037: import org.apache.excalibur.xmlizer.XMLizer;
038:
039: import org.apache.cocoon.CascadingIOException;
040: import org.apache.cocoon.ProcessingException;
041: import org.apache.cocoon.caching.Cache;
042: import org.apache.cocoon.caching.EventAware;
043: import org.apache.cocoon.caching.IdentifierCacheKey;
044: import org.apache.cocoon.caching.validity.EventValidity;
045: import org.apache.cocoon.caching.validity.NamedEvent;
046: import org.apache.cocoon.components.sax.XMLByteStreamCompiler;
047: import org.apache.cocoon.components.sax.XMLByteStreamInterpreter;
048: import org.apache.cocoon.xml.ContentHandlerWrapper;
049: import org.apache.cocoon.xml.XMLConsumer;
050:
051: import org.xml.sax.ContentHandler;
052: import org.xml.sax.SAXException;
053:
054: /**
055: * This class implements a proxy like source that uses another source
056: * to get the content. This implementation can cache the content for
057: * a given period of time.
058: *
059: * <h2>Syntax for Protocol</h2>
060: * <pre>
061: * cached:http://www.apache.org/[?cocoon:cache-expires=60&cocoon:cache-name=main]
062: * </pre>
063: *
064: * <p>The above examples show how the real source <code>http://www.apache.org</code>
065: * is wrapped and the cached contents is used for <code>60</code> seconds.
066: * The second querystring parameter instructs that the cache key be extended with the string
067: * <code>main</code>. This allows the use of multiple cache entries for the same source.</p>
068: *
069: * <p>The value of the expires parameter holds some additional semantics.
070: * Specifying <code>-1</code> will yield the cached response to be considered valid
071: * always. Value <code>0</code> can be used to achieve the exact opposite. That is to say,
072: * the cached contents will be thrown out and updated immediately and unconditionally.<p>
073: *
074: * @version $Id: CachingSource.java 485495 2006-12-11 04:44:23Z crossley $
075: */
076: public class CachingSource extends AbstractLogEnabled implements
077: Serviceable, Initializable, XMLizable, Source {
078:
079: // TODO: Decouple from eventcache block.
080:
081: // ---------------------------------------------------- Constants
082:
083: public static final String CACHE_EXPIRES_PARAM = "cache-expires";
084: public static final String CACHE_NAME_PARAM = "cache-name";
085:
086: private static final SourceMeta DUMMY = new SourceMeta();
087:
088: // ---------------------------------------------------- Instance variables
089:
090: /** The used protocol */
091: final protected String protocol;
092:
093: /** The full URI string */
094: final protected String uri;
095:
096: /** The full URI string of the underlying source */
097: final protected String sourceUri;
098:
099: /** The source object for the real content */
100: protected Source source;
101:
102: /** The ServiceManager */
103: protected ServiceManager manager;
104:
105: /** The current cache */
106: protected Cache cache;
107:
108: /** The cached response (if any) */
109: private CachedSourceResponse response;
110:
111: /** Did we just update meta info? */
112: private boolean freshMeta;
113:
114: /** The key used in the store */
115: final protected IdentifierCacheKey cacheKey;
116:
117: /** number of seconds before cached object becomes invalid */
118: final protected int expires;
119:
120: /** cache key extension */
121: final protected String cacheName;
122:
123: /** asynchronic refresh strategy ? */
124: final protected boolean async;
125:
126: final protected boolean eventAware;
127:
128: /**
129: * Construct a new object.
130: */
131: public CachingSource(final String protocol, final String uri,
132: final String sourceUri, final Source source,
133: final int expires, final String cacheName,
134: final boolean async, final boolean eventAware) {
135: this .protocol = protocol;
136: this .uri = uri;
137: this .sourceUri = sourceUri;
138: this .source = source;
139: this .expires = expires;
140: this .cacheName = cacheName;
141: this .async = async;
142: this .eventAware = eventAware;
143:
144: String key = "source:" + getSourceURI();
145: if (cacheName != null) {
146: key += ":" + cacheName;
147: }
148: this .cacheKey = new IdentifierCacheKey(key, false);
149: }
150:
151: // ---------------------------------------------------- Lifecycle
152:
153: /**
154: * Set the ServiceManager.
155: */
156: public void service(final ServiceManager manager)
157: throws ServiceException {
158: this .manager = manager;
159: }
160:
161: /**
162: * Initialize the Source.
163: */
164: public void initialize() throws Exception {
165: boolean checkValidity = true;
166: if (this .async && this .expires > 0 || this .expires == -1) {
167: if (getLogger().isDebugEnabled()) {
168: getLogger()
169: .debug("Using cached response if available.");
170: }
171: checkValidity = false;
172: }
173:
174: this .response = (CachedSourceResponse) this .cache
175: .get(this .cacheKey);
176:
177: if (this .response == null) {
178: if (getLogger().isDebugEnabled()) {
179: getLogger().debug("No cached response found.");
180: }
181: checkValidity = false;
182: } else if (this .expires == 0) {
183: if (getLogger().isDebugEnabled()) {
184: getLogger().debug("Not using cached response.");
185: }
186: this .response = null;
187: checkValidity = false;
188: }
189:
190: if (checkValidity && !checkValidity()) {
191: // remove invalid response
192: clearResponse();
193: }
194: }
195:
196: /**
197: * Cleanup.
198: */
199: public void dispose() {
200: this .response = null;
201: this .source = null;
202: this .manager = null;
203: this .cache = null;
204: }
205:
206: // ---------------------------------------------------- CachedSourceResponse object management
207:
208: private CachedSourceResponse getResponse() {
209: CachedSourceResponse response = this .response;
210: if (response == null) {
211: response = new CachedSourceResponse(getCacheValidities());
212: }
213: return response;
214: }
215:
216: private void setResponse(CachedSourceResponse response)
217: throws IOException {
218: this .response = response;
219: if (this .expires != 0) {
220: try {
221: this .cache.store(this .cacheKey, this .response);
222: } catch (ProcessingException e) {
223: throw new CascadingIOException(
224: "Failure storing response.", e);
225: }
226: }
227: }
228:
229: private void clearResponse() {
230: this .response = null;
231: this .cache.remove(this .cacheKey);
232: }
233:
234: /**
235: * Initialize the cached response with meta info.
236: *
237: * @throws IOException if an the binary response could not be initialized
238: */
239: protected SourceMeta getResponseMeta() throws IOException {
240: CachedSourceResponse response = getResponse();
241:
242: if (response.getExtra() == null) {
243: response.setExtra(readMeta(this .source));
244: this .freshMeta = true;
245: setResponse(response);
246: }
247:
248: return (SourceMeta) response.getExtra();
249: }
250:
251: /**
252: * Initialize the cached response with meta and binary contents.
253: *
254: * @throws IOException if an the binary response could not be initialized
255: */
256: protected byte[] getBinaryResponse() throws IOException {
257: CachedSourceResponse response = getResponse();
258:
259: if (response.getBinaryResponse() == null) {
260: if (!this .freshMeta) {
261: /* always refresh meta in this case */
262: response.setExtra(readMeta(this .source));
263: this .freshMeta = true;
264: }
265: if (((SourceMeta) response.getExtra()).exists()) {
266: response
267: .setBinaryResponse(readBinaryResponse(this .source));
268: }
269: setResponse(response);
270: }
271:
272: return response.getBinaryResponse();
273: }
274:
275: /**
276: * Initialize the cached response with meta, binary, and XML contents.
277: *
278: * @throws SAXException if something happened during xml processing
279: * @throws IOException if an IO level error occured
280: * @throws CascadingIOException wraps all other exception types
281: */
282: protected byte[] getXMLResponse() throws SAXException, IOException,
283: CascadingIOException {
284: CachedSourceResponse response = getResponse();
285:
286: if (response.getXMLResponse() == null) {
287: if (!this .freshMeta) {
288: /* always refresh meta in this case */
289: response.setExtra(readMeta(this .source));
290: this .freshMeta = true;
291: }
292: if (((SourceMeta) response.getExtra()).exists()) {
293: if (response.getBinaryResponse() == null) {
294: response
295: .setBinaryResponse(readBinaryResponse(this .source));
296: }
297: response.setXMLResponse(readXMLResponse(this .source,
298: response.getBinaryResponse(), this .manager));
299: }
300: setResponse(response);
301: }
302:
303: return response.getXMLResponse();
304: }
305:
306: private SourceMeta getMeta() {
307: try {
308: return getResponseMeta();
309: } catch (IOException e) {
310: // Could not initialize meta. Return default meta values.
311: return DUMMY;
312: }
313: }
314:
315: // ---------------------------------------------------- Source implementation
316:
317: /**
318: * Return the protocol identifier.
319: */
320: public String getScheme() {
321: return this .protocol;
322: }
323:
324: /**
325: * Get the content length of the source or -1 if it
326: * is not possible to determine the length.
327: */
328: public long getContentLength() {
329: return getMeta().getContentLength();
330: }
331:
332: /**
333: * Get the last modification date.
334: * @return The last modification in milliseconds since January 1, 1970 GMT
335: * or 0 if it is unknown
336: */
337: public long getLastModified() {
338: return getMeta().getLastModified();
339: }
340:
341: /**
342: * The mime-type of the content described by this object.
343: * If the source is not able to determine the mime-type by itself
344: * this can be null.
345: */
346: public String getMimeType() {
347: return getMeta().getMimeType();
348: }
349:
350: /**
351: * Return an <code>InputStream</code> object to read from the source.
352: */
353: public InputStream getInputStream() throws IOException,
354: SourceException {
355: try {
356: return new ByteArrayInputStream(getBinaryResponse());
357: } catch (IOException e) {
358: throw new SourceException("Failure getting input stream", e);
359: }
360: }
361:
362: /**
363: * Return the unique identifer for this source
364: */
365: public String getURI() {
366: return this .uri;
367: }
368:
369: /**
370: * @see org.apache.excalibur.source.Source#exists()
371: */
372: public boolean exists() {
373: return getMeta().exists();
374: }
375:
376: /**
377: * Get the Validity object. This can either wrap the last modification
378: * date or the expires information or...
379: * If it is currently not possible to calculate such an information
380: * <code>null</code> is returned.
381: */
382: public SourceValidity getValidity() {
383: long lastModified = getLastModified();
384: if (lastModified > 0) {
385: return new TimeStampValidity(lastModified);
386: }
387: return null;
388: }
389:
390: /**
391: * Refresh this object and update the last modified date
392: * and content length.
393: *
394: * This method will try to refresh the cached meta data
395: * and content only if cached content is expired.
396: */
397: public void refresh() {
398: if (response != null && checkValidity()) {
399: return;
400: }
401:
402: this .source.refresh();
403:
404: CachedSourceResponse response = getResponse();
405: try {
406: // always refresh meta data
407: SourceMeta meta = readMeta(source);
408: response.setExtra(meta);
409:
410: if (meta.exists()) {
411: // only create objects that are cached
412: if (response.getBinaryResponse() != null) {
413: response
414: .setBinaryResponse(readBinaryResponse(source));
415: }
416: if (response.getXMLResponse() != null) {
417: response
418: .setXMLResponse(readXMLResponse(source,
419: response.getBinaryResponse(),
420: this .manager));
421: }
422: } else {
423: if (getLogger().isDebugEnabled()) {
424: getLogger().debug(
425: "Source " + this .uri + " does not exist.");
426: }
427: // clear cached data
428: response.setBinaryResponse(null);
429: response.setXMLResponse(null);
430: }
431:
432: // Even if source does not exist, cache that fact.
433: setResponse(response);
434: } catch (Exception e) {
435: getLogger()
436: .warn(
437: "Error refreshing source "
438: + this .uri
439: + ". Cached response (if any) may be stale.",
440: e);
441: }
442: }
443:
444: // ---------------------------------------------------- XMLizable implementation
445:
446: /**
447: * Generates SAX events representing the object's state.
448: */
449: public void toSAX(ContentHandler contentHandler)
450: throws SAXException {
451: try {
452: XMLByteStreamInterpreter deserializer = new XMLByteStreamInterpreter();
453: if (contentHandler instanceof XMLConsumer) {
454: deserializer.setConsumer((XMLConsumer) contentHandler);
455: } else {
456: deserializer.setConsumer(new ContentHandlerWrapper(
457: contentHandler));
458: }
459: deserializer.deserialize(getXMLResponse());
460: } catch (CascadingIOException e) {
461: throw new SAXException(e.getMessage(), (Exception) e
462: .getCause());
463: } catch (IOException e) {
464: throw new SAXException("Failure reading SAX response.", e);
465: }
466: }
467:
468: // ---------------------------------------------------- CachingSource specific accessors
469:
470: /**
471: * Return the uri of the cached source.
472: */
473: protected String getSourceURI() {
474: return this .sourceUri;
475: }
476:
477: /**
478: * Return the used key.
479: */
480: protected String getCacheKey() {
481: return this .cacheKey.getKey();
482: }
483:
484: /**
485: * Expires (in milli-seconds)
486: */
487: protected long getExpiration() {
488: return this .expires * 1000;
489: }
490:
491: /**
492: * Read XML content from source.
493: *
494: * @return content from source
495: * @throws SAXException
496: * @throws IOException
497: * @throws CascadingIOException
498: */
499: protected byte[] readXMLResponse(Source source, byte[] binary,
500: ServiceManager manager) throws SAXException, IOException,
501: CascadingIOException {
502: XMLizer xmlizer = null;
503: try {
504: XMLByteStreamCompiler serializer = new XMLByteStreamCompiler();
505:
506: if (source instanceof XMLizable) {
507: ((XMLizable) source).toSAX(serializer);
508: } else {
509: final String mimeType = source.getMimeType();
510: if (mimeType != null) {
511: xmlizer = (XMLizer) manager.lookup(XMLizer.ROLE);
512: xmlizer.toSAX(new ByteArrayInputStream(binary),
513: mimeType, source.getURI(), serializer);
514: }
515: }
516:
517: return (byte[]) serializer.getSAXFragment();
518: } catch (ServiceException e) {
519: throw new CascadingIOException(
520: "Missing service dependency.", e);
521: } finally {
522: if (xmlizer != null) {
523: manager.release(xmlizer);
524: }
525: }
526: }
527:
528: /**
529: * Read binary content from source.
530: *
531: * @return content from source
532: * @throws IOException
533: * @throws SourceNotFoundException
534: */
535: protected byte[] readBinaryResponse(Source source)
536: throws IOException, SourceNotFoundException {
537: final ByteArrayOutputStream baos = new ByteArrayOutputStream();
538: final byte[] buffer = new byte[2048];
539: final InputStream inputStream = source.getInputStream();
540: int length;
541: while ((length = inputStream.read(buffer)) > -1) {
542: baos.write(buffer, 0, length);
543: }
544: baos.flush();
545: inputStream.close();
546: return baos.toByteArray();
547: }
548:
549: /**
550: * Read meta data from source.
551: */
552: protected SourceMeta readMeta(Source source) throws SourceException {
553: return new SourceMeta(source);
554: }
555:
556: private boolean checkValidity() {
557: if (this .response == null) {
558: return false;
559: }
560:
561: if (eventAware) {
562: if (getLogger().isDebugEnabled()) {
563: getLogger().debug(
564: "Cached response of source does not expire");
565: }
566: return true;
567: }
568:
569: final SourceValidity[] validities = this .response
570: .getValidityObjects();
571: boolean valid = true;
572:
573: final ExpiresValidity expiresValidity = (ExpiresValidity) validities[0];
574: final SourceValidity sourceValidity = validities[1];
575:
576: if (expiresValidity.isValid() != SourceValidity.VALID) {
577: int validity = sourceValidity != null ? sourceValidity
578: .isValid() : SourceValidity.INVALID;
579: if (validity == SourceValidity.INVALID
580: || validity == SourceValidity.UNKNOWN
581: && sourceValidity.isValid(source.getValidity()) != SourceValidity.VALID) {
582: if (getLogger().isDebugEnabled()) {
583: getLogger().debug(
584: "Response expired, invalid for "
585: + getSourceURI());
586: }
587: valid = false;
588: } else {
589: if (getLogger().isDebugEnabled()) {
590: getLogger().debug(
591: "Response expired, still valid for "
592: + getSourceURI());
593: }
594: // set new expiration period
595: validities[0] = new ExpiresValidity(getExpiration());
596: }
597: } else {
598: if (getLogger().isDebugEnabled()) {
599: getLogger().debug(
600: "Response not expired for " + getSourceURI());
601: }
602: }
603:
604: return valid;
605: }
606:
607: protected SourceValidity[] getCacheValidities() {
608: if (this .cache instanceof EventAware) {
609: // use event caching strategy, the associated event is the source uri
610: return new SourceValidity[] { new EventValidity(
611: new NamedEvent(this .source.getURI())) };
612: } else {
613: // we need to store both the cache expiration and the original source validity
614: // the former is to determine whether to recheck the latter (see checkValidity)
615: return new SourceValidity[] {
616: new ExpiresValidity(getExpiration()),
617: source.getValidity() };
618: }
619: }
620:
621: /**
622: * Data holder for caching Source meta info.
623: */
624: protected static class SourceMeta implements Serializable {
625: private boolean exists;
626: private long contentLength;
627: private String mimeType;
628: private long lastModified;
629:
630: public SourceMeta() {
631: }
632:
633: public SourceMeta(Source source) {
634: setExists(source.exists());
635: if (exists()) {
636: setContentLength(source.getContentLength());
637: final long lastModified = source.getLastModified();
638: if (lastModified > 0) {
639: setLastModified(lastModified);
640: } else {
641: setLastModified(System.currentTimeMillis());
642: }
643: setMimeType(source.getMimeType());
644: } else {
645: contentLength = -1;
646: }
647: }
648:
649: protected boolean exists() {
650: return exists;
651: }
652:
653: protected void setExists(boolean exists) {
654: this .exists = exists;
655: }
656:
657: protected long getContentLength() {
658: return contentLength;
659: }
660:
661: protected void setContentLength(long contentLength) {
662: this .contentLength = contentLength;
663: }
664:
665: protected long getLastModified() {
666: return lastModified;
667: }
668:
669: protected void setLastModified(long lastModified) {
670: this .lastModified = lastModified;
671: }
672:
673: protected String getMimeType() {
674: return mimeType;
675: }
676:
677: protected void setMimeType(String mimeType) {
678: this.mimeType = mimeType;
679: }
680: }
681: }
|