| /* |
| * 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.modules.input; |
| |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.logger.Logger; |
| 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.components.source.SourceUtil; |
| import org.apache.commons.collections.map.AbstractReferenceMap; |
| import org.apache.commons.collections.map.ReferenceMap; |
| import org.apache.excalibur.source.Source; |
| import org.apache.excalibur.source.SourceResolver; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.w3c.dom.Document; |
| |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| |
| <grammar> |
| <define name="input.module.config.contents" combine="choice"> |
| <optional><element name="reloadable"><data type="boolean"/></element></optional> |
| <optional><element name="cacheable"><data type="boolean"/></element></optional> |
| <optional> |
| <ref name="org.apache.cocoon.components.modules.input.XMLFileModule:file"> |
| </optional> |
| </define> |
| |
| <define name="input.module.runtime.contents" combine="choice"> |
| <optional> |
| <ref name="org.apache.cocoon.components.modules.input.XMLFileModule:file"> |
| </optional> |
| </define> |
| |
| <define name="org.apache.cocoon.components.modules.input.XMLFileModule:file"> |
| <element name="file"> |
| <attribute name="src"><data type="anyURI"/></attribute> |
| <optional><attribute name="reloadable"><data type="boolean"/></attribute></optional> |
| <optional><attribute name="cacheable"><data type="boolean"/></attribute></optional> |
| </element> |
| </define> |
| </grammar> |
| |
| * 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 held in memory for better performance and reloaded if |
| * changed. |
| * |
| * <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 defaults for |
| * caching and/or reloading.</p> |
| * |
| * <p>In addition, xpath expressions are 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.</p> |
| * |
| * @version $Id$ |
| */ |
| public class XMLFileModule extends AbstractJXPathModule |
| implements Serviceable, ThreadSafe { |
| |
| /** Static (cocoon.xconf) configuration location, for error reporting */ |
| String staticConfLocation; |
| |
| /** Cached documents */ |
| Map documents; |
| |
| /** Default value for reloadability of sources. Defaults to false. */ |
| boolean reloadAll; |
| |
| /** Default value for cacheability of sources. Defaults to true. */ |
| boolean cacheAll; |
| |
| /** Default value for cacheability of xpath expressions. Defaults to true. */ |
| boolean cacheExpressions; |
| |
| /** Default src */ |
| String src; |
| |
| SourceResolver resolver; |
| ServiceManager manager; |
| |
| // |
| // need two caches for Object and Object[] |
| // |
| |
| /** XPath expression cache for single attribute values. */ |
| private Map expressionCache; |
| |
| /** XPath expression cache for multiple attribute values. */ |
| private Map expressionValuesCache; |
| |
| |
| /** |
| * Takes care of (re-)loading and caching of sources. |
| */ |
| protected class DocumentHelper { |
| private boolean reloadable; |
| private boolean cacheable; |
| |
| /** Source location */ |
| private String uri; |
| |
| /** Source validity */ |
| private SourceValidity validity; |
| |
| /** Source content cached as DOM Document */ |
| private Document document; |
| |
| /** Remember who created us (and who's caching us) */ |
| private XMLFileModule instance; |
| |
| /** |
| * Creates a new <code>DocumentHelper</code> instance. |
| * |
| * @param reload a <code>boolean</code> value, whether this source should be reloaded if changed. |
| * @param cache a <code>boolean</code> value, whether this source should be kept in memory. |
| * @param src a <code>String</code> value containing the URI |
| */ |
| public DocumentHelper(boolean reload, boolean cache, String src, XMLFileModule instance) { |
| this.reloadable = reload; |
| this.cacheable = cache; |
| this.uri = src; |
| this.instance = instance; |
| // defer loading of the document |
| } |
| |
| /** |
| * Returns the Document belonging to the configured |
| * source. Transparently handles reloading and caching. |
| * |
| * @param manager a <code>ServiceManager</code> value |
| * @param resolver a <code>SourceResolver</code> value |
| * @return a <code>Document</code> value |
| * @exception Exception if an error occurs |
| */ |
| public synchronized Document getDocument(ServiceManager manager, SourceResolver resolver, Logger logger) |
| throws Exception { |
| Source src = null; |
| Document dom = null; |
| try { |
| if (this.document == null) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Document not cached... Loading uri " + this.uri); |
| } |
| src = resolver.resolveURI(this.uri); |
| this.validity = src.getValidity(); |
| this.document = SourceUtil.toDOM(src); |
| } else if (this.reloadable) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Document cached... checking validity of uri " + this.uri); |
| } |
| |
| int valid = this.validity == null? SourceValidity.INVALID: this.validity.isValid(); |
| if (valid != SourceValidity.VALID) { |
| // Get new source and validity |
| src = resolver.resolveURI(this.uri); |
| SourceValidity newValidity = src.getValidity(); |
| // If already invalid, or invalid after validities comparison, reload |
| if (valid == SourceValidity.INVALID || this.validity.isValid(newValidity) != SourceValidity.VALID) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Reloading document... uri " + this.uri); |
| } |
| this.validity = newValidity; |
| this.document = SourceUtil.toDOM(src); |
| |
| /* |
| * Clear the cache, otherwise reloads won't do much. |
| * |
| * FIXME (pf): caches should be held in the DocumentHelper |
| * instance itself, clearing global cache will |
| * clear everything for each configured document. |
| * (this is a quick fix, no time to do the whole) |
| */ |
| this.instance.flushCache(); |
| } |
| } |
| } |
| |
| dom = this.document; |
| } finally { |
| if (src != null) { |
| resolver.release(src); |
| } |
| |
| if (!this.cacheable) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("Not caching document cached... uri " + this.uri); |
| } |
| this.validity = null; |
| this.document = null; |
| } |
| } |
| |
| if (logger.isDebugEnabled()) { |
| logger.debug("Done with document... uri " + this.uri); |
| } |
| return dom; |
| } |
| } |
| |
| |
| /* (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>true|<b>false</b></reloadable> |
| * <cacheable><b>true</b>|false</cacheable> |
| * <file src="<i>src1</i>" reloadable="true|<b>false</b>" cacheable="<b>true</b>|false"/> |
| * <file src="<i>src2</i>" reloadable="true|<b>false</b>" cacheable="<b>true</b>|false"/> |
| * ... |
| * </...> |
| * |
| * Each <file/> element pre-loads an XML DOM for querying. Typically only one |
| * <file> is specified, and its <i>src</i> is used as a default if not |
| * overridden in the {@link #getContextObject(Configuration, Map)} |
| * |
| * @param config a <code>Configuration</code> value, as described above. |
| * @exception ConfigurationException if an error occurs |
| */ |
| public void configure(Configuration config) |
| throws ConfigurationException { |
| super.configure(config); |
| this.staticConfLocation = config.getLocation(); |
| this.reloadAll = config.getChild("reloadable").getValueAsBoolean(false); |
| |
| if (config.getChild("cachable", false) != null) { |
| throw new ConfigurationException("Bzzt! Wrong spelling at " + |
| config.getChild("cachable").getLocation() + |
| ": please use 'cacheable', not 'cachable'"); |
| } |
| this.cacheAll = config.getChild("cacheable").getValueAsBoolean(true); |
| |
| this.documents = Collections.synchronizedMap(new HashMap()); |
| Configuration[] files = config.getChildren("file"); |
| for (int i = 0; i < files.length; i++) { |
| boolean reload = files[i].getAttributeAsBoolean("reloadable", this.reloadAll); |
| boolean cache = files[i].getAttributeAsBoolean("cacheable", this.cacheAll); |
| this.src = files[i].getAttribute("src"); |
| // by assigning the source uri to this.src the last one will be the default |
| // OTOH caching / reload parameters can be specified in one central place |
| // if multiple file tags are used. |
| this.documents.put(files[i], new DocumentHelper(reload, cache, this.src, this)); |
| } |
| |
| // init caches |
| this.cacheExpressions = config.getChild("cache-expressions").getValueAsBoolean(true); |
| if (this.cacheExpressions) { |
| this.expressionCache = new ReferenceMap(AbstractReferenceMap.SOFT, AbstractReferenceMap.SOFT); |
| this.expressionValuesCache = new ReferenceMap(AbstractReferenceMap.SOFT, AbstractReferenceMap.SOFT); |
| } |
| } |
| |
| /** |
| * Dispose this component |
| */ |
| public void dispose() { |
| super.dispose(); |
| if (this.manager != null) { |
| this.manager.release(this.resolver); |
| this.resolver = null; |
| this.manager = null; |
| } |
| |
| this.documents = null; |
| this.expressionCache = null; |
| this.expressionValuesCache = null; |
| } |
| |
| |
| /** |
| * Retrieve document helper |
| */ |
| private DocumentHelper getDocumentHelper(Configuration modeConf) |
| throws ConfigurationException { |
| 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; |
| if (hasDynamicConf) { |
| src = fileConf.getAttribute("src"); |
| } |
| |
| if (src == null) { |
| throw new ConfigurationException( |
| "No source specified" |
| + (modeConf != null ? ", either dynamically in " + modeConf.getLocation() + ", or " : "") |
| + " statically in " |
| + staticConfLocation); |
| } |
| |
| if (!this.documents.containsKey(src)) { |
| boolean reload = this.reloadAll; |
| boolean cache = this.cacheAll; |
| if (hasDynamicConf) { |
| reload = fileConf.getAttributeAsBoolean("reloadable", reload); |
| cache = fileConf.getAttributeAsBoolean("cacheable", cache); |
| if (fileConf.getAttribute("cachable", null) != null) { |
| throw new ConfigurationException( |
| "Bzzt! Wrong spelling at " |
| + fileConf.getLocation() |
| + ": please use 'cacheable', not 'cachable'"); |
| } |
| } |
| |
| this.documents.put(src, new DocumentHelper(reload, cache, src, this)); |
| } |
| |
| return (DocumentHelper) this.documents.get(src); |
| } |
| |
| /** |
| * Get 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(Configuration)}. |
| * @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. |
| */ |
| protected Object getContextObject(Configuration modeConf, Map objectModel) |
| throws ConfigurationException { |
| DocumentHelper helper = getDocumentHelper(modeConf); |
| |
| try { |
| return helper.getDocument(this.manager, this.resolver, getLogger()); |
| } catch (Exception e) { |
| if (getLogger().isDebugEnabled()) { |
| getLogger().debug("Error using source " + src + "\n" + e.getMessage(), e); |
| } |
| throw new ConfigurationException("Error using source " + src, e); |
| } |
| } |
| |
| 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); |
| } |
| |
| private Object getAttribute(String name, Configuration modeConf, Map objectModel, boolean getValues) |
| throws ConfigurationException { |
| Object contextObj = getContextObject(modeConf, objectModel); |
| if (modeConf != null) { |
| name = modeConf.getChild("parameter").getValue(this.parameter != null ? this.parameter : name); |
| } |
| |
| Object result = null; |
| Map cache = null; |
| boolean hasBeenCached = false; |
| if (this.cacheExpressions) { |
| cache = getExpressionCache(getValues? this.expressionValuesCache: this.expressionCache, contextObj); |
| hasBeenCached = cache.containsKey(name); |
| if (hasBeenCached) { |
| result = cache.get(name); |
| } |
| } |
| |
| if (!hasBeenCached) { |
| if (getValues){ |
| result = JXPathHelper.getAttributeValues(name, modeConf, this.configuration, contextObj); |
| } else { |
| result = JXPathHelper.getAttribute(name, modeConf, this.configuration, contextObj); |
| } |
| if (this.cacheExpressions) { |
| cache.put(name, result); |
| if (this.getLogger().isDebugEnabled()) { |
| this.getLogger().debug("for " + name + " newly caching result " + result); |
| } |
| } else { |
| if (this.getLogger().isDebugEnabled()) { |
| this.getLogger().debug("for " + name + " result is " + result); |
| } |
| } |
| } else { |
| if (this.getLogger().isDebugEnabled()) { |
| this.getLogger().debug("for " + name + " using cached result " + result); |
| } |
| } |
| |
| return result; |
| } |
| |
| protected void flushCache() { |
| if (this.cacheExpressions) { |
| synchronized(this.expressionCache) { |
| this.expressionCache.clear(); |
| } |
| synchronized(this.expressionValuesCache) { |
| this.expressionValuesCache.clear(); |
| } |
| } |
| } |
| |
| private Map getExpressionCache(Map cache, Object key) { |
| synchronized (cache) { |
| Map map = (Map) cache.get(key); |
| if (map == null) { |
| map = Collections.synchronizedMap(new HashMap()); |
| cache.put(key, map); |
| } |
| return map; |
| } |
| } |
| } |