/* * 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.modules.input; 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.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.ConfigurationException; import org.apache.excalibur.source.SourceResolver; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.Source; import org.apache.excalibur.store.Store; 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.PatternException; import org.apache.cocoon.ProcessingException; import org.apache.commons.collections.map.ReferenceMap; import org.apache.commons.collections.map.AbstractReferenceMap; import org.w3c.dom.Document; import org.xml.sax.SAXException; import java.util.Map; import java.net.MalformedURLException; import java.io.IOException; /** * <grammar> * <define name="input.module.config.contents" combine="choice"> * <optional><element name="cacheable"><data type="boolean"/></element></optional> * <optional><element name="reloadable"><data type="boolean"/></element></optional> * <optional> * <ref name="org.apache.cocoon.components.modules.input.XPathXMLFileModule:file"> * </optional> * <optional><element name="cache-role"><data type="String"/></element></optional> * </define> * <p/> * <define name="input.module.runtime.contents" combine="choice"> * <optional> * <ref name="org.apache.cocoon.components.modules.input.XPathXMLFileModule:file"> * </optional> * </define> * <p/> * <define name="org.apache.cocoon.components.modules.input.XPathXMLFileModule:file"> * <element name="file"> * <attribute name="src"><data type="anyURI"/></attribute> * <optional><attribute name="cacheable"><data type="boolean"/></attribute></optional> * <optional><attribute name="reloadable"><data type="boolean"/></attribute></optional> * </element> * </define> * </grammar> * <p/> * This module provides an Input Module interface to any XML document, by using * XPath expressions as attribute keys. * The XML can be obtained from any Cocoon <code>Source</code> (e.g., * <code>cocoon:/...</code>, <code>context://..</code>, and regular URLs). * Sources can be cached in memory for better performance and reloaded if * changed. The source can also contain references to other input modules to allow the source * file name to be determined dynamically. * <p/> * Caching and reloading can be turned on / off (default: caching on, * reloading off) through <code><reloadable>false</reloadable></code> * and <code><cacheable>false</cacheable></code>. The file * (source) to use is specified through <code><file * src="protocol:path/to/file.xml" reloadable="true" cacheable="true"/></code> * optionally overriding the defaults for caching and/or reloading. When specfied as attributes * to the file element the values for cacheable and reloadable may be input module references which * will be resolved on every call. These must resolve to 'true' or 'false'. * </> * The XML documents will be cached using the Store configured via the cache-role configuration * element. If not specified the default Store as specified in this classes ROLE attribute will * be used. * <p/> * In addition, xpath expressions can be cached for higher performance. * Thus, if an expression has been evaluated for a file, the result * is cached and will be reused, the expression is not evaluated * a second time. This can be turned off using the <code>cache-expressions</code> * configuration option. * * @version $Id: $ */ public class XPathXMLFileModule extends AbstractInputModule implements Serviceable, ThreadSafe { public static final String ROLE = Store.ROLE + "/XPathXMLFileTransientStore"; /** * Contains all globally registered extension classes and * packages. Thus the lookup and loading of globally registered * extensions is done only once. */ protected JXPathHelperConfiguration configuration; /** * Static (cocoon.xconf) configuration location, for error reporting */ String staticConfLocation; /** * Cached documents */ private Store cache; /** * Determines whether the configured source document should be cached. */ private String cacheParm; private Boolean cacheSource; /** * Determines whether the configured source document should be reloaded. */ private String reloadParm; private Boolean reloadSource; /** * Default value for reloadability of sources. Defaults to false. */ boolean reloadAll; /** * Default value for cacheability of xpath expressions. Defaults to true. */ private boolean cacheExpressions; /** * Whether the source needs to be resolved. */ private boolean needsResolve; /** * Overrides attribute name */ protected String parameter; /** * Default src */ private String src; protected SourceResolver resolver; protected ServiceManager manager; /* (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; this.resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); } /** * Static (cocoon.xconf) configuration. * Configuration is expected to be of the form: * <...> * <reloadable><b>true</b>|false</reloadable> * <cacheable><b>true</b>|false</cacheable> * <cache-role>org.apache.excalibur.store.Store/TransientStore</cache-role> * <file src="<i>src</i>"/> * ... * </...> * <p/> * The <file/> element specifies a file pattern. Only one * <file> can be specified, however it can contain references to input modules which will be resolved * each time the module is used. The configured <i>src</i> is used if not * overridden via a file parameter in the sitemap. * * @param config a <code>Configuration</code> value, as described above. * @throws org.apache.avalon.framework.configuration.ConfigurationException * if an error occurs */ public void configure(Configuration config) throws ConfigurationException { this.configuration = JXPathHelper.setup(config); this.staticConfLocation = config.getLocation(); Configuration roleConfig = config.getChild("cache-role", true); boolean cacheAll = config.getChild("cacheable").getValueAsBoolean(true); this.reloadAll = config.getChild("reloadable").getValueAsBoolean(true); String cacheRole = roleConfig.getValue(ROLE); if (getLogger().isDebugEnabled()) { getLogger().debug("Using cache " + cacheRole); } try { this.cache = (Store) this.manager.lookup(cacheRole); } catch (ServiceException ce) { throw new ConfigurationException("Unable to lookup cache: " + cacheRole, ce); } Configuration fileConfig = config.getChild("file"); this.src = fileConfig.getAttribute("src"); this.cacheParm = fileConfig.getAttribute("cacheable", null); this.reloadParm = fileConfig.getAttribute("reloadable", null); if (this.cacheParm == null) { this.cacheSource = Boolean.valueOf(cacheAll); } else if (VariableResolverFactory.needsResolve(this.cacheParm)) { this.cacheSource = null; } else { this.cacheSource = Boolean.valueOf(this.cacheParm); } if (this.reloadParm == null) { this.reloadSource = Boolean.valueOf(this.reloadAll); } else if (VariableResolverFactory.needsResolve(this.reloadParm)) { this.reloadSource = null; } else { this.reloadSource = Boolean.valueOf(this.reloadParm); } // init caches this.cacheExpressions = config.getChild("cache-expressions").getValueAsBoolean(true); this.needsResolve = VariableResolverFactory.needsResolve(this.src); } /** * Dispose this component */ public void dispose() { super.dispose(); if (this.manager != null) { this.manager.release(this.resolver); this.manager.release(this.cache); this.resolver = null; this.cache = null; this.manager = null; } } public Object getAttribute(String name, Configuration modeConf, Map objectModel) throws ConfigurationException { return getAttribute(name, modeConf, objectModel, false); } public Object[] getAttributeValues(String name, Configuration modeConf, Map objectModel) throws ConfigurationException { Object result = getAttribute(name, modeConf, objectModel, true); return (result != null ? (Object[]) result : null); } /** * Get the DocumentInfo for the DOM object that JXPath will operate on when evaluating * attributes. This DOM is loaded from a Source, specified in the * modeConf, or (if modeConf is null) from the * {@link #configure(org.apache.avalon.framework.configuration.Configuration)}. * * @param name The JXPath to retrieve * @param modeConf The dynamic configuration for the current operation. May * be <code>null</code>, in which case static (cocoon.xconf) configuration * is used. Configuration is expected to have a <file> child node, and * be of the form: * <...> * <file src="..." reloadable="true|false"/> * </...> * @param objectModel Object Model for the current module operation. * @param getValues true if multiple values should be retrieve, false otherwise * @return the result of the XPath query into the XML document * @throws ConfigurationException if an error occurs. */ private Object getAttribute(String name, Configuration modeConf, Map objectModel, boolean getValues) throws ConfigurationException { if (modeConf != null) { name = modeConf.getChild("parameter").getValue(this.parameter != null ? this.parameter : name); } boolean hasDynamicConf = false; // whether we have a <file src="..."> dynamic configuration Configuration fileConf = null; // the nested <file>, if any if (modeConf != null && modeConf.getChildren().length > 0) { fileConf = modeConf.getChild("file", false); if (fileConf == null) { if (getLogger().isDebugEnabled()) { getLogger().debug("Missing 'file' child element at " + modeConf.getLocation()); } } else { hasDynamicConf = true; } } String src = this.src; Boolean cacheSource = this.cacheSource; Boolean reloadSource = this.cacheSource; boolean needsResolve = this.needsResolve; String cacheParm = this.cacheParm; String reloadParm = this.reloadParm; if (hasDynamicConf) { src = fileConf.getAttribute("src"); cacheParm = fileConf.getAttribute("cacheable", this.cacheParm); reloadParm = fileConf.getAttribute("reloadable", this.reloadParm); if (cacheParm == null) { cacheSource = this.cacheSource; } else if (VariableResolverFactory.needsResolve(cacheParm)) { cacheSource = null; if (cacheSource == null) { try { VariableResolver varResolver = VariableResolverFactory.getResolver(cacheParm, this.manager); cacheSource = Boolean.valueOf(varResolver.resolve(objectModel)); } catch (PatternException pe) { throw new ConfigurationException("Error resolving " + cacheParm, pe); } } } else { cacheSource = Boolean.valueOf(cacheParm); } if (reloadParm == null) { reloadSource = this.reloadSource; } else if (VariableResolverFactory.needsResolve(reloadParm)) { reloadSource = null; } else { reloadSource = Boolean.valueOf(reloadParm); } needsResolve = true; } if (cacheSource == null) { try { VariableResolver varResolver = VariableResolverFactory.getResolver(cacheParm, this.manager); cacheSource = Boolean.valueOf(varResolver.resolve(objectModel)); } catch (PatternException pe) { throw new ConfigurationException("Error resolving " + cacheParm, pe); } } if (reloadSource == null) { try { VariableResolver varResolver = VariableResolverFactory.getResolver(reloadParm, this.manager); reloadSource = Boolean.valueOf(varResolver.resolve(objectModel)); } catch (PatternException pe) { throw new ConfigurationException("Error resolving " + reloadParm, pe); } } if (src == null) { throw new ConfigurationException( "No source specified" + (modeConf != null ? ", either dynamically in " + modeConf.getLocation() + ", or " : "") + " statically in " + staticConfLocation); } if (needsResolve) { try { VariableResolver varResolver = VariableResolverFactory.getResolver(src, this.manager); src = varResolver.resolve(objectModel); } catch (PatternException pe) { throw new ConfigurationException("Error resolving variables for " + src, pe); } } Object result; if (cacheSource.booleanValue()) { DocumentInfo info = (DocumentInfo) this.cache.get(src); if (info == null || (reloadSource.booleanValue() && !info.isValid())) { Source docSource = null; try { docSource = resolver.resolveURI(src); DocumentInfo newInfo = new DocumentInfo(src, SourceUtil.toDOM(docSource), docSource.getValidity(), this.cacheExpressions, this.resolver); synchronized(this.cache) { DocumentInfo cachedInfo = (DocumentInfo)this.cache.get(src); if (cachedInfo == null || cachedInfo == info) { this.cache.store(src, newInfo); info = newInfo; } else { info = cachedInfo; } } } catch (MalformedURLException mue) { throw new ConfigurationException("Unable to resolve " + src, mue); } catch (IOException ioe) { throw new ConfigurationException("Unable to access" + src, ioe); } catch (ProcessingException pe) { throw new ConfigurationException("Unable to process " + src, pe); } catch (SAXException se) { throw new ConfigurationException("Error processing XML document " + src, se); } finally { if (docSource != null) { resolver.release(docSource); } } } if (info.cacheExpressions) { Map cache = getValues ? info.expressionValuesCache : info.expressionCache; synchronized (cache) { if (cache.containsKey(name)) { result = cache.get(name); if (getLogger().isDebugEnabled()) { getLogger().debug("for " + name + " using cached result " + result); } } else { result = getResult(name, info.document, modeConf, getValues); if (result != null) { cache.put(name, result); if (getLogger().isDebugEnabled()) { getLogger().debug("for " + name + " newly caching result " + result); } } else { if (getLogger().isDebugEnabled()) { getLogger().debug("for " + name + " result is null"); } } } } } else { result = getResult(name, info.document, modeConf, getValues); if (getLogger().isDebugEnabled()) { getLogger().debug("for " + name + " result is " + result); } } } else { Source docSource = null; try { docSource = resolver.resolveURI(src); result = getResult(name, SourceUtil.toDOM(docSource), modeConf, getValues); if (getLogger().isDebugEnabled()) { getLogger().debug("for " + name + " result is " + result); } } catch (MalformedURLException mue) { throw new ConfigurationException("Unable to resolve " + src, mue); } catch (IOException ioe) { throw new ConfigurationException("Unable to access" + src, ioe); } catch (ProcessingException pe) { throw new ConfigurationException("Unable to process " + src, pe); } catch (SAXException se) { throw new ConfigurationException("Error processing XML document " + src, se); } finally { if (docSource != null) { resolver.release(docSource); } } } return result; } private Object getResult(String name, Document document, Configuration modeConf, boolean getValues) throws ConfigurationException { Object result; if (getValues) { result = JXPathHelper.getAttributeValues(name, modeConf, this.configuration, document); } else { result = JXPathHelper.getAttributeValue(name, modeConf, this.configuration, document); } return result; } /** * Used to keep track of the Document, its validity and any cached expressions. */ private static class DocumentInfo { public DocumentInfo(String uri, Document doc, SourceValidity validity, boolean cacheExpressions, SourceResolver resolver) { this.cacheExpressions = cacheExpressions; if (cacheExpressions) { expressionCache = new ReferenceMap(AbstractReferenceMap.SOFT, AbstractReferenceMap.SOFT); expressionValuesCache = new ReferenceMap(AbstractReferenceMap.SOFT, AbstractReferenceMap.SOFT); } this.resolver = resolver; this.uri = uri; this.document = doc; this.validity = validity; } private boolean cacheExpressions; private final String uri; private final SourceValidity validity; private final SourceResolver resolver; /** * Source content cached as DOM Document */ private final Document document; private Map expressionCache; private Map expressionValuesCache; /** * Returns true if the document is valid, false otherwise. * <p/> * * @return returns true if the document is valid, false otherwise. */ private boolean isValid() { Source src = null; boolean result = true; try { int valid = validity == null ? SourceValidity.INVALID : validity.isValid(); if (valid == SourceValidity.UNKNOWN) { // Get new source and validity src = resolver.resolveURI(this.uri); SourceValidity newValidity = src.getValidity(); valid = validity.isValid(newValidity); } if (valid != SourceValidity.VALID) { result = false; } } catch (Exception ex) { result = false; } finally { if (src != null) { resolver.release(src); } } return result; } } }