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.generation;
018:
019: import java.io.BufferedReader;
020: import java.io.File;
021: import java.io.IOException;
022: import java.io.InputStreamReader;
023: import java.util.HashMap;
024: import java.util.Map;
025:
026: import org.apache.avalon.framework.logger.Logger;
027: import org.apache.avalon.framework.parameters.Parameters;
028: import org.apache.avalon.framework.service.ServiceException;
029: import org.apache.avalon.framework.service.ServiceManager;
030: import org.apache.cocoon.ProcessingException;
031:
032: import org.apache.cocoon.components.source.SourceUtil;
033:
034: import org.apache.cocoon.environment.SourceResolver;
035:
036: import org.apache.cocoon.xml.dom.DOMStreamer;
037:
038: import org.apache.excalibur.source.Source;
039: import org.apache.excalibur.source.SourceNotFoundException;
040:
041: import org.apache.excalibur.xml.dom.DOMParser;
042: import org.apache.excalibur.xml.xpath.PrefixResolver;
043: import org.apache.excalibur.xml.xpath.XPathProcessor;
044:
045: import org.apache.regexp.RE;
046: import org.apache.regexp.RESyntaxException;
047:
048: import org.w3c.dom.Document;
049: import org.w3c.dom.NodeList;
050:
051: import org.xml.sax.SAXException;
052:
053: import org.xml.sax.helpers.AttributesImpl;
054:
055: /**
056: * @cocoon.sitemap.component.documentation
057: * Generates an XML directory listing performing XPath queries on XML files. It can be used both as a plain
058: * DirectoryGenerator or, by specifying a parameter <code>xpath</code>, it will perform an XPath query on every XML
059: * resource.
060: *
061: * @cocoon.sitemap.component.name xpathdirectory
062: * @cocoon.sitemap.component.label content
063: * @cocoon.sitemap.component.documentation.caching
064: * Uses the last modification date of the directory and the contained documents
065: * @cocoon.sitemap.component.logger sitemap.generator.xpathdirectory
066: *
067: *
068: * <p>
069: * Generates an XML directory listing performing XPath queries on XML files. It can be used both as a plain
070: * DirectoryGenerator or, by specifying a parameter <code>xpath</code>, it will perform an XPath query on every XML
071: * resource. A <code>nsmapping</code> parameter can be specified to point to a file containing lines to map prefixes
072: * to namespaces like this:
073: * </p>
074: *
075: * <p>
076: * prefix=namespace-uri<br/> prefix2=namespace-uri-2
077: * </p>
078: *
079: * <p>
080: * A parameter <code>nsmapping-reload</code> specifies if the prefix-2-namespace mapping file should be checked to be
081: * reloaded on each request to this generator if it was modified since the last time it was read.
082: * </p>
083: *
084: * <p>
085: * An additional parameter <code>xmlFiles</code> can be set in the sitemap setting the regular expression pattern for
086: * determining if a file should be handled as XML file or not. The default value for this param is
087: * <code>\.xml$</code>, so that it matches all files ending <code>.xml</code>.
088: * </p>
089: *
090: * <p></p>
091: * <br>Sample usage: <br><br>Sitemap:
092: * <pre>
093: * <map:match pattern="documents/**">
094: * <map:generate type="xpathdirectory" src="docs/{1}">
095: * <map:parameter name="xpath" value="/article/title|/article/abstract"/>
096: * <map:parameter name="nsmapping" value="mapping.properties"/>
097: * <map:parameter name="nsmapping-reload" value="false"/>
098: * <map:parameter name="xmlFiles" value="\.xml$"/>
099: * </map:generate>
100: * <map:serialize type="xml" />
101: * </map:match>
102: * </pre>
103: *
104: * <p>
105: * Request: <br>http://www.some.host/documents/test
106: * </p>
107: * Result:
108: * <pre>
109: * <dir:directory name="test" lastModified="1010400942000" date="1/7/02 11:55 AM" requested="true" xmlns:dir="http://apache.org/cocoon/directory/2.0">
110: * <dir:directory name="subdirectory" lastModified="1010400942000" date="1/7/02 11:55 AM"/>
111: * <dir:file name="test.xml" lastModified="1011011579000" date="1/14/02 1:32 PM">
112: * <dir:xpath query="/article/title">
113: * <title>This is a test document</title>
114: * <abstract>
115: * <para>Abstract of my test article</para>
116: * </abstract>
117: * </dir:xpath>
118: * </dir:file>
119: * <dir:file name="test.gif" lastModified="1011011579000" date="1/14/02 1:32 PM"/>
120: * </dir:directory>
121: * </pre>
122: *
123: * @author <a href="mailto:giacomo@apache.org">Giacomo Pati</a>
124: * @author <a href="mailto:gianugo@apache.org">Gianugo Rabellino</a>
125: * @author <a href="mailto:joerg@apache.org">J\u00F6rg Heinicke</a>
126: * @version CVS $Id: XPathDirectoryGenerator.java 433543 2006-08-22 06:22:54Z crossley $
127: */
128: public class XPathDirectoryGenerator extends DirectoryGenerator {
129: /** Local name for the element that contains the included XML snippet. */
130: protected static final String XPATH_NODE_NAME = "xpath";
131:
132: /** Attribute for the XPath query. */
133: protected static final String QUERY_ATTR_NAME = "query";
134:
135: /** All the mapping files lastmodified dates */
136: protected static final Map mappingFiles = new HashMap();
137:
138: /** The parser for the XML snippets to be included. */
139: protected DOMParser parser = null;
140:
141: /** The document that should be parsed and (partly) included. */
142: protected Document doc = null;
143:
144: /** The PrefixResolver responsable for processing current request (if any). */
145: protected PrefixResolver prefixResolver = null;
146:
147: /** The regular expression for the XML files pattern. */
148: protected RE xmlRE = null;
149:
150: /** The XPath. */
151: protected String xpath = null;
152:
153: /** The XPath processor. */
154: protected XPathProcessor processor = null;
155:
156: /**
157: * Disposable
158: */
159: public void dispose() {
160: if (this .manager != null) {
161: this .manager.release(this .processor);
162: this .manager.release(this .parser);
163: this .processor = null;
164: this .parser = null;
165: }
166:
167: super .dispose();
168: }
169:
170: /**
171: * Recycle resources
172: */
173: public void recycle() {
174: this .xpath = null;
175: this .doc = null;
176:
177: //this.parser = null;
178: //this.processor = null;
179: super .recycle();
180: }
181:
182: /**
183: * Serviceable
184: *
185: * @param manager the ComponentManager
186: *
187: * @throws ServiceException in case a component could not be found
188: */
189: public void service(ServiceManager manager) throws ServiceException {
190: super .service(manager);
191: this .processor = (XPathProcessor) manager
192: .lookup(XPathProcessor.ROLE);
193: this .parser = (DOMParser) manager.lookup(DOMParser.ROLE);
194: }
195:
196: /**
197: * Setup this sitemap component
198: *
199: * @param resolver the SourceResolver
200: * @param objectModel The environmental object model
201: * @param src the source attribute
202: * @param par the parameters
203: *
204: * @throws ProcessingException if processing failes
205: * @throws SAXException in case of XML related errors
206: * @throws IOException in case of file related errors
207: */
208: public void setup(SourceResolver resolver, Map objectModel,
209: String src, Parameters par) throws ProcessingException,
210: SAXException, IOException {
211: super .setup(resolver, objectModel, src, par);
212:
213: // See if an XPath was specified
214: this .xpath = par.getParameter("xpath", null);
215: this .cacheKeyParList.add(this .xpath);
216:
217: if (getLogger().isDebugEnabled()) {
218: getLogger().debug(
219: "Applying XPath: " + this .xpath + " to directory "
220: + this .source);
221: }
222:
223: final String mappings = par.getParameter("nsmapping", null);
224:
225: if (null != mappings) {
226: final boolean mapping_reload = par.getParameterAsBoolean(
227: "nsmapping-reload", false);
228: final Source mappingSource = resolver.resolveURI(mappings);
229: final String mappingKey = mappingSource.getURI();
230: final MappingInfo mappingInfo = (MappingInfo) XPathDirectoryGenerator.mappingFiles
231: .get(mappingKey);
232:
233: if ((null == mappingInfo)
234: || (mappingInfo.reload == false)
235: || (mappingInfo.mappingSource.getLastModified() < mappingSource
236: .getLastModified())) {
237: this .prefixResolver = new MappingInfo(getLogger()
238: .getChildLogger("prefix-resolver"),
239: mappingSource, mapping_reload);
240: XPathDirectoryGenerator.mappingFiles.put(mappingKey,
241: this .prefixResolver);
242: } else {
243: this .prefixResolver = mappingInfo;
244: }
245: }
246:
247: String xmlFilesPattern = null;
248:
249: try {
250: xmlFilesPattern = par.getParameter("xmlFiles", "\\.xml$");
251: this .cacheKeyParList.add(xmlFilesPattern);
252: this .xmlRE = new RE(xmlFilesPattern);
253:
254: if (getLogger().isDebugEnabled()) {
255: getLogger().debug(
256: "pattern for XML files: " + xmlFilesPattern);
257: }
258: } catch (RESyntaxException rese) {
259: throw new ProcessingException(
260: "Syntax error in regexp pattern '"
261: + xmlFilesPattern + "'", rese);
262: }
263: }
264:
265: /**
266: * Determines if a given File shall be handled as XML.
267: *
268: * @param path the File to check
269: *
270: * @return true if the given File shall handled as XML, false otherwise.
271: */
272: protected boolean isXML(File path) {
273: return this .xmlRE.match(path.getName());
274: }
275:
276: /**
277: * Performs an XPath query on the file.
278: *
279: * @param xmlFile the File the XPath is performed on.
280: *
281: * @throws SAXException if something goes wrong while adding the XML snippet.
282: */
283: protected void performXPathQuery(File xmlFile) throws SAXException {
284: this .doc = null;
285:
286: Source source = null;
287:
288: try {
289: source = resolver.resolveURI(xmlFile.toURL()
290: .toExternalForm());
291: this .doc = this .parser.parseDocument(SourceUtil
292: .getInputSource(source));
293: } catch (SAXException e) {
294: getLogger().error(
295: "Warning:" + xmlFile.getName()
296: + " is not a valid XML file. Ignoring.", e);
297: } catch (ProcessingException e) {
298: getLogger().error(
299: "Warning: Problem while reading the file "
300: + xmlFile.getName() + ". Ignoring.", e);
301: } catch (IOException e) {
302: getLogger().error(
303: "Warning: Problem while reading the file "
304: + xmlFile.getName() + ". Ignoring.", e);
305: } finally {
306: resolver.release(source);
307: }
308:
309: if (doc != null) {
310: NodeList nl = (null == this .prefixResolver) ? this .processor
311: .selectNodeList(this .doc.getDocumentElement(),
312: this .xpath)
313: : this .processor.selectNodeList(this .doc
314: .getDocumentElement(), this .xpath,
315: this .prefixResolver);
316: AttributesImpl attributes = new AttributesImpl();
317: attributes.addAttribute("", QUERY_ATTR_NAME,
318: QUERY_ATTR_NAME, "CDATA", xpath);
319: super .contentHandler.startElement(URI, XPATH_NODE_NAME,
320: PREFIX + ":" + XPATH_NODE_NAME, attributes);
321:
322: DOMStreamer ds = new DOMStreamer(super .xmlConsumer);
323:
324: for (int i = 0; i < nl.getLength(); i++) {
325: ds.stream(nl.item(i));
326: }
327:
328: super .contentHandler.endElement(URI, XPATH_NODE_NAME,
329: PREFIX + ":" + XPATH_NODE_NAME);
330: }
331: }
332:
333: /**
334: * Extends the startNode() method of the DirectoryGenerator by starting a possible XPath query on a file.
335: *
336: * @param nodeName the node currently processing
337: * @param path the file path
338: *
339: * @throws SAXException in case of errors
340: */
341: protected void startNode(String nodeName, File path)
342: throws SAXException {
343: super .startNode(nodeName, path);
344:
345: if ((this .xpath != null) && path.isFile() && this .isXML(path)) {
346: performXPathQuery(path);
347: }
348: }
349:
350: /**
351: * The MappingInfo class to resolve namespace prefixes to their namespace URI
352: *
353: * @author <a href="mailto:giacomo(at)apache.org">Giacomo Pati</a>
354: * @version CVS $Id: XPathDirectoryGenerator.java 433543 2006-08-22 06:22:54Z crossley $
355: */
356: private static class MappingInfo implements PrefixResolver {
357: /** The Source of the mapping file */
358: public final Source mappingSource;
359:
360: /** Whether to reload if mapping file has changed */
361: public final boolean reload;
362:
363: /** Our Logger */
364: private final Logger logger;
365:
366: /** Map of prefixes to namespaces */
367: private final Map prefixMap;
368:
369: /**
370: * Creates a new MappingInfo object.
371: *
372: * @param logger DOCUMENT ME!
373: * @param mappingSource The Source of the mapping file
374: * @param reload Whether to reload if mapping file has changed
375: *
376: * @throws SourceNotFoundException In case the mentioned source is not there
377: * @throws IOException in case the source could not be read
378: */
379: public MappingInfo(final Logger logger,
380: final Source mappingSource, final boolean reload)
381: throws SourceNotFoundException, IOException {
382: this .logger = logger;
383: this .mappingSource = mappingSource;
384: this .reload = reload;
385: prefixMap = new HashMap();
386: InputStreamReader input = null;
387: BufferedReader br = null;
388:
389: try {
390: input = new InputStreamReader(mappingSource
391: .getInputStream());
392: br = new BufferedReader(input);
393:
394: for (String line = br.readLine(); line != null; line = br
395: .readLine()) {
396: final int i = line.indexOf('=');
397:
398: if (i > 0) {
399: final String prefix = line.substring(0, i);
400: final String namespace = line.substring(i + 1);
401: prefixMap.put(prefix, namespace);
402: logger.debug("added mapping: '" + prefix
403: + "'='" + namespace + "'");
404: }
405: }
406: } finally {
407: if (br != null) {
408: br.close();
409: }
410: if (input != null) {
411: input.close();
412: }
413: }
414: }
415:
416: /* (non-Javadoc)
417: * @see org.apache.excalibur.xml.xpath.PrefixResolver#prefixToNamespace(java.lang.String)
418: */
419: public String prefixToNamespace(String prefix) {
420: final String namespace = (String) this .prefixMap
421: .get(prefix);
422:
423: if (logger.isDebugEnabled()) {
424: logger.debug("have to resolve prefix='" + prefix
425: + ", found namespace='" + namespace + "'");
426: }
427:
428: return namespace;
429: }
430: }
431: }
|