| /* |
| * 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.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Serializable; |
| |
| import org.apache.avalon.framework.activity.Initializable; |
| import org.apache.avalon.framework.logger.AbstractLogEnabled; |
| import org.apache.avalon.framework.service.ServiceException; |
| import org.apache.avalon.framework.service.ServiceManager; |
| import org.apache.avalon.framework.service.Serviceable; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceException; |
| import org.apache.excalibur.source.SourceNotFoundException; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.apache.excalibur.source.impl.validity.ExpiresValidity; |
| import org.apache.excalibur.source.impl.validity.TimeStampValidity; |
| import org.apache.excalibur.xml.sax.XMLizable; |
| import org.apache.excalibur.xmlizer.XMLizer; |
| |
| import org.apache.cocoon.CascadingIOException; |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.caching.Cache; |
| import org.apache.cocoon.caching.EventAware; |
| import org.apache.cocoon.caching.IdentifierCacheKey; |
| import org.apache.cocoon.caching.validity.EventValidity; |
| import org.apache.cocoon.caching.validity.NamedEvent; |
| import org.apache.cocoon.components.sax.XMLByteStreamCompiler; |
| import org.apache.cocoon.components.sax.XMLByteStreamInterpreter; |
| import org.apache.cocoon.xml.ContentHandlerWrapper; |
| import org.apache.cocoon.xml.XMLConsumer; |
| |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * This class implements a proxy like source that uses another source |
| * to get the content. This implementation can cache the content for |
| * a given period of time. |
| * |
| * <h2>Syntax for Protocol</h2> |
| * <pre> |
| * cached:http://www.apache.org/[?cocoon:cache-expires=60&cocoon:cache-name=main] |
| * </pre> |
| * |
| * <p>The above examples show 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>The value of the expires parameter holds some additional semantics. |
| * Specifying <code>-1</code> will yield the cached response to be considered valid |
| * always. Value <code>0</code> can be used to achieve the exact opposite. That is to say, |
| * the cached contents will be thrown out and updated immediately and unconditionally.<p> |
| * |
| * @version $Id$ |
| */ |
| public class CachingSource extends AbstractLogEnabled |
| implements Serviceable, Initializable, XMLizable, |
| Source { |
| |
| // TODO: Decouple from eventcache block. |
| |
| // ---------------------------------------------------- Constants |
| |
| public static final String CACHE_EXPIRES_PARAM = "cache-expires"; |
| public static final String CACHE_NAME_PARAM = "cache-name"; |
| |
| private static final SourceMeta DUMMY = new SourceMeta(); |
| |
| // ---------------------------------------------------- Instance variables |
| |
| /** The used protocol */ |
| final protected String protocol; |
| |
| /** The full URI string */ |
| final protected String uri; |
| |
| /** The full URI string of the underlying source */ |
| final protected String sourceUri; |
| |
| /** The source object for the real content */ |
| protected Source source; |
| |
| |
| /** The ServiceManager */ |
| protected ServiceManager manager; |
| |
| /** The current cache */ |
| protected Cache cache; |
| |
| |
| /** The cached response (if any) */ |
| private CachedSourceResponse response; |
| |
| /** Did we just update meta info? */ |
| private boolean freshMeta; |
| |
| /** The key used in the store */ |
| final protected IdentifierCacheKey cacheKey; |
| |
| /** number of seconds before cached object becomes invalid */ |
| final protected int expires; |
| |
| /** cache key extension */ |
| final protected String cacheName; |
| |
| /** asynchronic refresh strategy ? */ |
| final protected boolean async; |
| |
| final protected boolean eventAware; |
| |
| /** |
| * Construct a new object. |
| */ |
| public CachingSource(final String protocol, |
| final String uri, |
| final String sourceUri, |
| final Source source, |
| final int expires, |
| final String cacheName, |
| final boolean async, |
| final boolean eventAware) { |
| this.protocol = protocol; |
| this.uri = uri; |
| this.sourceUri = sourceUri; |
| this.source = source; |
| this.expires = expires; |
| this.cacheName = cacheName; |
| this.async = async; |
| this.eventAware = eventAware; |
| |
| String key = "source:" + getSourceURI(); |
| if (cacheName != null) { |
| key += ":" + cacheName; |
| } |
| this.cacheKey = new IdentifierCacheKey(key, false); |
| } |
| |
| // ---------------------------------------------------- Lifecycle |
| |
| /** |
| * Set the ServiceManager. |
| */ |
| public void service(final ServiceManager manager) throws ServiceException { |
| this.manager = manager; |
| } |
| |
| /** |
| * Initialize the Source. |
| */ |
| public void initialize() throws Exception { |
| boolean checkValidity = true; |
| if (this.async && this.expires > 0 || this.expires == -1) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Using cached response if available."); |
| } |
| checkValidity = false; |
| } |
| |
| this.response = (CachedSourceResponse) this.cache.get(this.cacheKey); |
| |
| if (this.response == null) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("No cached response found."); |
| } |
| checkValidity = false; |
| } else if (this.expires == 0) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Not using cached response."); |
| } |
| this.response = null; |
| checkValidity = false; |
| } |
| |
| if (checkValidity && !checkValidity()) { |
| // remove invalid response |
| clearResponse(); |
| } |
| } |
| |
| /** |
| * Cleanup. |
| */ |
| public void dispose() { |
| this.response = null; |
| this.source = null; |
| this.manager = null; |
| this.cache = null; |
| } |
| |
| // ---------------------------------------------------- CachedSourceResponse object management |
| |
| private CachedSourceResponse getResponse() { |
| CachedSourceResponse response = this.response; |
| if (response == null) { |
| response = new CachedSourceResponse(getCacheValidities()); |
| } |
| return response; |
| } |
| |
| private void setResponse(CachedSourceResponse response) throws IOException { |
| this.response = response; |
| if (this.expires != 0) { |
| try { |
| this.cache.store(this.cacheKey, this.response); |
| } catch (ProcessingException e) { |
| throw new CascadingIOException("Failure storing response.", e); |
| } |
| } |
| } |
| |
| private void clearResponse() { |
| this.response = null; |
| this.cache.remove(this.cacheKey); |
| } |
| |
| /** |
| * Initialize the cached response with meta info. |
| * |
| * @throws IOException if an the binary response could not be initialized |
| */ |
| protected SourceMeta getResponseMeta() throws IOException { |
| CachedSourceResponse response = getResponse(); |
| |
| if (response.getExtra() == null) { |
| response.setExtra(readMeta(this.source)); |
| this.freshMeta = true; |
| setResponse(response); |
| } |
| |
| return (SourceMeta) response.getExtra(); |
| } |
| |
| /** |
| * Initialize the cached response with meta and binary contents. |
| * |
| * @throws IOException if an the binary response could not be initialized |
| */ |
| protected byte[] getBinaryResponse() throws IOException { |
| CachedSourceResponse response = getResponse(); |
| |
| if (response.getBinaryResponse() == null) { |
| if (!this.freshMeta) { |
| /* always refresh meta in this case */ |
| response.setExtra(readMeta(this.source)); |
| this.freshMeta = true; |
| } |
| if (((SourceMeta) response.getExtra()).exists()) { |
| response.setBinaryResponse(readBinaryResponse(this.source)); |
| } |
| setResponse(response); |
| } |
| |
| return response.getBinaryResponse(); |
| } |
| |
| /** |
| * Initialize the cached response with meta, binary, and XML contents. |
| * |
| * @throws SAXException if something happened during xml processing |
| * @throws IOException if an IO level error occured |
| * @throws CascadingIOException wraps all other exception types |
| */ |
| protected byte[] getXMLResponse() throws SAXException, IOException, CascadingIOException { |
| CachedSourceResponse response = getResponse(); |
| |
| if (response.getXMLResponse() == null) { |
| if (!this.freshMeta) { |
| /* always refresh meta in this case */ |
| response.setExtra(readMeta(this.source)); |
| this.freshMeta = true; |
| } |
| if (((SourceMeta) response.getExtra()).exists()) { |
| if (response.getBinaryResponse() == null) { |
| response.setBinaryResponse(readBinaryResponse(this.source)); |
| } |
| response.setXMLResponse(readXMLResponse(this.source, response.getBinaryResponse(), this.manager)); |
| } |
| setResponse(response); |
| } |
| |
| return response.getXMLResponse(); |
| } |
| |
| private SourceMeta getMeta() { |
| try { |
| return getResponseMeta(); |
| } catch (IOException e) { |
| // Could not initialize meta. Return default meta values. |
| return DUMMY; |
| } |
| } |
| |
| // ---------------------------------------------------- Source implementation |
| |
| /** |
| * Return the protocol identifier. |
| */ |
| public String getScheme() { |
| return this.protocol; |
| } |
| |
| /** |
| * Get the content length of the source or -1 if it |
| * is not possible to determine the length. |
| */ |
| public long getContentLength() { |
| return getMeta().getContentLength(); |
| } |
| |
| /** |
| * Get the last modification date. |
| * @return The last modification in milliseconds since January 1, 1970 GMT |
| * or 0 if it is unknown |
| */ |
| public long getLastModified() { |
| return getMeta().getLastModified(); |
| } |
| |
| /** |
| * The mime-type of the content described by this object. |
| * If the source is not able to determine the mime-type by itself |
| * this can be null. |
| */ |
| public String getMimeType() { |
| return getMeta().getMimeType(); |
| } |
| |
| /** |
| * Return an <code>InputStream</code> object to read from the source. |
| */ |
| public InputStream getInputStream() throws IOException, SourceException { |
| try { |
| return new ByteArrayInputStream(getBinaryResponse()); |
| } catch (IOException e) { |
| throw new SourceException("Failure getting input stream", e); |
| } |
| } |
| |
| /** |
| * Return the unique identifer for this source |
| */ |
| public String getURI() { |
| return this.uri; |
| } |
| |
| /** |
| * @see org.apache.excalibur.source.Source#exists() |
| */ |
| public boolean exists() { |
| return getMeta().exists(); |
| } |
| |
| /** |
| * Get the Validity object. This can either wrap the last modification |
| * date or the expires information or... |
| * If it is currently not possible to calculate such an information |
| * <code>null</code> is returned. |
| */ |
| public SourceValidity getValidity() { |
| long lastModified = getLastModified(); |
| if (lastModified > 0) { |
| return new TimeStampValidity(lastModified); |
| } |
| return null; |
| } |
| |
| /** |
| * Refresh this object and update the last modified date |
| * and content length. |
| * |
| * This method will try to refresh the cached meta data |
| * and content only if cached content is expired. |
| */ |
| public void refresh() { |
| if (response != null && checkValidity()) { |
| return; |
| } |
| |
| this.source.refresh(); |
| |
| CachedSourceResponse response = getResponse(); |
| try { |
| // always refresh meta data |
| SourceMeta meta = readMeta(source); |
| response.setExtra(meta); |
| |
| if (meta.exists()) { |
| // only create objects that are cached |
| if (response.getBinaryResponse() != null) { |
| response.setBinaryResponse(readBinaryResponse(source)); |
| } |
| if (response.getXMLResponse() != null) { |
| response.setXMLResponse(readXMLResponse(source, response.getBinaryResponse(), this.manager)); |
| } |
| } else { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Source " + this.uri + " does not exist."); |
| } |
| // clear cached data |
| response.setBinaryResponse(null); |
| response.setXMLResponse(null); |
| } |
| |
| // Even if source does not exist, cache that fact. |
| setResponse(response); |
| } catch (Exception e) { |
| getLogger().warn("Error refreshing source " + this.uri + |
| ". Cached response (if any) may be stale.", e); |
| } |
| } |
| |
| // ---------------------------------------------------- XMLizable implementation |
| |
| /** |
| * Generates SAX events representing the object's state. |
| */ |
| public void toSAX(ContentHandler contentHandler) throws SAXException { |
| try { |
| XMLByteStreamInterpreter deserializer = new XMLByteStreamInterpreter(); |
| if (contentHandler instanceof XMLConsumer) { |
| deserializer.setConsumer((XMLConsumer) contentHandler); |
| } else { |
| deserializer.setConsumer(new ContentHandlerWrapper(contentHandler)); |
| } |
| deserializer.deserialize(getXMLResponse()); |
| } catch (CascadingIOException e) { |
| throw new SAXException(e.getMessage(), (Exception) e.getCause()); |
| } catch (IOException e) { |
| throw new SAXException("Failure reading SAX response.", e); |
| } |
| } |
| |
| // ---------------------------------------------------- CachingSource specific accessors |
| |
| /** |
| * Return the uri of the cached source. |
| */ |
| protected String getSourceURI() { |
| return this.sourceUri; |
| } |
| |
| /** |
| * Return the used key. |
| */ |
| protected String getCacheKey() { |
| return this.cacheKey.getKey(); |
| } |
| |
| /** |
| * Expires (in milli-seconds) |
| */ |
| protected long getExpiration() { |
| return this.expires * 1000; |
| } |
| |
| /** |
| * Read XML content from source. |
| * |
| * @return content from source |
| * @throws SAXException |
| * @throws IOException |
| * @throws CascadingIOException |
| */ |
| protected byte[] readXMLResponse(Source source, byte[] binary, ServiceManager manager) |
| throws SAXException, IOException, CascadingIOException { |
| XMLizer xmlizer = null; |
| try { |
| XMLByteStreamCompiler serializer = new XMLByteStreamCompiler(); |
| |
| if (source instanceof XMLizable) { |
| ((XMLizable) source).toSAX(serializer); |
| } else { |
| final String mimeType = source.getMimeType(); |
| if (mimeType != null) { |
| xmlizer = (XMLizer) manager.lookup(XMLizer.ROLE); |
| xmlizer.toSAX(new ByteArrayInputStream(binary), |
| mimeType, |
| source.getURI(), |
| serializer); |
| } |
| } |
| |
| return (byte[]) serializer.getSAXFragment(); |
| } catch (ServiceException e) { |
| throw new CascadingIOException("Missing service dependency.", e); |
| } finally { |
| if (xmlizer != null) { |
| manager.release(xmlizer); |
| } |
| } |
| } |
| |
| /** |
| * Read binary content from source. |
| * |
| * @return content from source |
| * @throws IOException |
| * @throws SourceNotFoundException |
| */ |
| protected byte[] readBinaryResponse(Source source) |
| throws IOException, SourceNotFoundException { |
| final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| final byte[] buffer = new byte[2048]; |
| final InputStream inputStream = source.getInputStream(); |
| int length; |
| while ((length = inputStream.read(buffer)) > -1) { |
| baos.write(buffer, 0, length); |
| } |
| baos.flush(); |
| inputStream.close(); |
| return baos.toByteArray(); |
| } |
| |
| /** |
| * Read meta data from source. |
| */ |
| protected SourceMeta readMeta(Source source) throws SourceException { |
| return new SourceMeta(source); |
| } |
| |
| private boolean checkValidity() { |
| if (this.response == null) { |
| return false; |
| } |
| |
| if (eventAware) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Cached response of source does not expire"); |
| } |
| return true; |
| } |
| |
| final SourceValidity[] validities = this.response.getValidityObjects(); |
| boolean valid = true; |
| |
| final ExpiresValidity expiresValidity = (ExpiresValidity) validities[0]; |
| final SourceValidity sourceValidity = validities[1]; |
| |
| if (expiresValidity.isValid() != SourceValidity.VALID) { |
| int validity = sourceValidity != null? sourceValidity.isValid() : SourceValidity.INVALID; |
| if (validity == SourceValidity.INVALID || |
| validity == SourceValidity.UNKNOWN && |
| sourceValidity.isValid(source.getValidity()) != SourceValidity.VALID) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Response expired, invalid for " + getSourceURI()); |
| } |
| valid = false; |
| } else { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Response expired, still valid for " + getSourceURI()); |
| } |
| // set new expiration period |
| validities[0] = new ExpiresValidity(getExpiration()); |
| } |
| } else { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Response not expired for " + getSourceURI()); |
| } |
| } |
| |
| return valid; |
| } |
| |
| protected SourceValidity[] getCacheValidities() { |
| if (this.cache instanceof EventAware) { |
| // use event caching strategy, the associated event is the source uri |
| return new SourceValidity[] { new EventValidity(new NamedEvent(this.source.getURI())) }; |
| } else { |
| // we need to store both the cache expiration and the original source validity |
| // the former is to determine whether to recheck the latter (see checkValidity) |
| return new SourceValidity[] { new ExpiresValidity(getExpiration()), source.getValidity() }; |
| } |
| } |
| |
| /** |
| * Data holder for caching Source meta info. |
| */ |
| protected static class SourceMeta implements Serializable { |
| private boolean exists; |
| private long contentLength; |
| private String mimeType; |
| private long lastModified; |
| |
| public SourceMeta() { |
| } |
| |
| public SourceMeta(Source source) { |
| setExists(source.exists()); |
| if (exists()) { |
| setContentLength(source.getContentLength()); |
| final long lastModified = source.getLastModified(); |
| if (lastModified > 0) { |
| setLastModified(lastModified); |
| } else { |
| setLastModified(System.currentTimeMillis()); |
| } |
| setMimeType(source.getMimeType()); |
| } else { |
| contentLength = -1; |
| } |
| } |
| |
| protected boolean exists() { |
| return exists; |
| } |
| |
| protected void setExists(boolean exists) { |
| this.exists = exists; |
| } |
| |
| protected long getContentLength() { |
| return contentLength; |
| } |
| |
| protected void setContentLength(long contentLength) { |
| this.contentLength = contentLength; |
| } |
| |
| protected long getLastModified() { |
| return lastModified; |
| } |
| |
| protected void setLastModified(long lastModified) { |
| this.lastModified = lastModified; |
| } |
| |
| protected String getMimeType() { |
| return mimeType; |
| } |
| |
| protected void setMimeType(String mimeType) { |
| this.mimeType = mimeType; |
| } |
| } |
| } |