blob: ab096849848e75d1bc60dd39eeaf36c563078656 [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.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;
}
}
}