| /* |
| * 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.treeprocessor; |
| |
| import org.apache.avalon.excalibur.component.DefaultRoleManager; |
| import org.apache.avalon.excalibur.component.ExcaliburComponentSelector; |
| import org.apache.avalon.excalibur.component.RoleManageable; |
| import org.apache.avalon.excalibur.component.RoleManager; |
| 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.component.ComponentException; |
| import org.apache.avalon.framework.component.ComponentManager; |
| import org.apache.avalon.framework.component.ComponentSelector; |
| import org.apache.avalon.framework.component.Recomposable; |
| import org.apache.avalon.framework.configuration.AbstractConfiguration; |
| 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.NamespacedSAXConfigurationHandler; |
| 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.cocoon.ProcessingException; |
| import org.apache.cocoon.components.ExtendedComponentSelector; |
| import org.apache.cocoon.components.LifecycleHelper; |
| import org.apache.cocoon.components.PropertyAwareNamespacedSAXConfigurationHandler; |
| import org.apache.cocoon.components.source.SourceUtil; |
| import org.apache.cocoon.components.treeprocessor.variables.VariableResolverFactory; |
| 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.cocoon.util.Settings; |
| import org.apache.cocoon.util.SettingsHelper; |
| import org.apache.excalibur.source.Source; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * |
| * @author <a href="mailto:sylvain@apache.org">Sylvain Wallez</a> |
| * @version CVS $Id$ |
| */ |
| |
| public class DefaultTreeBuilder extends AbstractLogEnabled implements TreeBuilder, |
| Recomposable, Configurable, Contextualizable, RoleManageable, Recyclable, Disposable { |
| |
| protected Map attributes = new HashMap(); |
| |
| /** |
| * The tree processor that we're building. |
| */ |
| protected ConcreteTreeProcessor processor; |
| |
| //----- lifecycle-related objects ------ |
| protected Context context; |
| |
| /** |
| * The parent component manager, set using <code>compose()</code> and <code>recompose()</code> |
| * (implementation of <code>Recomposable</code>). |
| */ |
| protected ComponentManager parentManager; |
| |
| /** |
| * The parent role manager, set using <code>setRoleManager</code> (implementation of |
| * <code>RoleManageable</code>). |
| */ |
| protected RoleManager parentRoleManager; |
| |
| protected Configuration configuration; |
| // ------------------------------------- |
| |
| /** |
| * Component manager created by {@link #createComponentManager(Configuration)}. |
| */ |
| protected ComponentManager manager; |
| |
| /** |
| * Role manager result created by {@link #createRoleManager()}. |
| */ |
| protected RoleManager roleManager; |
| |
| /** Selector for ProcessingNodeBuilders */ |
| protected ComponentSelector builderSelector; |
| |
| protected LifecycleHelper lifecycle; |
| |
| protected String namespace; |
| |
| protected String parameterElement; |
| |
| protected String languageName; |
| |
| protected String fileName; |
| |
| /** 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(); |
| |
| |
| public void contextualize(Context context) throws ContextException { |
| this.context = context; |
| } |
| |
| public void compose(ComponentManager manager) throws ComponentException { |
| this.parentManager = manager; |
| } |
| |
| public void recompose(ComponentManager manager) throws ComponentException { |
| this.parentManager = manager; |
| } |
| |
| public void setRoleManager(RoleManager rm) { |
| this.parentRoleManager = rm; |
| } |
| |
| /** |
| * Configurable |
| */ |
| public void configure(Configuration config) throws ConfigurationException { |
| this.configuration = config; |
| |
| this.languageName = config.getAttribute("name"); |
| if (this.getLogger().isDebugEnabled()) { |
| getLogger().debug("Configuring Builder for language : " + this.languageName); |
| } |
| |
| this.fileName = config.getChild("file").getAttribute("name"); |
| |
| this.namespace = config.getChild("namespace").getAttribute("uri", ""); |
| |
| this.parameterElement = config.getChild("parameter").getAttribute("element", "parameter"); |
| } |
| |
| public void setAttribute(String name, Object value) { |
| this.attributes.put(name, value); |
| } |
| |
| public Object getAttribute(String name) { |
| return this.attributes.get(name); |
| } |
| |
| /** |
| * Create a role manager that will be used by all <code>RoleManageable</code> |
| * components. The default here is to create a role manager with the contents of |
| * the <roles> element of the configuration. |
| * <p> |
| * Subclasses can redefine this method to create roles from other sources than |
| * the one used here. |
| * |
| * @return the role manager |
| */ |
| protected RoleManager createRoleManager() throws Exception |
| { |
| RoleManager roles = new DefaultRoleManager(this.parentRoleManager); |
| |
| LifecycleHelper.setupComponent(roles, |
| getLogger(), |
| this.context, |
| this.manager, |
| this.parentRoleManager, |
| this.configuration.getChild("roles") |
| ); |
| |
| return roles; |
| } |
| |
| /** |
| * Create a component manager that will be used for all <code>Composable</code> |
| * <code>ProcessingNodeBuilder</code>s and <code>ProcessingNode</code>s. |
| * <p> |
| * The default here is to simply return the manager set by <code>compose()</code>, |
| * i.e. the component manager set by the calling <code>TreeProcessor</code>. |
| * <p> |
| * Subclasses can redefine this method to create a component manager local to a tree, |
| * such as for sitemap's <map:components>. |
| * |
| * @return a component manager |
| */ |
| protected ComponentManager createComponentManager(Configuration tree) throws Exception |
| { |
| return this.parentManager; |
| } |
| |
| /** |
| * Create a <code>ComponentSelector</code> for <code>ProcessingNodeBuilder</code>s. |
| * It creates a selector with the contents of the "node" element of the configuration. |
| * |
| * @return a selector for node builders |
| */ |
| protected ComponentSelector createBuilderSelector() throws Exception { |
| |
| // Create the NodeBuilder selector. |
| ExcaliburComponentSelector selector = new ExtendedComponentSelector() { |
| protected String getComponentInstanceName() { |
| return "node"; |
| } |
| |
| protected String getClassAttributeName() { |
| return "builder"; |
| } |
| }; |
| |
| // Automagically initialize the selector |
| LifecycleHelper.setupComponent(selector, |
| getLogger(), |
| this.context, |
| this.manager, |
| this.roleManager, |
| this.configuration.getChild("nodes") |
| ); |
| |
| return selector; |
| } |
| |
| public void setProcessor(ConcreteTreeProcessor processor) { |
| this.processor = processor; |
| } |
| |
| public ConcreteTreeProcessor getProcessor() { |
| return this.processor; |
| } |
| |
| /** |
| * Returns the language that is being built (e.g. "sitemap"). |
| */ |
| public String getLanguage() { |
| return this.languageName; |
| } |
| |
| /** |
| * Returns the name of the parameter element. |
| */ |
| public String getParameterName() { |
| return this.parameterElement; |
| } |
| |
| /** |
| * @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); |
| } else { |
| throw new IllegalArgumentException("Categories are only available during buildNode()"); |
| } |
| } |
| |
| public ProcessingNodeBuilder createNodeBuilder(Configuration config) throws Exception { |
| //FIXME : check namespace |
| String nodeName = config.getName(); |
| |
| if (this.getLogger().isDebugEnabled()) { |
| getLogger().debug("Creating node builder for " + nodeName); |
| } |
| |
| ProcessingNodeBuilder builder; |
| try { |
| builder = (ProcessingNodeBuilder)this.builderSelector.select(nodeName); |
| |
| } catch(ComponentException ce) { |
| // Is it because this element is unknown ? |
| if (this.builderSelector.hasComponent(nodeName)) { |
| // No : rethrow |
| throw ce; |
| } else { |
| // Throw a more meaningful exception |
| String msg = "Unknown element '" + nodeName + "' at " + config.getLocation(); |
| throw new ConfigurationException(msg); |
| } |
| } |
| |
| if (builder instanceof Recomposable) { |
| ((Recomposable)builder).recompose(this.manager); |
| } |
| |
| 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.namespace; |
| } |
| |
| public ProcessingNode build(Source source) |
| throws Exception { |
| |
| try { |
| // Build a namespace-aware configuration object |
| Settings settings = SettingsHelper.getSettings(this.context); |
| NamespacedSAXConfigurationHandler handler = |
| new PropertyAwareNamespacedSAXConfigurationHandler(settings, getLogger()); |
| AnnotationsFilter annotationsFilter = new AnnotationsFilter(handler); |
| SourceUtil.toSAX( source, annotationsFilter ); |
| Configuration treeConfig = handler.getConfiguration(); |
| |
| return build(treeConfig); |
| } catch (ProcessingException e) { |
| throw e; |
| } catch(Exception e) { |
| throw new ProcessingException("Failed to load " + this.languageName + " from " + |
| source.getURI(), e); |
| } |
| } |
| |
| public String getFileName() { |
| return this.fileName; |
| } |
| |
| /** |
| * Build a processing tree from a <code>Configuration</code>. |
| */ |
| public ProcessingNode build(Configuration tree) throws Exception { |
| |
| this.roleManager = createRoleManager(); |
| |
| this.manager = createComponentManager(tree); |
| |
| // Create a helper object to setup components |
| this.lifecycle = new LifecycleHelper(getLogger(), |
| this.context, |
| this.manager, |
| this.roleManager, |
| null // configuration |
| ); |
| |
| this.builderSelector = createBuilderSelector(); |
| |
| // 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; |
| } |
| |
| /** |
| * Return the sitemap component manager |
| */ |
| public ComponentManager getSitemapComponentManager() { |
| return this.manager; |
| } |
| |
| /** |
| * 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); |
| } |
| |
| this.lifecycle.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 = 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, null); |
| return new LocationImpl(desc.toString(), rawLoc.getURI(), rawLoc.getLineNumber(), rawLoc.getColumnNumber()); |
| } |
| |
| /** |
| * Get <xxx:parameter> 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(this.parameterElement); |
| |
| 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( |
| VariableResolverFactory.getResolver(name, this.manager), |
| VariableResolverFactory.getResolver(value, this.manager)); |
| } 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 hint of the <code>ExtendedSelector</code> designated by |
| * role <code>role</code>. |
| * |
| * @throws ConfigurationException if the default type could not be found. |
| */ |
| public String getTypeForStatement(Configuration statement, String role) throws ConfigurationException { |
| |
| String type = statement.getAttribute("type", null); |
| |
| ComponentSelector selector = null; |
| |
| try { |
| try { |
| selector = (ComponentSelector)this.manager.lookup(role); |
| } catch(ComponentException ce) { |
| String msg = "Cannot get component selector for '" + statement.getName() + "' at " + |
| statement.getLocation(); |
| throw new ConfigurationException(msg, ce); |
| } |
| |
| if (type == null && selector instanceof ExtendedComponentSelector) { |
| type = ((ExtendedComponentSelector)selector).getDefaultHint(); |
| } |
| |
| if (type == null) { |
| String msg = "No default type exists for '" + statement.getName() + "' at " + |
| statement.getLocation(); |
| throw new ConfigurationException(msg); |
| } |
| |
| if (!selector.hasComponent(type)) { |
| String msg = "Type '" + type + "' is not defined for '" + statement.getName() + "' at " + |
| statement.getLocation(); |
| throw new ConfigurationException(msg); |
| } |
| } finally { |
| this.manager.release(selector); |
| } |
| return type; |
| } |
| |
| public void recycle() { |
| this.lifecycle = null; // Created in build() |
| this.initializableNodes.clear(); |
| this.linkedBuilders.clear(); |
| this.canGetNode = false; |
| this.registeredNodes.clear(); |
| |
| // Don't clear disposableNodes as they're used by the Processor |
| this.disposableNodes = new ArrayList(); |
| VariableResolverFactory.setDisposableCollector(null); |
| |
| this.processor = null; |
| this.manager = null; |
| this.roleManager = null; |
| } |
| |
| public void dispose() { |
| LifecycleHelper.dispose(this.builderSelector); |
| |
| // Don't dispose manager or roles : they are used by the built tree |
| // and thus must live longer than the builder. |
| } |
| } |