Package org.apache.cocoon.components.source.impl

Source Code of org.apache.cocoon.components.source.impl.CachingSource

/*
* 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: CachingSource.java 1413488 2012-11-26 07:00:20Z crossley $
*/
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;
        }
    }
}
TOP

Related Classes of org.apache.cocoon.components.source.impl.CachingSource

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.