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.source.impl;
018:
019: import java.io.InputStream;
020: import java.io.IOException;
021: import java.io.OutputStream;
022: import java.io.ByteArrayOutputStream;
023: import java.io.ByteArrayInputStream;
024: import java.net.MalformedURLException;
025: import java.util.Map;
026:
027: import org.apache.avalon.framework.configuration.ConfigurationException;
028: import org.apache.avalon.framework.logger.Logger;
029: import org.apache.avalon.framework.service.ServiceException;
030: import org.apache.avalon.framework.service.ServiceManager;
031: import org.apache.avalon.framework.service.ServiceSelector;
032:
033: import org.apache.excalibur.source.ModifiableSource;
034: import org.apache.excalibur.source.SourceException;
035: import org.apache.excalibur.source.impl.AbstractSource;
036: import org.apache.excalibur.xml.sax.SAXParser;
037: import org.apache.excalibur.xml.sax.XMLizable;
038:
039: import org.apache.cocoon.components.modules.input.InputModule;
040: import org.apache.cocoon.components.modules.output.OutputModule;
041: import org.apache.cocoon.serialization.Serializer;
042: import org.apache.cocoon.util.jxpath.DOMFactory;
043: import org.apache.cocoon.xml.dom.DOMBuilder;
044: import org.apache.cocoon.xml.dom.DOMStreamer;
045:
046: import org.apache.commons.jxpath.JXPathContext;
047:
048: import org.w3c.dom.Document;
049: import org.w3c.dom.Node;
050:
051: import org.xml.sax.ContentHandler;
052: import org.xml.sax.InputSource;
053: import org.xml.sax.SAXException;
054:
055: /**
056: * A <code>ModifiableSource</code> that takes its content from a
057: * module.
058: * <p>The URI syntax is
059: * "xmodule:[<input-module>|<output-module>]:attribute-name[#XPath]",
060: * where :
061: * <ul>
062: * <li>an input-module name is used for finding an input-module for reading data from</li>,
063: * <li>an output-module name is used for finding an output-module for writing data to</li>,
064: * <li>"attribute-name" is the name of the attribute found in the module</li>,
065: * <li>"XPath" is an XPath that is aplied on the object in the
066: * attribute, by using JXPath.</li>
067: * </ul>
068: * </p>
069: *
070: * @author <a href="mailto:danielf@nada.kth.se">Daniel Fagerstom</a>
071: */
072: public class XModuleSource extends AbstractSource implements
073: ModifiableSource, XMLizable, DOMBuilder.Listener {
074:
075: private final static String SCHEME = "xmodule";
076: private String attributeType;
077: private String attributeName;
078: private String xPath;
079: protected ServiceManager manager;
080: private Map objectModel;
081: private Logger logger;
082: // TODO: make this actually configurable
083: private String configuredSerializerName = "xml";
084:
085: /**
086: * Create a xmodule source from a 'xmodule:' uri and a the object model.
087: * <p>The uri is of the form "xmodule:/attribute-type/attribute-name/xpath</p>
088: */
089: public XModuleSource(Map objectModel, String uri,
090: ServiceManager manager, Logger logger)
091: throws MalformedURLException {
092:
093: this .objectModel = objectModel;
094: this .manager = manager;
095: this .logger = logger;
096:
097: setSystemId(uri);
098:
099: // Scheme
100: int start = 0;
101: int end = uri.indexOf(':');
102: if (end == -1)
103: throw new MalformedURLException(
104: "Malformed uri for xmodule source (cannot find scheme) : "
105: + uri);
106:
107: String scheme = uri.substring(start, end);
108: if (!SCHEME.equals(scheme))
109: throw new MalformedURLException(
110: "Malformed uri for a xmodule source : " + uri);
111:
112: setScheme(scheme);
113:
114: // Attribute type
115: start = end + 1;
116: end = uri.indexOf(':', start);
117: if (end == -1) {
118: throw new MalformedURLException(
119: "Malformed uri for xmodule source (cannot find attribute type) : "
120: + uri);
121: }
122: this .attributeType = uri.substring(start, end);
123:
124: // Attribute name
125: start = end + 1;
126: end = uri.indexOf('#', start);
127:
128: if (end == -1)
129: end = uri.length();
130:
131: if (end == start)
132: throw new MalformedURLException(
133: "Malformed uri for xmodule source (cannot find attribute name) : "
134: + uri);
135:
136: this .attributeName = uri.substring(start, end);
137:
138: // xpath
139: start = end + 1;
140: this .xPath = start < uri.length() ? uri.substring(start) : "";
141: }
142:
143: /**
144: * Implement this method to obtain SAX events.
145: *
146: */
147:
148: public void toSAX(ContentHandler handler) throws SAXException {
149:
150: Object obj = getInputAttribute(this .attributeType,
151: this .attributeName);
152: if (obj == null)
153: throw new SAXException(" The attribute: "
154: + this .attributeName + " is empty");
155:
156: if (!(this .xPath.length() == 0 || this .xPath.equals("/"))) {
157: JXPathContext context = JXPathContext.newContext(obj);
158:
159: obj = context.getPointer(this .xPath).getNode();
160:
161: if (obj == null)
162: throw new SAXException("the xpath: " + this .xPath
163: + " applied on the attribute: "
164: + this .attributeName + " returns null");
165: }
166:
167: if (obj instanceof Document) {
168: DOMStreamer domStreamer = new DOMStreamer(handler);
169: domStreamer.stream((Document) obj);
170: } else if (obj instanceof Node) {
171: DOMStreamer domStreamer = new DOMStreamer(handler);
172: handler.startDocument();
173: domStreamer.stream((Node) obj);
174: handler.endDocument();
175: } else if (obj instanceof XMLizable) {
176: ((XMLizable) obj).toSAX(handler);
177: } else {
178: throw new SAXException("The object type: " + obj.getClass()
179: + " could not be serialized to XML: " + obj);
180: }
181: }
182:
183: /**
184: * Return an <code>InputStream</code> object to read from the source.
185: *
186: * @throws IOException if I/O error occured.
187: */
188: public InputStream getInputStream() throws IOException,
189: SourceException {
190: if (this .logger.isDebugEnabled()) {
191: this .logger.debug("Getting InputStream for " + getURI());
192: }
193:
194: // Serialize the SAX events to the XMLSerializer
195: ByteArrayInputStream inputStream = null;
196:
197: ServiceSelector selector = null;
198: Serializer serializer = null;
199: try {
200: selector = (ServiceSelector) this .manager
201: .lookup(Serializer.ROLE + "Selector");
202: serializer = (Serializer) selector
203: .select(this .configuredSerializerName);
204:
205: ByteArrayOutputStream outputStream = new ByteArrayOutputStream(
206: 2048);
207: serializer.setOutputStream(outputStream);
208: toSAX(serializer);
209: inputStream = new ByteArrayInputStream(outputStream
210: .toByteArray());
211: } catch (SAXException e) {
212: throw new SourceException(
213: "Serializing SAX to a ByteArray failed!", e);
214: } catch (ServiceException e) {
215: throw new SourceException("Retrieving serializer failed.",
216: e);
217: } finally {
218: if (selector != null) {
219: selector.release(serializer);
220: this .manager.release(selector);
221: }
222: }
223: return inputStream;
224: }
225:
226: /**
227: * Does this source actually exist ?
228: *
229: * @return true if the resource exists.
230: *
231: */
232: public boolean exists() {
233: boolean exists = false;
234: try {
235: exists = getInputAttribute(this .attributeType,
236: this .attributeName) != null;
237: } catch (SAXException e) {
238: exists = false;
239: }
240: return exists;
241: }
242:
243: /**
244: * Get an <code>InputStream</code> where raw bytes can be written to.
245: * The signification of these bytes is implementation-dependent and
246: * is not restricted to a serialized XML document.
247: *
248: * @return a stream to write to
249: */
250: public OutputStream getOutputStream() throws IOException {
251: return new DOMOutputStream();
252: }
253:
254: /**
255: * Delete the source
256: */
257: public void delete() throws SourceException {
258: if (!(this .xPath.length() == 0 || this .xPath.equals("/"))) {
259: Object value;
260: try {
261: value = getInputAttribute(this .attributeType,
262: this .attributeName);
263: } catch (SAXException e) {
264: throw new SourceException("delete: ", e);
265: }
266: if (value == null)
267: throw new SourceException(" The attribute: "
268: + this .attributeName + " is empty");
269:
270: JXPathContext context = JXPathContext.newContext(value);
271: context.removeAll(this .xPath);
272: } else {
273: try {
274: setOutputAttribute(this .attributeType,
275: this .attributeName, null);
276: } catch (SAXException e) {
277: throw new SourceException("delete: ", e);
278: }
279: }
280: }
281:
282: /**
283: * FIXME
284: * delete is an operator in java script, this method is for
285: * testing puposes in java script only
286: */
287: public void deleteTest() throws SourceException {
288: delete();
289: }
290:
291: /**
292: * Can the data sent to an <code>OutputStream</code> returned by
293: * {@link #getOutputStream()} be cancelled ?
294: *
295: * @return true if the stream can be cancelled
296: */
297: public boolean canCancel(OutputStream stream) {
298: return false;
299: }
300:
301: /**
302: * Cancel the data sent to an <code>OutputStream</code> returned by
303: * {@link #getOutputStream()}.
304: * <p>
305: * After cancel, the stream should no more be used.
306: */
307: public void cancel(OutputStream stream) throws IOException {
308: }
309:
310: /**
311: * Get a <code>ContentHandler</code> where an XML document can
312: * be written using SAX events.
313: * <p>
314: * Care should be taken that the returned handler can actually
315: * be a {@link org.apache.cocoon.xml.XMLConsumer} supporting also
316: * lexical events such as comments.
317: *
318: * @return a handler for SAX events
319: */
320: public ContentHandler getContentHandler() {
321: return new DOMBuilder(this );
322: }
323:
324: public void notify(Document insertDoc) throws SAXException {
325:
326: // handle xpaths, we are only handling inserts, i.e. if there is no
327: // attribute of the given name and type the operation will fail
328: if (!(this .xPath.length() == 0 || this .xPath.equals("/"))) {
329:
330: Object value = getInputAttribute(this .attributeType,
331: this .attributeName);
332: if (value == null)
333: throw new SAXException(" The attribute: "
334: + this .attributeName + " is empty");
335:
336: JXPathContext context = JXPathContext.newContext(value);
337:
338: if (value instanceof Document) {
339: // If the attribute contains a dom document we
340: // create the elements in the given xpath if
341: // necesary, import the input document and put it
342: // in the place described by the xpath.
343: Document doc = (Document) value;
344:
345: Node importedNode = doc.importNode(insertDoc
346: .getDocumentElement(), true);
347:
348: context.setLenient(true);
349: context.setFactory(new DOMFactory());
350: context.createPathAndSetValue(this .xPath, importedNode);
351: } else {
352: // Otherwise just try to put a the input document in
353: // the place pointed to by the xpath
354: context.setValue(this .xPath, insertDoc);
355: }
356:
357: } else {
358: setOutputAttribute(this .attributeType, this .attributeName,
359: insertDoc);
360: }
361: }
362:
363: private class DOMOutputStream extends ByteArrayOutputStream {
364: public void close() throws IOException {
365: SAXParser parser = null;
366: try {
367: parser = (SAXParser) XModuleSource.this .manager
368: .lookup(SAXParser.ROLE);
369:
370: parser.parse(new InputSource(new ByteArrayInputStream(
371: super .toByteArray())), XModuleSource.this
372: .getContentHandler());
373: } catch (Exception e) {
374: throw new IOException("Exception during processing of "
375: + XModuleSource.this .getURI() + e.getMessage());
376: } finally {
377: if (parser != null)
378: XModuleSource.this .manager.release(parser);
379: }
380: super .close();
381: }
382: }
383:
384: private Object getInputAttribute(String inputModuleName,
385: String attributeName) throws SAXException {
386: Object obj;
387: ServiceSelector selector = null;
388: InputModule inputModule = null;
389: try {
390: selector = (ServiceSelector) this .manager
391: .lookup(InputModule.ROLE + "Selector");
392: inputModule = (InputModule) selector
393: .select(inputModuleName);
394: obj = inputModule.getAttribute(attributeName, null,
395: this .objectModel);
396:
397: } catch (ServiceException e) {
398: throw new SAXException(
399: "Could not find an InputModule of the type "
400: + inputModuleName, e);
401: } catch (ConfigurationException e) {
402: throw new SAXException("Could not find an attribute: "
403: + attributeName + " from the InputModule "
404: + inputModuleName, e);
405: } finally {
406: if (selector != null) {
407: selector.release(inputModule);
408: this .manager.release(selector);
409: }
410: }
411:
412: return obj;
413: }
414:
415: private void setOutputAttribute(String outputModuleName,
416: String attributeName, Object value) throws SAXException {
417: ServiceSelector selector = null;
418: OutputModule outputModule = null;
419: try {
420: selector = (ServiceSelector) this .manager
421: .lookup(OutputModule.ROLE + "Selector");
422: outputModule = (OutputModule) selector
423: .select(outputModuleName);
424: outputModule.setAttribute(null, this .objectModel,
425: attributeName, value);
426: outputModule.commit(null, this .objectModel);
427:
428: } catch (ServiceException e) {
429: throw new SAXException(
430: "Could not find an OutputModule of the type "
431: + outputModuleName, e);
432: } finally {
433: if (selector != null) {
434: selector.release(outputModule);
435: this.manager.release(selector);
436: }
437: }
438: }
439: }
|