blob: 9520dbad8005fa94e20a5af48e75a70ae2dea5c1 [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation.
*
* Licensed 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.treeprocessor;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.avalon.excalibur.component.RoleManageable;
import org.apache.avalon.excalibur.component.RoleManager;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.Recomposable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.configuration.SAXConfigurationHandler;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Logger;
import org.apache.avalon.framework.thread.ThreadSafe;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.Processor;
import org.apache.cocoon.components.ChainedConfiguration;
import org.apache.cocoon.components.CocoonComponentManager;
import org.apache.cocoon.components.ExtendedComponentSelector;
import org.apache.cocoon.components.LifecycleHelper;
import org.apache.cocoon.components.pipeline.ProcessingPipeline;
import org.apache.cocoon.components.source.SourceUtil;
import org.apache.cocoon.components.source.impl.DelayedRefreshSourceWrapper;
import org.apache.cocoon.components.treeprocessor.sitemap.PipelinesNode;
import org.apache.cocoon.environment.Environment;
import org.apache.cocoon.environment.ForwardRedirector;
import org.apache.cocoon.environment.wrapper.EnvironmentWrapper;
import org.apache.cocoon.environment.wrapper.MutableEnvironmentFacade;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceResolver;
/**
* Interpreted tree-traversal implementation of a pipeline assembly language.
*
* @author <a href="mailto:sylvain@apache.org">Sylvain Wallez</a>
* @version CVS $Id: TreeProcessor.java,v 1.20 2004/03/05 13:02:51 bdelacretaz Exp $
*/
public class TreeProcessor
extends AbstractLogEnabled
implements ThreadSafe,
Processor,
Composable,
Configurable,
RoleManageable,
Contextualizable,
Disposable {
public static final String REDIRECTOR_ATTR = "sitemap:redirector";
public static final String COCOON_REDIRECT_ATTR = "sitemap:cocoon-redirect";
private static final String XCONF_URL =
"resource://org/apache/cocoon/components/treeprocessor/treeprocessor-builtins.xml";
/** The parent TreeProcessor, if any */
protected TreeProcessor parent;
/** The context */
protected Context context;
/** The component manager */
protected ComponentManager manager;
/** The role manager */
protected RoleManager roleManager;
/** The language used by this processor */
protected String language;
/** Selector of TreeBuilders, the hint is the language name */
protected ExtendedComponentSelector builderSelector;
/** The root node of the processing tree */
protected ProcessingNode rootNode;
/** The list of processing nodes that should be disposed when disposing this processor */
protected List disposableNodes;
/** Last modification time */
protected long lastModified = 0;
/** The source of the tree definition */
protected DelayedRefreshSourceWrapper source;
/** Delay for <code>sourceLastModified</code>. */
protected long lastModifiedDelay;
/** The current language configuration */
protected Configuration currentLanguage;
/** The file to process */
protected String fileName;
/** Check for reload? */
protected boolean checkReload;
/** The component configurations from the sitemap (if any) */
protected Configuration componentConfigurations;
/** The different sitemap component configurations */
protected Map sitemapComponentConfigurations;
/** The component manager for the sitemap */
protected ComponentManager sitemapComponentManager;
/** The source resolver */
protected SourceResolver resolver;
/**
* Create a TreeProcessor.
*/
public TreeProcessor() {
// Language can be overriden in the configuration.
this.language = "sitemap";
this.checkReload = true;
this.lastModifiedDelay = 1000;
}
/**
* Create a child processor for a given language
*/
protected TreeProcessor(TreeProcessor parent, ComponentManager manager, String language) {
this.parent = parent;
this.language = (language == null) ? parent.language : language;
// Copy all that can be copied from the parent
this.enableLogging(parent.getLogger());
this.context = parent.context;
this.roleManager = parent.roleManager;
this.builderSelector = parent.builderSelector;
this.checkReload = parent.checkReload;
this.lastModifiedDelay = parent.lastModifiedDelay;
// We have our own CM
this.manager = manager;
// Other fields are setup in initialize()
}
/**
* Create a new child of this processor (used for mounting submaps).
*
* @param manager the component manager to be used by the child processor.
* @param language the language to be used by the child processor.
* @return a new child processor.
*/
public TreeProcessor createChildProcessor(
ComponentManager manager,
String language,
Source source)
throws Exception {
// Note: lifecycle methods aren't called, since this constructors copies all
// that can be copied from the parent (see above)
TreeProcessor child = new TreeProcessor(this, manager, language);
child.source = new DelayedRefreshSourceWrapper(source, lastModifiedDelay);
return child;
}
public void contextualize(Context context) throws ContextException {
this.context = context;
}
public void compose(ComponentManager manager) throws ComponentException {
this.manager = manager;
this.resolver = (SourceResolver)this.manager.lookup(SourceResolver.ROLE);
}
public void setRoleManager(RoleManager rm) {
this.roleManager = rm;
}
/*
<processor>
<reload delay="10"/>
<root-language name="sitemap"/>
<language>...</language>
</processor>
*/
public void configure(Configuration config)
throws ConfigurationException {
this.fileName = config.getAttribute("file", null);
this.checkReload = config.getAttributeAsBoolean("check-reload", true);
Configuration rootLangConfig = config.getChild("root-language", false);
if (rootLangConfig != null) {
this.language = rootLangConfig.getAttribute("name");
}
// Obtain the configuration file, or use the XCONF_URL if none
// is defined
String xconfURL = config.getAttribute("config", XCONF_URL);
// Reload check delay. Default is 1 second.
this.lastModifiedDelay = config.getChild("reload").getAttributeAsLong("delay", 1000L);
// Read the builtin languages definition file
Configuration builtin;
try {
Source source = this.resolver.resolveURI( xconfURL );
try {
SAXConfigurationHandler handler = new SAXConfigurationHandler();
SourceUtil.toSAX( this.manager, source, null, handler);
builtin = handler.getConfiguration();
} finally {
this.resolver.release( source );
}
} catch(Exception e) {
String msg = "Error while reading " + xconfURL + ": " + e.getMessage();
throw new ConfigurationException(msg, e);
} finally {
this.manager.release( resolver );
}
// Create a selector for tree builders of all languages
this.builderSelector = new ExtendedComponentSelector(Thread.currentThread().getContextClassLoader());
try {
LifecycleHelper.setupComponent(this.builderSelector,
getLogger(),
this.context,
this.manager,
this.roleManager,
builtin
);
} catch(ConfigurationException ce) {
throw ce;
} catch(Exception e) {
throw new ConfigurationException("Could not setup builder selector", e);
}
}
/**
* Process the given <code>Environment</code> producing the output.
* @return If the processing is successfull <code>true</code> is returned.
* If not match is found in the sitemap <code>false</code>
* is returned.
* @throws org.apache.cocoon.ResourceNotFoundException If a sitemap component tries
* to access a resource which can not
* be found, e.g. the generator
* ConnectionResetException If the connection was reset
*/
public boolean process(Environment environment) throws Exception {
InvokeContext context = new InvokeContext();
context.enableLogging(getLogger());
try {
return process(environment, context);
} finally {
context.dispose();
}
}
/**
* Process the given <code>Environment</code> to assemble
* a <code>ProcessingPipeline</code>.
* @since 2.1
*/
public ProcessingPipeline buildPipeline(Environment environment)
throws Exception {
InvokeContext context = new InvokeContext( true );
context.enableLogging(getLogger());
try {
if ( process(environment, context) ) {
return context.getProcessingPipeline();
} else {
return null;
}
} finally {
context.dispose();
}
}
/**
* Do the actual processing, be it producing the response or just building the pipeline
* @param environment
* @param context
* @return true if the pipeline was successfully built, false otherwise.
* @throws Exception
*/
protected boolean process(Environment environment, InvokeContext context)
throws Exception {
// first, check for sitemap changes
if (this.rootNode == null ||
(this.checkReload && this.source.getLastModified() > this.lastModified)) {
setupRootNode(environment);
}
// and now process
CocoonComponentManager.enterEnvironment(environment, this.sitemapComponentManager, this);
Map objectModel = environment.getObjectModel();
Object oldResolver = objectModel.get(ProcessingNode.OBJECT_SOURCE_RESOLVER);
Object oldRedirector = environment.getAttribute(REDIRECTOR_ATTR);
// Build a redirector
TreeProcessorRedirector redirector = new TreeProcessorRedirector(environment, context);
setupLogger(redirector);
objectModel.put(ProcessingNode.OBJECT_SOURCE_RESOLVER, environment);
environment.setAttribute(REDIRECTOR_ATTR, redirector);
try {
boolean success = this.rootNode.invoke(environment, context);
return success;
} finally {
CocoonComponentManager.leaveEnvironment();
// Restore old redirector and resolver
environment.setAttribute(REDIRECTOR_ATTR, oldRedirector);
objectModel.put(PipelinesNode.OBJECT_SOURCE_RESOLVER, oldResolver);
}
}
private boolean handleCocoonRedirect(String uri, Environment environment, InvokeContext context) throws Exception {
// Build an environment wrapper
// If the current env is a facade, change the delegate and continue processing the facade, since
// we may have other redirects that will in turn also change the facade delegate
MutableEnvironmentFacade facade = environment instanceof MutableEnvironmentFacade ?
((MutableEnvironmentFacade)environment) : null;
if (facade != null) {
// Consider the facade delegate (the real environment)
environment = facade.getDelegate();
}
Environment newEnv = new ForwardEnvironmentWrapper(environment, this.manager, uri, getLogger());
if (facade != null) {
// Change the facade delegate
facade.setDelegate((EnvironmentWrapper)newEnv);
newEnv = facade;
}
// Get the processor that should process this request
TreeProcessor processor;
if (newEnv.getRootContext() == newEnv.getContext()) {
processor = (TreeProcessor)getRootProcessor();
} else {
processor = this;
}
// Process the redirect
// No more reset since with TreeProcessorRedirector, we need to pop values from the redirect location
// context.reset();
return processor.process(newEnv, context);
}
/**
* Get the root parent of this processor
* @since 2.1.1
*/
public Processor getRootProcessor() {
TreeProcessor result = this;
while(result.parent != null) {
result = result.parent;
}
return result;
}
/**
* Set the sitemap component configurations
*/
public void setComponentConfigurations(Configuration componentConfigurations) {
this.componentConfigurations = componentConfigurations;
this.sitemapComponentConfigurations = null;
}
/**
* Get the sitemap component configurations
* @since 2.1
*/
public Map getComponentConfigurations() {
// do we have the sitemap configurations prepared for this processor?
if ( null == this.sitemapComponentConfigurations ) {
synchronized (this) {
if ( this.sitemapComponentConfigurations == null ) {
// do we have configurations?
final Configuration[] childs = (this.componentConfigurations == null
? null
: this.componentConfigurations.getChildren());
if ( null != childs ) {
if ( null == this.parent ) {
this.sitemapComponentConfigurations = new HashMap(12);
} else {
// copy all configurations from parent
this.sitemapComponentConfigurations = new HashMap(this.parent.getComponentConfigurations());
}
// and now check for new configurations
for(int m = 0; m < childs.length; m++) {
final String r = this.roleManager.getRoleForName(childs[m].getName());
this.sitemapComponentConfigurations.put(r, new ChainedConfiguration(childs[m],
(ChainedConfiguration)this.sitemapComponentConfigurations.get(r)));
}
} else {
// we don't have configurations
if ( null == this.parent ) {
this.sitemapComponentConfigurations = Collections.EMPTY_MAP;
} else {
// use configuration from parent
this.sitemapComponentConfigurations = this.parent.getComponentConfigurations();
}
}
}
}
}
return this.sitemapComponentConfigurations;
}
protected synchronized void setupRootNode(Environment env) throws Exception {
// Now that we entered the synchronized area, recheck what's already
// been checked in process().
if (this.rootNode != null && source.getLastModified() <= this.lastModified) {
// Nothing changed
return;
}
long startTime = System.currentTimeMillis();
// Dispose the previous tree, if any
disposeTree();
// Get a builder
TreeBuilder builder = (TreeBuilder)this.builderSelector.select(this.language);
ProcessingNode root;
try {
if (builder instanceof Recomposable) {
((Recomposable)builder).recompose(this.manager);
}
builder.setProcessor(this);
if (this.fileName == null) {
this.fileName = builder.getFileName();
}
if (this.source == null) {
this.source = new DelayedRefreshSourceWrapper(this.resolver.resolveURI(this.fileName), lastModifiedDelay);
}
root = builder.build(this.source);
this.sitemapComponentManager = builder.getSitemapComponentManager();
this.disposableNodes = builder.getDisposableNodes();
} finally {
this.builderSelector.release(builder);
}
this.lastModified = System.currentTimeMillis();
if (getLogger().isDebugEnabled()) {
double time = (this.lastModified - startTime) / 1000.0;
getLogger().debug("TreeProcessor built in " + time + " secs from " + source.getURI());
}
// Finished
this.rootNode = root;
}
public void dispose() {
disposeTree();
if (this.parent == null) {
// root processor : dispose the builder selector
this.builderSelector.dispose();
}
if ( this.manager != null ) {
if ( this.source != null ) {
this.resolver.release(this.source.getSource());
this.source = null;
}
this.manager.release(this.resolver);
this.resolver = null;
this.manager = null;
}
}
/**
* Dispose all nodes in the tree that are disposable
*/
protected void disposeTree() {
if (this.disposableNodes != null) {
// we must dispose the nodes in reverse order
// otherwise selector nodes are freed before the components node
for(int i=this.disposableNodes.size()-1; i>-1; i--) {
((Disposable)disposableNodes.get(i)).dispose();
}
this.disposableNodes = null;
}
}
private class TreeProcessorRedirector extends ForwardRedirector {
private InvokeContext context;
public TreeProcessorRedirector(Environment env, InvokeContext context) {
super(env);
this.context = context;
}
protected void cocoonRedirect(String uri) throws IOException, ProcessingException {
try {
TreeProcessor.this.handleCocoonRedirect(uri, this.env, this.context);
} catch(IOException ioe) {
throw ioe;
} catch(ProcessingException pe) {
throw pe;
} catch(RuntimeException re) {
throw re;
} catch(Exception ex) {
throw new ProcessingException(ex);
}
}
}
/**
* Local extension of EnvironmentWrapper to propagate otherwise blocked
* methods to the actual environment.
*/
private static final class ForwardEnvironmentWrapper extends EnvironmentWrapper {
public ForwardEnvironmentWrapper(Environment env,
ComponentManager manager, String uri, Logger logger) throws MalformedURLException {
super(env, manager, uri, logger);
}
public void setStatus(int statusCode) {
environment.setStatus(statusCode);
}
public void setContentLength(int length) {
environment.setContentLength(length);
}
public void setContentType(String contentType) {
environment.setContentType(contentType);
}
public String getContentType() {
return environment.getContentType();
}
}
}