001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.components.modules.input;
018:
019: import org.apache.avalon.framework.component.Component;
020: import org.apache.avalon.framework.component.ComponentException;
021: import org.apache.avalon.framework.component.ComponentManager;
022: import org.apache.avalon.framework.component.Composable;
023: import org.apache.avalon.framework.configuration.Configuration;
024: import org.apache.avalon.framework.configuration.ConfigurationException;
025: import org.apache.avalon.framework.logger.Logger;
026: import org.apache.avalon.framework.thread.ThreadSafe;
027: import org.apache.cocoon.components.source.SourceUtil;
028: import org.apache.commons.collections.map.AbstractReferenceMap;
029: import org.apache.commons.collections.map.ReferenceMap;
030: import org.apache.excalibur.source.Source;
031: import org.apache.excalibur.source.SourceResolver;
032: import org.apache.excalibur.source.SourceValidity;
033: import org.w3c.dom.Document;
034:
035: import java.util.Collections;
036: import java.util.HashMap;
037: import java.util.Map;
038:
039: /**
040:
041: <grammar>
042: <define name="input.module.config.contents" combine="choice">
043: <optional><element name="reloadable"><data type="boolean"/></element></optional>
044: <optional><element name="cacheable"><data type="boolean"/></element></optional>
045: <optional>
046: <ref name="org.apache.cocoon.components.modules.input.XMLFileModule:file">
047: </optional>
048: </define>
049:
050: <define name="input.module.runtime.contents" combine="choice">
051: <optional>
052: <ref name="org.apache.cocoon.components.modules.input.XMLFileModule:file">
053: </optional>
054: </define>
055:
056: <define name="org.apache.cocoon.components.modules.input.XMLFileModule:file">
057: <element name="file">
058: <attribute name="src"><data type="anyURI"/></attribute>
059: <optional><attribute name="reloadable"><data type="boolean"/></attribute></optional>
060: <optional><attribute name="cacheable"><data type="boolean"/></attribute></optional>
061: </element>
062: </define>
063: </grammar>
064:
065: * This module provides an Input Module interface to any XML document, by using
066: * XPath expressions as attribute keys.
067: * The XML can be obtained from any Cocoon <code>Source</code> (e.g.,
068: * <code>cocoon:/...</code>, <code>context://..</code>, and regular URLs).
069: * Sources can be held in memory for better performance and reloaded if
070: * changed.
071: *
072: * <p>Caching and reloading can be turned on / off (default: caching on,
073: * reloading off) through <code><reloadable>false</reloadable></code>
074: * and <code><cacheable>false</cacheable></code>. The file
075: * (source) to use is specified through <code><file
076: * src="protocol:path/to/file.xml" reloadable="true"
077: * cacheable="true"/></code> optionally overriding defaults for
078: * caching and/or reloading.</p>
079: *
080: * <p>In addition, xpath expressions are cached for higher performance.
081: * Thus, if an expression has been evaluated for a file, the result
082: * is cached and will be reused, the expression is not evaluated
083: * a second time. This can be turned off using the <code>cache-expressions</code>
084: * configuration option.</p>
085: *
086: * @author <a href="mailto:jefft@apache.org">Jeff Turner</a>
087: * @author <a href="mailto:haul@apache.org">Christian Haul</a>
088: * @version $Id: XMLFileModule.java 540711 2007-05-22 19:36:07Z cziegeler $
089: */
090: public class XMLFileModule extends AbstractJXPathModule implements
091: Composable, ThreadSafe {
092:
093: /** Static (cocoon.xconf) configuration location, for error reporting */
094: String staticConfLocation;
095:
096: /** Cached documents */
097: Map documents;
098:
099: /** Default value for reloadability of sources. Defaults to false. */
100: boolean reloadAll;
101:
102: /** Default value for cacheability of sources. Defaults to true. */
103: boolean cacheAll;
104:
105: /** Default value for cacheability of xpath expressions. Defaults to true. */
106: boolean cacheExpressions;
107:
108: /** Default src */
109: String src;
110:
111: SourceResolver resolver;
112: ComponentManager manager;
113:
114: //
115: // need two caches for Object and Object[]
116: //
117:
118: /** XPath expression cache for single attribute values. */
119: private Map expressionCache;
120:
121: /** XPath expression cache for multiple attribute values. */
122: private Map expressionValuesCache;
123:
124: /**
125: * Takes care of (re-)loading and caching of sources.
126: */
127: protected static class DocumentHelper {
128: private boolean reloadable;
129: private boolean cacheable;
130:
131: /** Source location */
132: private String uri;
133:
134: /** Source validity */
135: private SourceValidity validity;
136:
137: /** Source content cached as DOM Document */
138: private Document document;
139:
140: /** Remember who created us (and who's caching us) */
141: private XMLFileModule instance;
142:
143: /**
144: * Creates a new <code>DocumentHelper</code> instance.
145: *
146: * @param reload a <code>boolean</code> value, whether this source should be reloaded if changed.
147: * @param cache a <code>boolean</code> value, whether this source should be kept in memory.
148: * @param src a <code>String</code> value containing the URI
149: */
150: public DocumentHelper(boolean reload, boolean cache,
151: String src, XMLFileModule instance) {
152: this .reloadable = reload;
153: this .cacheable = cache;
154: this .uri = src;
155: this .instance = instance;
156: // defer loading of the document
157: }
158:
159: /**
160: * Returns the Document belonging to the configured
161: * source. Transparently handles reloading and caching.
162: *
163: * @param manager a <code>ComponentManager</code> value
164: * @param resolver a <code>SourceResolver</code> value
165: * @return a <code>Document</code> value
166: * @exception Exception if an error occurs
167: */
168: public synchronized Document getDocument(
169: ComponentManager manager, SourceResolver resolver,
170: Logger logger) throws Exception {
171: Source src = null;
172: Document dom = null;
173: try {
174: if (this .document == null) {
175: if (logger.isDebugEnabled()) {
176: logger
177: .debug("Document not cached... Loading uri "
178: + this .uri);
179: }
180: src = resolver.resolveURI(this .uri);
181: this .validity = src.getValidity();
182: this .document = SourceUtil.toDOM(src);
183: } else if (this .reloadable) {
184: if (logger.isDebugEnabled()) {
185: logger
186: .debug("Document cached... checking validity of uri "
187: + this .uri);
188: }
189:
190: int valid = this .validity == null ? SourceValidity.INVALID
191: : this .validity.isValid();
192: if (valid != SourceValidity.VALID) {
193: // Get new source and validity
194: src = resolver.resolveURI(this .uri);
195: SourceValidity newValidity = src.getValidity();
196: // If already invalid, or invalid after validities comparison, reload
197: if (valid == SourceValidity.INVALID
198: || this .validity.isValid(newValidity) != SourceValidity.VALID) {
199: if (logger.isDebugEnabled()) {
200: logger
201: .debug("Reloading document... uri "
202: + this .uri);
203: }
204: this .validity = newValidity;
205: this .document = SourceUtil.toDOM(src);
206:
207: /*
208: * Clear the cache, otherwise reloads won't do much.
209: *
210: * FIXME (pf): caches should be held in the DocumentHelper
211: * instance itself, clearing global cache will
212: * clear everything for each configured document.
213: * (this is a quick fix, no time to do the whole)
214: */
215: this .instance.flushCache();
216: }
217: }
218: }
219: dom = this .document;
220: } finally {
221: if (src != null) {
222: resolver.release(src);
223: }
224: if (!this .cacheable) {
225: if (logger.isDebugEnabled()) {
226: logger
227: .debug("Not caching document cached... uri "
228: + this .uri);
229: }
230: this .validity = null;
231: this .document = null;
232: }
233: }
234: if (logger.isDebugEnabled()) {
235: logger.debug("Done with document... uri " + this .uri);
236: }
237: return dom;
238: }
239: }
240:
241: /**
242: * Set the current <code>ComponentManager</code> instance used by this
243: * <code>Composable</code>.
244: */
245: public void compose(ComponentManager manager)
246: throws ComponentException {
247: this .manager = manager;
248: this .resolver = (SourceResolver) manager
249: .lookup(SourceResolver.ROLE);
250: }
251:
252: /**
253: * Static (cocoon.xconf) configuration.
254: * Configuration is expected to be of the form:
255: * <...>
256: * <reloadable>true|<b>false</b></reloadable>
257: * <cacheable><b>true</b>|false</cacheable>
258: * <file src="<i>src1</i>" reloadable="true|<b>false</b>" cacheable="<b>true</b>|false"/>
259: * <file src="<i>src2</i>" reloadable="true|<b>false</b>" cacheable="<b>true</b>|false"/>
260: * ...
261: * </...>
262: *
263: * Each <file/> element pre-loads an XML DOM for querying. Typically only one
264: * <file> is specified, and its <i>src</i> is used as a default if not
265: * overridden in the {@link #getContextObject(Configuration, Map)}
266: *
267: * @param config a <code>Configuration</code> value, as described above.
268: * @exception ConfigurationException if an error occurs
269: */
270: public void configure(Configuration config)
271: throws ConfigurationException {
272: super .configure(config);
273: this .staticConfLocation = config.getLocation();
274: this .reloadAll = config.getChild("reloadable")
275: .getValueAsBoolean(false);
276:
277: if (config.getChild("cachable", false) != null) {
278: throw new ConfigurationException("Bzzt! Wrong spelling at "
279: + config.getChild("cachable").getLocation()
280: + ": please use 'cacheable', not 'cachable'");
281: }
282: this .cacheAll = config.getChild("cacheable").getValueAsBoolean(
283: true);
284:
285: this .documents = Collections
286: .synchronizedMap(new ReferenceMap());
287: Configuration[] files = config.getChildren("file");
288: for (int i = 0; i < files.length; i++) {
289: boolean reload = files[i].getAttributeAsBoolean(
290: "reloadable", this .reloadAll);
291: boolean cache = files[i].getAttributeAsBoolean("cacheable",
292: this .cacheAll);
293: this .src = files[i].getAttribute("src");
294: // by assigning the source uri to this.src the last one will be the default
295: // OTOH caching / reload parameters can be specified in one central place
296: // if multiple file tags are used.
297: this .documents.put(this .src, new DocumentHelper(reload,
298: cache, this .src, this ));
299: }
300:
301: // init caches
302: this .cacheExpressions = config.getChild("cache-expressions")
303: .getValueAsBoolean(true);
304: if (this .cacheExpressions) {
305: this .expressionCache = new ReferenceMap(
306: AbstractReferenceMap.SOFT,
307: AbstractReferenceMap.SOFT);
308: this .expressionValuesCache = new ReferenceMap(
309: AbstractReferenceMap.SOFT,
310: AbstractReferenceMap.SOFT);
311: }
312: }
313:
314: /**
315: * Dispose this component
316: */
317: public void dispose() {
318: super .dispose();
319: if (this .manager != null) {
320: this .manager.release((Component) this .resolver);
321: this .resolver = null;
322: this .manager = null;
323: }
324:
325: this .documents = null;
326: this .expressionCache = null;
327: this .expressionValuesCache = null;
328: }
329:
330: /**
331: * Retrieve document helper
332: */
333: private DocumentHelper getDocumentHelper(Configuration modeConf)
334: throws ConfigurationException {
335: boolean hasDynamicConf = false; // whether we have a <file src="..."> dynamic configuration
336: Configuration fileConf = null; // the nested <file>, if any
337:
338: if (modeConf != null && modeConf.getChildren().length > 0) {
339: fileConf = modeConf.getChild("file", false);
340: if (fileConf == null) {
341: if (this .getLogger().isDebugEnabled()) {
342: this .getLogger().debug(
343: "Missing 'file' child element at "
344: + modeConf.getLocation());
345: }
346: } else {
347: hasDynamicConf = true;
348: }
349: }
350:
351: String src = this .src;
352: if (hasDynamicConf) {
353: src = fileConf.getAttribute("src");
354: }
355:
356: if (src == null) {
357: throw new ConfigurationException("No source specified"
358: + (modeConf != null ? ", either dynamically in "
359: + modeConf.getLocation() + ", or " : "")
360: + " statically in " + this .staticConfLocation);
361: }
362: if (!this .documents.containsKey(src)) {
363: boolean reload = this .reloadAll;
364: boolean cache = this .cacheAll;
365: if (hasDynamicConf) {
366: reload = fileConf.getAttributeAsBoolean("reloadable",
367: reload);
368: cache = fileConf.getAttributeAsBoolean("cacheable",
369: cache);
370: if (fileConf.getAttribute("cachable", null) != null) {
371: throw new ConfigurationException(
372: "Bzzt! Wrong spelling at "
373: + fileConf.getLocation()
374: + ": please use 'cacheable', not 'cachable'");
375: }
376: }
377: this .documents.put(src, new DocumentHelper(reload, cache,
378: src, this ));
379: }
380: return (DocumentHelper) this .documents.get(src);
381: }
382:
383: /**
384: * Get the DOM object that JXPath will operate on when evaluating
385: * attributes. This DOM is loaded from a Source, specified in the
386: * modeConf, or (if modeConf is null) from the
387: * {@link #configure(Configuration)}.
388: * @param modeConf The dynamic configuration for the current operation. May
389: * be <code>null</code>, in which case static (cocoon.xconf) configuration
390: * is used. Configuration is expected to have a <file> child node, and
391: * be of the form:
392: * <...>
393: * <file src="..." reloadable="true|false"/>
394: * </...>
395: * @param objectModel Object Model for the current module operation.
396: */
397: protected Object getContextObject(Configuration modeConf,
398: Map objectModel) throws ConfigurationException {
399: DocumentHelper helper = this .getDocumentHelper(modeConf);
400:
401: try {
402: return helper.getDocument(this .manager, this .resolver, this
403: .getLogger());
404: } catch (Exception e) {
405: if (this .getLogger().isDebugEnabled()) {
406: this .getLogger().debug(
407: "Error using source " + this .src + "\n"
408: + e.getMessage(), e);
409: }
410: throw new ConfigurationException("Error using source "
411: + this .src, e);
412: }
413: }
414:
415: public Object getAttribute(String name, Configuration modeConf,
416: Map objectModel) throws ConfigurationException {
417: return this .getAttribute(name, modeConf, objectModel, false);
418: }
419:
420: public Object[] getAttributeValues(String name,
421: Configuration modeConf, Map objectModel)
422: throws ConfigurationException {
423: Object result = this .getAttribute(name, modeConf, objectModel,
424: true);
425: return (result != null ? (Object[]) result : null);
426: }
427:
428: private Object getAttribute(String name, Configuration modeConf,
429: Map objectModel, boolean getValues)
430: throws ConfigurationException {
431: Object contextObj = this
432: .getContextObject(modeConf, objectModel);
433: if (modeConf != null) {
434: name = modeConf.getChild("parameter").getValue(
435: this .parameter != null ? this .parameter : name);
436: }
437:
438: Object result = null;
439: Map cache = null;
440: boolean hasBeenCached = false;
441: if (this .cacheExpressions) {
442: cache = this .getExpressionCache(
443: getValues ? this .expressionValuesCache
444: : this .expressionCache, contextObj);
445: hasBeenCached = cache.containsKey(name);
446: if (hasBeenCached) {
447: result = cache.get(name);
448: }
449: }
450:
451: if (!hasBeenCached) {
452: if (getValues) {
453: result = JXPathHelper.getAttributeValues(name,
454: modeConf, this .configuration, contextObj);
455: } else {
456: result = JXPathHelper.getAttribute(name, modeConf,
457: this .configuration, contextObj);
458: }
459: if (this .cacheExpressions) {
460: cache.put(name, result);
461: if (this .getLogger().isDebugEnabled()) {
462: this .getLogger().debug(
463: "for " + name + " newly caching result "
464: + result);
465: }
466: } else {
467: if (this .getLogger().isDebugEnabled()) {
468: this .getLogger().debug(
469: "for " + name + " result is " + result);
470: }
471: }
472: } else {
473: if (this .getLogger().isDebugEnabled()) {
474: this .getLogger().debug(
475: "for " + name + " using cached result "
476: + result);
477: }
478: }
479:
480: return result;
481: }
482:
483: protected void flushCache() {
484: if (this .cacheExpressions) {
485: synchronized (this .expressionCache) {
486: this .expressionCache.clear();
487: }
488: synchronized (this .expressionValuesCache) {
489: this .expressionValuesCache.clear();
490: }
491: }
492: }
493:
494: private Map getExpressionCache(Map cache, Object key) {
495: synchronized (cache) {
496: Map map = (Map) cache.get(key);
497: if (map == null) {
498: map = Collections.synchronizedMap(new HashMap());
499: cache.put(key, map);
500: }
501: return map;
502: }
503: }
504: }
|