blob: bfb2321b6d657e413876d1d7a4dd39a8727eaa7c [file] [log] [blame]
/*
* Copyright 1999-2005 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.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.avalon.excalibur.pool.Recyclable;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.AbstractConfiguration;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.configuration.DefaultConfiguration;
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.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.service.ServiceSelector;
import org.apache.avalon.framework.service.Serviceable;
import org.apache.cocoon.core.container.StandaloneServiceSelector;
import org.apache.cocoon.components.LifecycleHelper;
import org.apache.cocoon.components.source.SourceUtil;
import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory;
import org.apache.cocoon.components.treeprocessor.variables.VariableResolver;
import org.apache.cocoon.sitemap.ComponentLocator;
import org.apache.cocoon.sitemap.PatternException;
import org.apache.cocoon.sitemap.SitemapParameters;
import org.apache.cocoon.util.location.Location;
import org.apache.cocoon.util.location.LocationImpl;
import org.apache.cocoon.util.location.LocationUtils;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceResolver;
/**
*
* @version $Id$
*/
public class DefaultTreeBuilder
extends AbstractLogEnabled
implements TreeBuilder, Contextualizable, Serviceable,
Recyclable, Disposable {
protected Map attributes = new HashMap();
//----- lifecycle-related objects ------
/**
* This component's avalon context
*/
private Context context;
/**
* This component's service manager
*/
private ServiceManager manager;
// -------------------------------------
/**
* The tree processor that we are building.
*/
protected ConcreteTreeProcessor processor;
/**
* The namespace of configuration for the processor that we are building.
*/
protected String itsNamespace;
/**
* The context for the processor that we are building
* It is created by {@link #createContext(Configuration)}.
*/
private Context itsContext;
/**
* The service manager for the processor that we are building.
* It is created by {@link #createServiceManager(ClassLoader, Context, Configuration)}.
*/
private ServiceManager itsManager;
/**
* The classloader for the processor that we are building.
* It is created by {@link #createServiceManager(ClassLoader, Context, Configuration)}.
*/
protected ClassLoader itsClassLoader;
/**
* Helper object which sets up components in the context
* of the processor that we are building.
*/
private LifecycleHelper itsLifecycle;
/**
* Selector for ProcessingNodeBuilders which is set up
* in the context of the processor that we are building.
*/
private ServiceSelector itsBuilders;
/**
* The sitemap component information grabbed while building itsMaanger
*/
protected ProcessorComponentInfo itsComponentInfo;
/** Optional application container */
protected ComponentLocator applicationContainer;
/** Optional event listeners for the enter sitemap event */
protected List enterSitemapEventListeners = new ArrayList();
/** Optional event listeners for the leave sitemap event */
protected List leaveSitemapEventListeners = new ArrayList();
// -------------------------------------
/**
* Component processor of the parent manager
* (can be null for the root sitemap)
*/
protected ServiceManager parentProcessorManager;
/** Nodes gone through setupNode() that implement Initializable */
private List initializableNodes = new ArrayList();
/** Nodes gone through setupNode() that implement Disposable */
private List disposableNodes = new ArrayList();
/** NodeBuilders created by createNodeBuilder() that implement LinkedProcessingNodeBuilder */
private List linkedBuilders = new ArrayList();
/** Are we in a state that allows to get registered nodes ? */
private boolean canGetNode = false;
/** Nodes registered using registerNode() */
private Map registeredNodes = new HashMap();
/* (non-Javadoc)
* @see org.apache.avalon.framework.context.Contextualizable#contextualize(org.apache.avalon.framework.context.Context)
*/
public void contextualize(Context context) throws ContextException {
this.context = context;
}
/* (non-Javadoc)
* @see org.apache.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
*/
public void service(ServiceManager manager) throws ServiceException {
this.manager = manager;
}
/**
* Get the location of the treebuilder config file. Can be overridden for other versions.
* @return The location of the treebuilder config file
*/
protected String getBuilderConfigURL() {
return "resource://org/apache/cocoon/components/treeprocessor/sitemap-language.xml";
}
public void setParentProcessorManager(ServiceManager manager) {
this.parentProcessorManager = manager;
}
/* (non-Javadoc)
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#setAttribute(java.lang.String, java.lang.Object)
*/
public void setAttribute(String name, Object value) {
this.attributes.put(name, value);
}
/* (non-Javadoc)
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#getAttribute(java.lang.String)
*/
public Object getAttribute(String name) {
return this.attributes.get(name);
}
/**
* Create a context that will be used for all <code>Contextualizable</code>
* <code>ProcessingNodeBuilder</code>s and <code>ProcessingNode</code>s.
*
* <p>The default here is to simply return the context set in
* <code>contextualize()</code>, i.e. the context set by the calling
* <code>TreeProcessor</code>.
*
* <p>Subclasses can redefine this method to create a context local to
* a tree, such as for sitemap's &lt;map:components&gt;.
*
* @return a context
*/
protected Context createContext(Configuration tree)
throws Exception {
return this.context;
}
protected ClassLoader createClassLoader(Configuration tree) throws Exception {
// Useless method as it's redefined in SitemapLanguage
// which is the only used incarnation.
return Thread.currentThread().getContextClassLoader();
}
/**
* Create a service manager that will be used for all <code>Serviceable</code>
* <code>ProcessingNodeBuilder</code>s and <code>ProcessingNode</code>s.
*
* <p>The default here is to simply return the manager set in
* <code>compose()</code>, i.e. the component manager set by the calling
* <code>TreeProcessor</code>.
*
* <p>Subclasses can redefine this method to create a service manager local to
* a tree, such as for sitemap's &lt;map:components&gt;.
*
* @return a component manager
*/
protected ServiceManager createServiceManager(ClassLoader classloader, Context context, Configuration tree)
throws Exception {
return this.manager;
}
/* (non-Javadoc)
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#setProcessor(ConcreteTreeProcessor)
*/
public void setProcessor(ConcreteTreeProcessor processor) {
this.processor = processor;
}
/* (non-Javadoc)
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#getProcessor()
*/
public ConcreteTreeProcessor getProcessor() {
return this.processor;
}
public ServiceManager getBuiltProcessorManager() {
return this.itsManager;
}
public ClassLoader getBuiltProcessorClassLoader() {
return this.itsClassLoader;
}
/**
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#getComponentLocator()
*/
public ComponentLocator getComponentLocator() {
// Useless method as it's redefined in SitemapLanguage
return this.applicationContainer;
}
/**
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#getEnterSitemapEventListeners()
*/
public List getEnterSitemapEventListeners() {
// we make a copy here, so we can clear(recylce) the list after the
// sitemap is build
return (List)((ArrayList)this.enterSitemapEventListeners).clone();
}
/**
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#getLeaveSitemapEventListeners()
*/
public List getLeaveSitemapEventListeners() {
// we make a copy here, so we can clear(recylce) the list after the
// sitemap is build
return (List)((ArrayList)this.leaveSitemapEventListeners).clone();
}
/**
* @see org.apache.cocoon.components.treeprocessor.TreeBuilder#registerNode(java.lang.String, org.apache.cocoon.components.treeprocessor.ProcessingNode)
*/
public boolean registerNode(String name, ProcessingNode node) {
if ( this.registeredNodes.containsKey(name) ) {
return false;
}
this.registeredNodes.put(name, node);
return true;
}
public ProcessingNode getRegisteredNode(String name) {
if (this.canGetNode) {
return (ProcessingNode)this.registeredNodes.get(name);
}
throw new IllegalArgumentException("Categories are only available during buildNode()");
}
public ProcessingNodeBuilder createNodeBuilder(Configuration config) throws Exception {
// FIXME : check namespace
String nodeName = config.getName();
if (getLogger().isDebugEnabled()) {
getLogger().debug("Creating node builder for " + nodeName);
}
ProcessingNodeBuilder builder;
try {
builder = (ProcessingNodeBuilder) this.itsBuilders.select(nodeName);
} catch (ServiceException ce) {
// Is it because this element is unknown ?
if (this.itsBuilders.isSelectable(nodeName)) {
// No : rethrow
throw ce;
}
// Throw a more meaningful exception
String msg = "Unknown element '" + nodeName + "' at " + config.getLocation();
throw new ConfigurationException(msg);
}
builder.setBuilder(this);
if (builder instanceof LinkedProcessingNodeBuilder) {
this.linkedBuilders.add(builder);
}
return builder;
}
/**
* Create the tree once component manager and node builders have been set up.
* Can be overriden by subclasses to perform pre/post tree creation operations.
*/
protected ProcessingNode createTree(Configuration tree) throws Exception {
// Create a node builder from the top-level element
ProcessingNodeBuilder rootBuilder = createNodeBuilder(tree);
// Build the whole tree (with an empty buildModel)
return rootBuilder.buildNode(tree);
}
/**
* Resolve links : call <code>linkNode()</code> on all
* <code>LinkedProcessingNodeBuilder</code>s.
* Can be overriden by subclasses to perform pre/post resolution operations.
*/
protected void linkNodes() throws Exception {
// Resolve links
Iterator iter = this.linkedBuilders.iterator();
while(iter.hasNext()) {
((LinkedProcessingNodeBuilder)iter.next()).linkNode();
}
}
/**
* Get the namespace URI that builders should use to find their nodes.
*/
public String getNamespace() {
return this.itsNamespace;
}
/**
* Build a processing tree from a <code>Configuration</code>.
*/
public ProcessingNode build(Configuration tree) throws Exception {
// The namespace used in the whole sitemap is the one of the root element
this.itsNamespace = tree.getNamespace();
Configuration componentConfig = tree.getChild("components", false);
if (componentConfig == null) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Sitemap has no components definition at " + tree.getLocation());
}
componentConfig = new DefaultConfiguration("", "");
}
// Context and manager and classloader for the sitemap we build
this.itsContext = createContext(tree);
// this.itsClassLoader = createClassLoader(componentConfig);
//
// Thread currentThread = Thread.currentThread();
//ClassLoader oldClassLoader = currentThread.getContextClassLoader();
// currentThread.setContextClassLoader(this.itsClassLoader);
this.itsClassLoader = Thread.currentThread().getContextClassLoader();
this.itsManager = createServiceManager(this.itsClassLoader, this.itsContext, componentConfig);
this.itsComponentInfo = (ProcessorComponentInfo)this.itsManager.lookup(ProcessorComponentInfo.ROLE);
// Create a helper object to setup components
this.itsLifecycle = new LifecycleHelper(getLogger(),
this.itsContext,
this.itsManager,
null /* configuration */);
// Create & initialize the NodeBuilder selector.
{
StandaloneServiceSelector selector = new StandaloneServiceSelector() {
protected String getComponentInstanceName() {
return "node";
}
protected String getClassAttributeName() {
return "builder";
}
};
// Load the builder config file
SourceResolver resolver = (SourceResolver) this.manager.lookup(SourceResolver.ROLE);
String url = getBuilderConfigURL();
Configuration config;
try {
Source src = resolver.resolveURI(url);
try {
SAXConfigurationHandler handler = new SAXConfigurationHandler();
SourceUtil.toSAX(this.manager, src, null, handler);
config = handler.getConfiguration();
} finally {
resolver.release(src);
}
} catch (Exception e) {
throw new ConfigurationException("Could not load TreeBuilder configuration from " + url, e);
} finally {
this.manager.release(resolver);
}
LifecycleHelper.setupComponent(selector,
getLogger(),
this.itsContext,
this.itsManager,
config.getChild("nodes", false),
true);
this.itsBuilders = selector;
}
// Calls to getRegisteredNode() are forbidden
this.canGetNode = false;
// Collect all disposable variable resolvers
VariableResolverFactory.setDisposableCollector(this.disposableNodes);
ProcessingNode result = createTree(tree);
// Calls to getRegisteredNode() are now allowed
this.canGetNode = true;
linkNodes();
// Initialize all Initializable nodes
Iterator iter = this.initializableNodes.iterator();
while(iter.hasNext()) {
((Initializable)iter.next()).initialize();
}
// And that's all !
return result;
}
/**
* Return the list of <code>ProcessingNodes</code> part of this tree that are
* <code>Disposable</code>. Care should be taken to properly dispose them before
* trashing the processing tree.
*/
public List getDisposableNodes() {
return this.disposableNodes;
}
/**
* Setup a <code>ProcessingNode</code> by setting its location, calling all
* the lifecycle interfaces it implements and giving it the parameter map if
* it's a <code>ParameterizableNode</code>.
* <p>
* As a convenience, the node is returned by this method to allow constructs
* like <code>return treeBuilder.setupNode(new MyNode(), config)</code>.
*/
public ProcessingNode setupNode(ProcessingNode node, Configuration config)
throws Exception {
Location location = getLocation(config);
if (node instanceof AbstractProcessingNode) {
((AbstractProcessingNode)node).setLocation(location);
((AbstractProcessingNode)node).setSitemapExecutor(this.processor.getSitemapExecutor());
}
this.itsLifecycle.setupComponent(node, false);
if (node instanceof ParameterizableProcessingNode) {
Map params = getParameters(config, location);
((ParameterizableProcessingNode)node).setParameters(params);
}
if (node instanceof Initializable) {
this.initializableNodes.add(node);
}
if (node instanceof Disposable) {
this.disposableNodes.add(node);
}
return node;
}
protected LocationImpl getLocation(Configuration config) {
String prefix = "";
if (config instanceof AbstractConfiguration) {
//FIXME: AbstractConfiguration has a _protected_ getPrefix() method.
// So make some reasonable guess on the prefix until it becomes public
String namespace = null;
try {
namespace = ((AbstractConfiguration)config).getNamespace();
} catch (ConfigurationException e) {
// ignore
}
if ("http://apache.org/cocoon/sitemap/1.0".equals(namespace)) {
prefix="map";
}
}
StringBuffer desc = new StringBuffer().append('<');
if (prefix.length() > 0) {
desc.append(prefix).append(':').append(config.getName());
} else {
desc.append(config.getName());
}
String type = config.getAttribute("type", null);
if (type != null) {
desc.append(" type=\"").append(type).append('"');
}
desc.append('>');
Location rawLoc = LocationUtils.getLocation(config);
return new LocationImpl(desc.toString(), rawLoc.getURI(), rawLoc.getLineNumber(), rawLoc.getColumnNumber());
}
/**
* Get &lt;xxx:parameter&gt; elements as a <code>Map</code> of </code>ListOfMapResolver</code>s,
* that can be turned into parameters using <code>ListOfMapResolver.buildParameters()</code>.
*
* @return the Map of ListOfMapResolver, or <code>null</code> if there are no parameters.
*/
protected Map getParameters(Configuration config, Location location) throws ConfigurationException {
Configuration[] children = config.getChildren("parameter");
if (children.length == 0) {
// Parameters are only the component's location
// TODO Optimize this
return new SitemapParameters.LocatedHashMap(location, 0);
}
Map params = new SitemapParameters.LocatedHashMap(location, children.length+1);
for (int i = 0; i < children.length; i++) {
Configuration child = children[i];
if (true) { // FIXME : check namespace
String name = child.getAttribute("name");
String value = child.getAttribute("value");
try {
params.put(resolve(name), resolve(value));
} catch(PatternException pe) {
String msg = "Invalid pattern '" + value + "' at " + child.getLocation();
throw new ConfigurationException(msg, pe);
}
}
}
return params;
}
/**
* Get the type for a statement : it returns the 'type' attribute if present,
* and otherwhise the default type defined for this role in the components declarations.
*
* @throws ConfigurationException if the type could not be found.
*/
public String getTypeForStatement(Configuration statement, String role) throws ConfigurationException {
// Get the component type for the statement
String type = statement.getAttribute("type", null);
if (type == null) {
type = this.itsComponentInfo.getDefaultType(role);
}
if (type == null) {
throw new ConfigurationException("No default type exists for 'map:" + statement.getName() +
"' at " + statement.getLocation()
);
}
// Check that this type actually exists
ServiceSelector selector = null;
try {
selector = (ServiceSelector) this.itsManager.lookup(role + "Selector");
} catch (ServiceException e) {
throw new ConfigurationException("Cannot get service selector for 'map:" +
statement.getName() + "' at " + statement.getLocation(),
e);
}
if (!selector.isSelectable(type)) {
throw new ConfigurationException("Type '" + type + "' does not exist for 'map:" +
statement.getName() + "' at " + statement.getLocation());
}
this.itsManager.release(selector);
return type;
}
/**
* Resolve expression using its manager
*/
protected VariableResolver resolve (String expression)
throws PatternException {
return VariableResolverFactory.getResolver(expression, this.itsManager);
}
public void recycle() {
// Reset all data created during the build
this.attributes.clear();
this.canGetNode = false;
this.disposableNodes = new ArrayList(); // Must not be cleared as it's used for processor disposal
this.initializableNodes.clear();
this.linkedBuilders.clear();
this.parentProcessorManager = null; // Set in setParentProcessorManager()
this.processor = null; // Set in setProcessor()
this.itsNamespace = null; // Set in build()
LifecycleHelper.dispose(this.itsBuilders);
this.itsBuilders = null; // Set in build()
this.itsLifecycle = null; // Set in build()
this.itsManager = null; // Set in build()
this.itsContext = null; // Set in build()
this.registeredNodes.clear();
this.initializableNodes.clear();
this.linkedBuilders.clear();
this.canGetNode = false;
this.registeredNodes.clear();
VariableResolverFactory.setDisposableCollector(null);
this.applicationContainer = null;
this.enterSitemapEventListeners.clear();
this.leaveSitemapEventListeners.clear();
}
public void dispose() {
// Don't dispose manager or roles: they are used by the built tree
// and thus must live longer than the builder.
}
}