/* * 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.source.impl; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.io.ByteArrayOutputStream; import java.io.ByteArrayInputStream; import java.net.MalformedURLException; import java.util.Map; 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.ServiceSelector; import org.apache.excalibur.source.ModifiableSource; import org.apache.excalibur.source.SourceException; import org.apache.excalibur.source.impl.AbstractSource; import org.apache.excalibur.xml.sax.SAXParser; import org.apache.excalibur.xml.sax.XMLizable; import org.apache.cocoon.components.modules.input.InputModule; import org.apache.cocoon.components.modules.output.OutputModule; import org.apache.cocoon.serialization.Serializer; import org.apache.cocoon.util.jxpath.DOMFactory; import org.apache.cocoon.xml.dom.DOMBuilder; import org.apache.cocoon.xml.dom.DOMStreamer; import org.apache.commons.jxpath.JXPathContext; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * A <code>ModifiableSource</code> that takes its content from a * module. * <p>The URI syntax is * "xmodule:[<input-module>|<output-module>]:attribute-name[#XPath]", * where : * <ul> * <li>an input-module name is used for finding an input-module for reading data from</li>, * <li>an output-module name is used for finding an output-module for writing data to</li>, * <li>"attribute-name" is the name of the attribute found in the module</li>, * <li>"XPath" is an XPath that is aplied on the object in the * attribute, by using JXPath.</li> * </ul> * </p> * * @author <a href="mailto:danielf@nada.kth.se">Daniel Fagerstom</a> */ public class XModuleSource extends AbstractSource implements ModifiableSource, XMLizable, DOMBuilder.Listener { private final static String SCHEME = "xmodule"; private String attributeType; private String attributeName; private String xPath; protected ServiceManager manager; private Map objectModel; private Logger logger; // TODO: make this actually configurable private String configuredSerializerName = "xml"; /** * Create a xmodule source from a 'xmodule:' uri and a the object model. * <p>The uri is of the form "xmodule:/attribute-type/attribute-name/xpath</p> */ public XModuleSource( Map objectModel, String uri, ServiceManager manager, Logger logger ) throws MalformedURLException { this.objectModel = objectModel; this.manager = manager; this.logger = logger; setSystemId( uri ); // Scheme int start = 0; int end = uri.indexOf( ':' ); if ( end == -1 ) throw new MalformedURLException("Malformed uri for xmodule source (cannot find scheme) : " + uri); String scheme = uri.substring( start, end ); if ( !SCHEME.equals( scheme ) ) throw new MalformedURLException("Malformed uri for a xmodule source : " + uri); setScheme( scheme ); // Attribute type start = end + 1; end = uri.indexOf( ':', start ); if ( end == -1 ) { throw new MalformedURLException("Malformed uri for xmodule source (cannot find attribute type) : " + uri); } this.attributeType = uri.substring( start, end ); // Attribute name start = end + 1; end = uri.indexOf( '#', start ); if ( end == -1 ) end = uri.length(); if ( end == start ) throw new MalformedURLException("Malformed uri for xmodule source (cannot find attribute name) : " + uri); this.attributeName = uri.substring( start, end ); // xpath start = end + 1; this.xPath = start < uri.length() ? uri.substring( start ) : ""; } /** * Implement this method to obtain SAX events. * */ public void toSAX(ContentHandler handler) throws SAXException { Object obj = getInputAttribute( this.attributeType, this.attributeName ); if ( obj == null ) throw new SAXException( " The attribute: " + this.attributeName + " is empty" ); if ( !(this.xPath.length() == 0 || this.xPath.equals( "/" )) ) { JXPathContext context = JXPathContext.newContext( obj ); obj = context.getPointer( this.xPath ).getNode(); if ( obj == null ) throw new SAXException( "the xpath: " + this.xPath + " applied on the attribute: " + this.attributeName + " returns null"); } if ( obj instanceof Document ) { DOMStreamer domStreamer = new DOMStreamer( handler ); domStreamer.stream( (Document)obj ); } else if ( obj instanceof Node ) { DOMStreamer domStreamer = new DOMStreamer( handler ); handler.startDocument(); domStreamer.stream( (Node)obj ); handler.endDocument(); } else if ( obj instanceof XMLizable ) { ((XMLizable)obj).toSAX( handler ); } else { throw new SAXException( "The object type: " + obj.getClass() + " could not be serialized to XML: " + obj ); } } /** * Return an <code>InputStream</code> object to read from the source. * * @throws IOException if I/O error occured. */ public InputStream getInputStream() throws IOException, SourceException { if ( this.logger.isDebugEnabled() ) { this.logger.debug( "Getting InputStream for " + getURI() ); } // Serialize the SAX events to the XMLSerializer ByteArrayInputStream inputStream = null; ServiceSelector selector = null; Serializer serializer = null; try { selector = (ServiceSelector)this.manager.lookup(Serializer.ROLE + "Selector"); serializer = (Serializer)selector.select(this.configuredSerializerName); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2048); serializer.setOutputStream(outputStream); toSAX(serializer); inputStream = new ByteArrayInputStream(outputStream.toByteArray()); } catch (SAXException e) { throw new SourceException("Serializing SAX to a ByteArray failed!", e); } catch (ServiceException e) { throw new SourceException("Retrieving serializer failed.", e); } finally { if (selector != null) { selector.release(serializer); this.manager.release(selector); } } return inputStream; } /** * Does this source actually exist ? * * @return true if the resource exists. * */ public boolean exists() { boolean exists = false; try { exists = getInputAttribute( this.attributeType, this.attributeName ) != null; } catch ( SAXException e ) { exists = false; } return exists; } /** * Get an <code>InputStream</code> where raw bytes can be written to. * The signification of these bytes is implementation-dependent and * is not restricted to a serialized XML document. * * @return a stream to write to */ public OutputStream getOutputStream() throws IOException { return new DOMOutputStream(); } /** * Delete the source */ public void delete() throws SourceException { if ( !(this.xPath.length() == 0 || this.xPath.equals( "/" )) ) { Object value; try { value = getInputAttribute( this.attributeType, this.attributeName ); } catch ( SAXException e ) { throw new SourceException( "delete: ", e ); } if ( value == null ) throw new SourceException( " The attribute: " + this.attributeName + " is empty" ); JXPathContext context = JXPathContext.newContext( value ); context.removeAll( this.xPath ); } else { try { setOutputAttribute( this.attributeType, this.attributeName, null ); } catch ( SAXException e ) { throw new SourceException( "delete: ", e ); } } } /** * FIXME * delete is an operator in java script, this method is for * testing puposes in java script only */ public void deleteTest() throws SourceException { delete(); } /** * Can the data sent to an <code>OutputStream</code> returned by * {@link #getOutputStream()} be cancelled ? * * @return true if the stream can be cancelled */ public boolean canCancel( OutputStream stream ) { return false; } /** * Cancel the data sent to an <code>OutputStream</code> returned by * {@link #getOutputStream()}. * <p> * After cancel, the stream should no more be used. */ public void cancel(OutputStream stream) throws IOException {} /** * Get a <code>ContentHandler</code> where an XML document can * be written using SAX events. * <p> * Care should be taken that the returned handler can actually * be a {@link org.apache.cocoon.xml.XMLConsumer} supporting also * lexical events such as comments. * * @return a handler for SAX events */ public ContentHandler getContentHandler() { return new DOMBuilder( this ); } public void notify( Document insertDoc ) throws SAXException { // handle xpaths, we are only handling inserts, i.e. if there is no // attribute of the given name and type the operation will fail if ( !(this.xPath.length() == 0 || this.xPath.equals( "/" )) ) { Object value = getInputAttribute( this.attributeType, this.attributeName ); if ( value == null ) throw new SAXException( " The attribute: " + this.attributeName + " is empty" ); JXPathContext context = JXPathContext.newContext( value ); if ( value instanceof Document ) { // If the attribute contains a dom document we // create the elements in the given xpath if // necesary, import the input document and put it // in the place described by the xpath. Document doc = (Document)value; Node importedNode = doc.importNode( insertDoc.getDocumentElement(), true ); context.setLenient( true ); context.setFactory( new DOMFactory() ); context.createPathAndSetValue( this.xPath, importedNode ); } else { // Otherwise just try to put a the input document in // the place pointed to by the xpath context.setValue( this.xPath, insertDoc ); } } else { setOutputAttribute( this.attributeType, this.attributeName, insertDoc ); } } private class DOMOutputStream extends ByteArrayOutputStream { public void close() throws IOException { SAXParser parser = null; try { parser = (SAXParser)XModuleSource.this.manager.lookup( SAXParser.ROLE ); parser.parse( new InputSource( new ByteArrayInputStream( super.toByteArray() ) ), XModuleSource.this.getContentHandler()); } catch (Exception e){ throw new IOException("Exception during processing of " + XModuleSource.this.getURI() + e.getMessage()); } finally { if (parser != null) XModuleSource.this.manager.release( parser ); } super.close(); } } private Object getInputAttribute( String inputModuleName, String attributeName ) throws SAXException { Object obj; ServiceSelector selector = null; InputModule inputModule = null; try { selector = (ServiceSelector) this.manager.lookup( InputModule.ROLE + "Selector" ); inputModule = (InputModule) selector.select( inputModuleName ); obj = inputModule.getAttribute( attributeName, null, this.objectModel ); } catch ( ServiceException e ) { throw new SAXException( "Could not find an InputModule of the type " + inputModuleName , e ); } catch ( ConfigurationException e ) { throw new SAXException( "Could not find an attribute: " + attributeName + " from the InputModule " + inputModuleName, e ); } finally { if ( selector != null ) { selector.release( inputModule ); this.manager.release( selector ); } } return obj; } private void setOutputAttribute( String outputModuleName, String attributeName, Object value ) throws SAXException{ ServiceSelector selector = null; OutputModule outputModule = null; try { selector = (ServiceSelector) this.manager.lookup( OutputModule.ROLE + "Selector" ); outputModule = (OutputModule) selector.select( outputModuleName ); outputModule.setAttribute( null, this.objectModel, attributeName, value ); outputModule.commit( null, this.objectModel ); } catch ( ServiceException e ) { throw new SAXException( "Could not find an OutputModule of the type " + outputModuleName , e ); } finally { if ( selector != null ) { selector.release( outputModule ); this.manager.release( selector ); } } } }