/* * 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.transformation.helpers; import java.io.IOException; import java.net.URL; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.component.Component; import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.parameters.ParameterException; import org.apache.avalon.framework.parameters.Parameterizable; import org.apache.avalon.framework.parameters.Parameters; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; import org.apache.avalon.framework.thread.ThreadSafe; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CachedResponse; import org.apache.cocoon.components.sax.XMLDeserializer; import org.apache.cocoon.components.sax.XMLSerializer; import org.apache.cocoon.components.sax.XMLTeePipe; import org.apache.cocoon.components.source.SourceUtil; import org.apache.cocoon.components.thread.RunnableManager; import org.apache.cocoon.environment.CocoonRunnable; import org.apache.cocoon.xml.XMLConsumer; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.store.Store; import org.xml.sax.SAXException; import EDU.oswego.cs.dl.util.concurrent.CountDown; /** * Default implementation of a {@link IncludeCacheManager}. * * This implementation requires a configuration, if preemptive * loading is used: * <parameter name="preemptive-loader-url" value="some url"/> * * This is a url inside cocoon, that contains the preemptive loader * url; it must be specified absolute (with http://...) * If this loader cannot be started, only an error is logged into the * log, so actually cached content is never updated! * * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a> * @version CVS $Id$ * @since 2.1 */ public final class DefaultIncludeCacheManager extends AbstractLogEnabled implements IncludeCacheManager, ThreadSafe, Serviceable, Disposable, Parameterizable, Component { private ServiceManager manager; private SourceResolver resolver; private Store store; private IncludeCacheStorageProxy defaultCacheStorage; private String preemptiveLoaderURI; /** * @see IncludeCacheManager#getSession(org.apache.avalon.framework.parameters.Parameters) */ public IncludeCacheManagerSession getSession(Parameters pars) { String sourceURI = pars.getParameter("source", null); IncludeCacheManagerSession session; if ( null == sourceURI ) { session = new IncludeCacheManagerSession(pars, this.defaultCacheStorage); } else { Source source = null; try { source = this.resolver.resolveURI(sourceURI); IncludeCacheStorageProxy proxy = new ModifiableSourceIncludeCacheStorageProxy(this.resolver, source.getURI(), this.getLogger()); session = new IncludeCacheManagerSession(pars, proxy); } catch (Exception local) { session = new IncludeCacheManagerSession(pars, this.defaultCacheStorage); this.getLogger().warn("Error creating writeable source.", local); } finally { this.resolver.release(source); } } if (session.isPreemptive()) { if ( null == this.preemptiveLoaderURI ) { this.getLogger().error("Preemptive loading is turned off because the preemptive-loader-url is not configured."); session.setPreemptive(false); } else { if ( !PreemptiveLoader.getInstance().alive ) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Booting preemptive loader: " + this.preemptiveLoaderURI); } PreemptiveBooter thread = new PreemptiveBooter( this.preemptiveLoaderURI ); try { final RunnableManager runnableManager = (RunnableManager)this.manager.lookup( RunnableManager.ROLE ); runnableManager.execute( thread ); this.manager.release( runnableManager ); } catch( final ServiceException se ) { getLogger().error( "Cannot lookup RunnableManager", se ); } } } } if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Creating cache manager session: " + session); } return session; } /** * @see IncludeCacheManager#load(java.lang.String, IncludeCacheManagerSession) */ public String load(String uri, IncludeCacheManagerSession session) throws IOException, SourceException { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Load " + uri + " for session " + session); } // first make the URI absolute if ( uri.indexOf("://") == -1) { final Source source = session.resolveURI(uri, this.resolver); uri = source.getURI(); } // if we are not processing in parallel (or do preemptive) // then we don't have to do anything in this method - everything // is done in the stream method. // if we are processing in parallel (and not preemptive) then.... if ( session.isParallel() && !session.isPreemptive()) { // first look-up if we have a valid stored response IncludeCacheStorageProxy storage = session.getCacheStorageProxy(); CachedResponse response = (CachedResponse)storage.get(uri); if ( null != response) { SourceValidity[] validities = response.getValidityObjects(); // if we are valid and do not purging if ( !session.isPurging() && validities[0].isValid() == SourceValidity.VALID) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Using cached response for parallel processing."); } session.add(uri, response.getResponse()); return uri; } else { // response is not used storage.remove(uri); } } if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Starting parallel thread for loading " + uri); } // now we start a parallel thread, this thread gets all required avalon components // so it does not have to lookup them by itself try { XMLSerializer serializer = (XMLSerializer)this.manager.lookup(XMLSerializer.ROLE); Source source = session.resolveURI(uri, this.resolver); LoaderThread loader = new LoaderThread(source, serializer, this.manager); // Load the included content in a thread that inherits the current thread's environment Thread thread = new Thread(new CocoonRunnable(loader)); session.add(uri, loader); thread.start(); if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Thread started for " + uri); } } catch (ServiceException ce) { throw new SourceException("Unable to lookup thread pool or xml serializer.", ce); } catch (Exception e) { throw new SourceException("Unable to get pooled thread.", e); } } return uri; } /** * @see IncludeCacheManager#stream(java.lang.String, IncludeCacheManagerSession, XMLConsumer) */ public void stream(String uri, IncludeCacheManagerSession session, XMLConsumer handler) throws IOException, SourceException, SAXException { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Stream " + uri + " for session " + session); } // if we are processing in parallel (and not preemptive) then.... if ( session.isParallel() && !session.isPreemptive()) { // get either the cached content or the pooled thread Object object = session.get(uri); if ( null == object ) { // this should never happen! throw new SAXException("No pooled thread found for " + uri); } byte[] result; // is this a pooled thread? if (object instanceof LoaderThread) { LoaderThread loader = (LoaderThread)object; if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Waiting for pooled thread to finish loading."); } // wait for it loader.join(); if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Pooled thread finished loading."); } // did an exception occur? Then reraise it if ( null != loader.exception) { if ( loader.exception instanceof SAXException ) { throw (SAXException)loader.exception; } else if (loader.exception instanceof SourceException ) { throw (SourceException)loader.exception; } else if (loader.exception instanceof IOException) { throw (IOException)loader.exception; } else { throw new SAXException("Exception.", loader.exception); } } if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Streaming from pooled thread."); } result = loader.content; // cache the response (remember preemptive is off) if (session.getExpires() > 0) { SourceValidity[] validities = new SourceValidity[1]; validities[0] = session.getExpiresValidity(); CachedResponse response = new CachedResponse(validities, result); session.getCacheStorageProxy().put(uri, response); } } else { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Streaming from cached response."); } // use the response from the cache result = (byte[])object; } // stream the content XMLDeserializer deserializer = null; try { deserializer = (XMLDeserializer)this.manager.lookup( XMLDeserializer.ROLE ); deserializer.setConsumer(handler); deserializer.deserialize(result); } catch (ServiceException ce) { throw new SAXException("Unable to lookup xml deserializer.", ce); } finally { this.manager.release( deserializer ); } return; } else { // we are not processing parallel // first: test for a cached response IncludeCacheStorageProxy storage = session.getCacheStorageProxy(); CachedResponse response = (CachedResponse)storage.get(uri); if ( null != response) { SourceValidity[] validities = response.getValidityObjects(); // if purging is turned off and either the cached response is valid or // we are loading preemptive, then use the cached response if ( !session.isPurging() && (session.isPreemptive() || validities[0].isValid() == SourceValidity.VALID)) { // stream the content if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Streaming from cached response."); } XMLDeserializer deserializer = null; try { deserializer = (XMLDeserializer)this.manager.lookup( XMLDeserializer.ROLE ); deserializer.setConsumer(handler); deserializer.deserialize(response.getResponse()); } catch (ServiceException ce) { throw new SAXException("Unable to lookup xml deserializer.", ce); } finally { this.manager.release( deserializer ); } // load preemptive if the response is not valid if ( session.getExpires() > 0 && session.isPreemptive() && validities[0].isValid() != SourceValidity.VALID) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Add uri to preemptive loader list " + uri); } if (!PreemptiveLoader.getInstance().alive) { this.getLogger().error("Preemptive loader has not started yet."); } PreemptiveLoader.getInstance().add(session.getCacheStorageProxy(), uri, session.getExpires()); } return; } else { // cached response is not valid storage.remove(uri); } } } // we are not processing in parallel and have no (valid) cached response XMLSerializer serializer = null; try { final Source source = session.resolveURI(uri, this.resolver); // stream directly (and cache the response) if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Streaming directly from source."); } if (session.getExpires() > 0) { serializer = (XMLSerializer)this.manager.lookup(XMLSerializer.ROLE); XMLTeePipe tee = new XMLTeePipe(handler, serializer); SourceUtil.toSAX(source, tee); SourceValidity[] validities = new SourceValidity[1]; validities[0] = session.getExpiresValidity(); CachedResponse response = new CachedResponse(validities, (byte[])serializer.getSAXFragment()); session.getCacheStorageProxy().put(uri, response); } else { SourceUtil.toSAX(source, handler); } } catch (ProcessingException pe) { throw new SAXException("ProcessingException", pe); } catch (ServiceException e) { throw new SAXException("Unable to lookup xml serializer.", e); } finally { this.manager.release(serializer); } } /** * @see IncludeCacheManager#terminateSession(IncludeCacheManagerSession) */ public void terminateSession(IncludeCacheManagerSession session) { if (this.getLogger().isDebugEnabled()) { this.getLogger().debug("Terminating cache manager session " + session); } session.cleanup(this.resolver); } /** * @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager) */ public void service(ServiceManager manager) throws ServiceException { this.manager = manager; this.resolver = (SourceResolver)this.manager.lookup(SourceResolver.ROLE); } /** * @see org.apache.avalon.framework.activity.Disposable#dispose() */ public void dispose() { // stop preemptive loader (if running) PreemptiveLoader.getInstance().stop(); if ( null != this.manager ) { this.manager.release( this.resolver); this.manager.release(this.store); this.store = null; this.resolver = null; this.manager = null; this.defaultCacheStorage = null; } } /** * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(org.apache.avalon.framework.parameters.Parameters) */ public void parameterize(Parameters parameters) throws ParameterException { this.preemptiveLoaderURI = parameters.getParameter("preemptive-loader-url", null); if ( null != this.preemptiveLoaderURI && this.preemptiveLoaderURI.indexOf("://") == -1) { throw new ParameterException("The preemptive-loader-url must be absolute: " + this.preemptiveLoaderURI); } final String storeRole = parameters.getParameter("use-store", Store.ROLE); try { this.store = (Store)this.manager.lookup(storeRole); } catch (ServiceException e) { throw new ParameterException("Unable to lookup store with role " + storeRole, e); } this.defaultCacheStorage = new StoreIncludeCacheStorageProxy(this.store, this.getLogger()); } final private static class LoaderThread implements Runnable { final private Source source; final private XMLSerializer serializer; final private CountDown finished; Exception exception; byte[] content; final private ServiceManager manager; public LoaderThread(Source source, XMLSerializer serializer, ServiceManager manager) { this.source = source; this.serializer = serializer; this.finished = new CountDown( 1 ); this.manager = manager; } public void run() { try { SourceUtil.toSAX(this.source, this.serializer); this.content = (byte[])this.serializer.getSAXFragment(); } catch (Exception local) { this.exception = local; } finally { this.manager.release( this.serializer ); this.finished.release(); } } void join() { try { this.finished.acquire(); } catch ( final InterruptedException ie) { // ignore } } } final static class PreemptiveBooter implements Runnable { private final String uri; public PreemptiveBooter( final String uri ) { this.uri = uri; } public void run() { try { URL url = new URL(this.uri); url.getContent(); } catch (Exception ignore) { } } } }