| /* |
| * 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.transformation; |
| |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.io.StringWriter; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Properties; |
| |
| import javax.xml.transform.OutputKeys; |
| import javax.xml.transform.TransformerConfigurationException; |
| import javax.xml.transform.TransformerFactory; |
| import javax.xml.transform.sax.SAXTransformerFactory; |
| import javax.xml.transform.sax.TransformerHandler; |
| import javax.xml.transform.stream.StreamResult; |
| |
| import org.apache.avalon.framework.activity.Initializable; |
| import org.apache.avalon.framework.configuration.Configurable; |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.parameters.Parameters; |
| import org.apache.cocoon.ProcessingException; |
| import org.apache.cocoon.ResourceNotFoundException; |
| import org.apache.cocoon.caching.CacheableProcessingComponent; |
| import org.apache.cocoon.environment.SourceResolver; |
| import org.apache.cocoon.util.TraxErrorHandler; |
| import org.apache.excalibur.source.SourceValidity; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.AttributesImpl; |
| import org.xmldb.api.DatabaseManager; |
| import org.xmldb.api.base.Collection; |
| import org.xmldb.api.base.Database; |
| import org.xmldb.api.base.Resource; |
| import org.xmldb.api.base.XMLDBException; |
| import org.xmldb.api.modules.CollectionManagementService; |
| import org.xmldb.api.modules.XUpdateQueryService; |
| |
| /** |
| * This transformer allows to perform resource creation, deletion, and |
| * XUpdate command execution in XML:DB. All operations are performed either |
| * in <code>base</code> collection, or context collection, which |
| * is specified as <code>collection</code> attribute on the <code>query</code> |
| * element. Context collection must be specified relative to the base collection. |
| * |
| * <p>Definition:</p> |
| * <pre> |
| * <map:transformer name="xmldb" src="org.apache.cocoon.transformation.XMLDBTransformer"> |
| * <!-- Optional driver parameter. Uncomment if you want transformer to register a database. |
| * <driver>org.apache.xindice.client.xmldb.DatabaseImpl</driver> |
| * --> |
| * <base>xmldb:xindice:///db/collection</base> |
| * <user>myDatabaseLogin</user> |
| * <password>myDatabasePassword</password> |
| * </map:transformer> |
| * </pre> |
| * |
| * <p>The component configuration defined in <map:transformer> can be |
| * overriden with sitemap parameters on the <map:transform>:</p> |
| * <pre> |
| * <map:transform type="xmldb"> |
| * <map:parameter name="base" value="xmldb:xindice:///db/collection"/> |
| * <map:parameter name="user" value="myDatabaseLogin"/> |
| * <map:parameter name="password" value="myDatabasePassword"/> |
| * </map:transform> |
| * </pre> |
| * |
| * <p>Input XML document example:</p> |
| * <pre> |
| * <page xmlns:db="http://apache.org/cocoon/xmldb/1.0"> |
| * ... |
| * <p>Create XML resource in base collection with specified object ID</p> |
| * <db:query type="create" oid="xmldb-object-id"> |
| * <page> |
| * XML Object body |
| * </page> |
| * </db:query> |
| * |
| * <p>Delete XML resource from the base collection with specified object ID</p> |
| * <db:query type="delete" oid="xmldb-object-id"/> |
| * |
| * <p>Update XML resource with specified object ID</p> |
| * <db:query type="update" oid="xmldb-object-id"> |
| * <xu:modifications version="1.0" xmlns:xu="http://www.xmldb.org/xupdate"> |
| * <xu:remove select="/person/phone[@type = 'home']"/> |
| * <xu:update select="/person/phone[@type = 'work']"> |
| * 480-300-3003 |
| * </xu:update> |
| * </xu:modifications> |
| * </db:query> |
| * |
| * <p>Create collection nested into the base collection</p> |
| * <db:query type="create" oid="inner/"/> |
| * |
| * <p>Create XML resource in context collection with specified object ID</p> |
| * <db:query type="create" collection="inner" oid="xmldb-object-id"> |
| * <page> |
| * XML Object body |
| * </page> |
| * </db:query> |
| * ... |
| * </page> |
| * </pre> |
| * |
| * <p>Output XML document example:</p> |
| * <pre> |
| * <page xmlns:db="http://apache.org/cocoon/xmldb/1.0"> |
| * ... |
| * <db:query type="create" oid="xmldb-object-id" result="success"/> |
| * |
| * <db:query type="delete" oid="xmldb-object-id" result="success"/> |
| * |
| * <db:query type="update" oid="xmldb-object-id" result="failure"> |
| * Resource xmldb-object-id is not found |
| * </db:query> |
| * ... |
| * </page> |
| * </pre> |
| * |
| * <p>Known bugs and limitations:</p> |
| * <ul> |
| * <li>No namespaces with Xalan (see AbstractTextSerializer)</li> |
| * </ul> |
| * |
| * @version $Id$ |
| */ |
| public class XMLDBTransformer extends AbstractTransformer |
| implements CacheableProcessingComponent, Configurable, Initializable { |
| |
| private static String XMLDB_URI = "http://apache.org/cocoon/xmldb/1.0"; |
| private static String XMLDB_QUERY_ELEMENT = "query"; |
| private static String XMLDB_QUERY_TYPE_ATTRIBUTE = "type"; |
| private static String XMLDB_QUERY_CONTEXT_ATTRIBUTE = "collection"; |
| private static String XMLDB_QUERY_OID_ATTRIBUTE = "oid"; |
| private static String XMLDB_QUERY_RESULT_ATTRIBUTE = "result"; |
| |
| /** The trax <code>TransformerFactory</code> used by this transformer. */ |
| private SAXTransformerFactory tfactory = null; |
| private Properties format = new Properties(); |
| |
| /** The map of namespace prefixes. */ |
| private Map prefixMap = new HashMap(); |
| |
| /** XML:DB driver class name (optional) */ |
| private String driver = null; |
| |
| /** Default collection name. */ |
| private String default_base; |
| |
| /** Default user name. */ |
| private String default_user; |
| |
| /** Default password. */ |
| private String default_password; |
| |
| /** Current collection name. */ |
| private String local_base; |
| |
| /** Current collection name. */ |
| private String xbase; |
| |
| /** Current collection. */ |
| private Collection collection; |
| |
| /** database login */ |
| private String local_user = null; |
| |
| /** database password */ |
| private String local_password = null; |
| |
| /** Operation. One of: create, delete, update. */ |
| private String operation; |
| |
| /** Document ID. Can be null if update or insert is performed on collection. */ |
| private String key; |
| |
| /** Result of current operation. Success or failure. */ |
| private String result; |
| |
| /** Message in case current operation failed. */ |
| private String message; |
| |
| private StringWriter queryWriter; |
| private TransformerHandler queryHandler; |
| |
| /** True when inside <query> element. */ |
| private boolean processing; |
| |
| |
| public XMLDBTransformer() { |
| format.put(OutputKeys.ENCODING, "utf-8"); |
| format.put(OutputKeys.INDENT, "no"); |
| format.put(OutputKeys.OMIT_XML_DECLARATION, "yes"); |
| } |
| |
| public void configure(Configuration configuration) throws ConfigurationException { |
| this.driver = configuration.getChild("driver").getValue(null); |
| if (driver == null) { |
| getLogger().debug("Driver parameter is missing. Transformer will not initialize database."); |
| } |
| |
| this.default_base = configuration.getChild("base").getValue(null); |
| this.default_user = configuration.getChild("user").getValue(null); |
| this.default_password = configuration.getChild("password").getValue(null); |
| } |
| |
| /** |
| * Initializes XML:DB database instance if driver class was configured. |
| */ |
| public void initialize() throws Exception { |
| if (driver != null) { |
| Class c = Class.forName(driver); |
| Database database = (Database)c.newInstance(); |
| DatabaseManager.registerDatabase(database); |
| } |
| } |
| |
| /** Setup the transformer. */ |
| public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) |
| throws ProcessingException, SAXException, IOException { |
| |
| this.local_base = par.getParameter("base", this.default_base); |
| if (this.local_base == null) { |
| throw new ProcessingException("Required base parameter is missing. Syntax is: xmldb:xindice:///db/collection"); |
| } |
| |
| /** Get user password from parameter for the database. Usefull for update action */ |
| this.local_user = par.getParameter("user", this.default_user); |
| this.local_password = par.getParameter("password", this.default_password); |
| |
| try { |
| this.collection = DatabaseManager.getCollection(this.local_base, this.local_user, this.local_password); |
| } catch (XMLDBException e) { |
| throw new ProcessingException("Could not get collection " + this.local_base + ": " + e.errorCode, e); |
| } |
| |
| if (this.collection == null) { |
| throw new ResourceNotFoundException("Collection " + this.local_base + " does not exist"); |
| } |
| } |
| |
| /** |
| * Helper for TransformerFactory. |
| */ |
| protected SAXTransformerFactory getTransformerFactory() { |
| if (tfactory == null) { |
| tfactory = (SAXTransformerFactory) TransformerFactory.newInstance(); |
| tfactory.setErrorListener(new TraxErrorHandler(getLogger())); |
| } |
| return tfactory; |
| } |
| |
| /** |
| * Generate the unique key. |
| * This key must be unique inside the space of this component. |
| * This method must be invoked before the generateValidity() method. |
| * |
| * @return The generated key or <code>null</code> if the component |
| * is currently not cacheable. |
| */ |
| public Serializable getKey() { |
| return null; |
| } |
| |
| /** |
| * Generate the validity object. |
| * Before this method can be invoked the generateKey() method |
| * must be invoked. |
| * |
| * @return The generated validity object or <code>null</code> if the |
| * component is currently not cacheable. |
| */ |
| public SourceValidity getValidity() { |
| return null; |
| } |
| |
| /** |
| * Receive notification of the beginning of a document. |
| */ |
| public void startDocument() throws SAXException { |
| super.startDocument(); |
| } |
| |
| /** |
| * Receive notification of the end of a document. |
| */ |
| public void endDocument() throws SAXException { |
| super.endDocument(); |
| } |
| |
| /** |
| * Begin the scope of a prefix-URI Namespace mapping. |
| * |
| * @param prefix The Namespace prefix being declared. |
| * @param uri The Namespace URI the prefix is mapped to. |
| */ |
| public void startPrefixMapping(String prefix, String uri) throws SAXException { |
| if (!processing) { |
| super.startPrefixMapping(prefix,uri); |
| prefixMap.put(prefix,uri); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.startPrefixMapping(prefix, uri); |
| } |
| } |
| |
| /** |
| * End the scope of a prefix-URI mapping. |
| * |
| * @param prefix The prefix that was being mapping. |
| */ |
| public void endPrefixMapping(String prefix) throws SAXException { |
| if (!processing) { |
| super.endPrefixMapping(prefix); |
| prefixMap.remove(prefix); |
| } else if (this.queryHandler != null){ |
| this.queryHandler.endPrefixMapping(prefix); |
| } |
| } |
| |
| /** |
| * Receive notification of the beginning of an element. |
| * |
| * @param uri The Namespace URI, or the empty string if the element has no |
| * Namespace URI or if Namespace |
| * processing is not being performed. |
| * @param loc The local name (without prefix), or the empty string if |
| * Namespace processing is not being performed. |
| * @param raw The raw XML 1.0 name (with prefix), or the empty string if |
| * raw names are not available. |
| * @param a The attributes attached to the element. If there are no |
| * attributes, it shall be an empty Attributes object. |
| */ |
| public void startElement(String uri, String loc, String raw, Attributes a) |
| throws SAXException { |
| if (!processing) { |
| if (XMLDB_URI.equals(uri) && XMLDB_QUERY_ELEMENT.equals(loc)){ |
| |
| this.operation = a.getValue(XMLDB_QUERY_TYPE_ATTRIBUTE); |
| if (!"create".equals(operation) && !"delete".equals(operation) && !"update".equals(operation)) { |
| throw new SAXException("Supported operation types are: create, delete, update"); |
| } |
| |
| this.key = a.getValue(XMLDB_QUERY_OID_ATTRIBUTE); |
| if ("delete".equals(operation) && this.key == null) { |
| throw new SAXException("Object ID attribute is missing on query element"); |
| } |
| |
| this.xbase = a.getValue(XMLDB_QUERY_CONTEXT_ATTRIBUTE); |
| |
| // Start processing |
| result = "failure"; |
| message = null; |
| processing = true; |
| |
| if ("create".equals(operation) && this.key != null && this.key.endsWith("/")) { |
| } else if (!"delete".equals(operation)) { |
| // Prepare SAX query writer |
| queryWriter = new StringWriter(256); |
| try { |
| this.queryHandler = getTransformerFactory().newTransformerHandler(); |
| this.queryHandler.setResult(new StreamResult(queryWriter)); |
| this.queryHandler.getTransformer().setOutputProperties(format); |
| } catch (TransformerConfigurationException e) { |
| throw new SAXException("Failed to get transformer handler", e); |
| } |
| |
| // Start query document |
| this.queryHandler.startDocument(); |
| Iterator i = prefixMap.entrySet().iterator(); |
| while (i.hasNext()) { |
| Map.Entry entry = (Map.Entry)i.next(); |
| this.queryHandler.startPrefixMapping((String)entry.getKey(), (String)entry.getValue()); |
| } |
| } |
| } else { |
| super.startElement(uri, loc, raw, a); |
| } |
| } else if (this.queryHandler != null) { |
| this.queryHandler.startElement(uri, loc, raw, a); |
| } |
| } |
| |
| /** |
| * Receive notification of the end of an element. |
| * |
| * @param uri The Namespace URI, or the empty string if the element has no |
| * Namespace URI or if Namespace |
| * processing is not being performed. |
| * @param loc The local name (without prefix), or the empty string if |
| * Namespace processing is not being performed. |
| * @param raw The raw XML 1.0 name (with prefix), or the empty string if |
| * raw names are not available. |
| */ |
| public void endElement(String uri, String loc, String raw) |
| throws SAXException { |
| if (!processing) { |
| super.endElement(uri,loc,raw); |
| } else { |
| if (XMLDB_URI.equals(uri) && XMLDB_QUERY_ELEMENT.equals(loc)) { |
| processing = false; |
| |
| String document = null; |
| if (this.queryHandler != null) { |
| // Finish building query. Remove existing prefix mappings. |
| Iterator i = prefixMap.entrySet().iterator(); |
| while (i.hasNext()) { |
| Map.Entry entry = (Map.Entry) i.next(); |
| this.queryHandler.endPrefixMapping((String)entry.getKey()); |
| } |
| this.queryHandler.endDocument(); |
| document = this.queryWriter.toString(); |
| } |
| |
| // Perform operation |
| Collection collection = null; |
| try { |
| // Obtain collection for the current operation |
| collection = (xbase != null)? DatabaseManager.getCollection(local_base + "/" + xbase, this.local_user, this.local_password) : this.collection; |
| |
| if (collection == null) { |
| message = "Failed to " + operation + " resource " + this.key + ": Collection " + local_base + "/" + xbase + " not found."; |
| getLogger().debug(message); |
| } else if ("create".equals(operation)) { |
| if (key != null && key.endsWith("/")) { |
| try { |
| // Cut trailing '/' |
| String k = this.key.substring(0, this.key.length() - 1); |
| CollectionManagementService service = |
| (CollectionManagementService) collection.getService("CollectionManagementService", "1.0"); |
| service.createCollection(k); |
| result = "success"; |
| } catch (XMLDBException e) { |
| message = "Failed to create collection " + this.key + ": " + e.errorCode; |
| getLogger().error(message, e); |
| } |
| } else { |
| try { |
| if (key == null) { |
| key = collection.createId(); |
| } |
| // Support of binary objects can be added. Content can be obtained using Source. |
| Resource resource = collection.createResource(key, "XMLResource"); |
| resource.setContent(document); |
| collection.storeResource(resource); |
| result = "success"; |
| key = resource.getId(); |
| } catch (XMLDBException e) { |
| message = "Failed to create resource " + key + ": " + e.errorCode; |
| getLogger().debug(message, e); |
| } |
| } |
| } else if ("delete".equals(operation)) { |
| if (key != null && key.endsWith("/")) { |
| try { |
| // Cut trailing '/' |
| String k = this.key.substring(0, this.key.length() - 1); |
| CollectionManagementService service = |
| (CollectionManagementService) collection.getService("CollectionManagementService", "1.0"); |
| service.removeCollection(k); |
| result = "success"; |
| } catch (XMLDBException e) { |
| message = "Failed to delete collection " + this.key + ": " + e.errorCode; |
| getLogger().error(message, e); |
| } |
| } else { |
| try { |
| Resource resource = collection.getResource(this.key); |
| if (resource == null) { |
| message = "Resource " + this.key + " does not exist"; |
| getLogger().debug(message); |
| } else { |
| collection.removeResource(resource); |
| result = "success"; |
| } |
| } catch (XMLDBException e) { |
| message = "Failed to delete resource " + key + ": " + e.errorCode; |
| getLogger().debug(message, e); |
| } |
| } |
| } else if ("update".equals(operation)) { |
| try { |
| XUpdateQueryService service = |
| (XUpdateQueryService) collection.getService("XUpdateQueryService", "1.0"); |
| long count = (this.key == null)? |
| service.update(document) : service.updateResource(this.key, document); |
| message = count + " entries updated."; |
| result = "success"; |
| } catch (XMLDBException e) { |
| message = "Failed to update resource " + key + ": " + e.errorCode; |
| getLogger().debug(message, e); |
| } |
| } |
| } catch (XMLDBException e) { |
| message = "Failed to get context collection for the query (base: " + local_base + ", context: " + xbase + "): " + e.errorCode; |
| getLogger().debug(message, e); |
| } finally { |
| if (xbase != null && collection != null) { |
| try { |
| collection.close(); |
| } catch (XMLDBException ignored) { |
| } |
| } |
| } |
| |
| // Report result |
| AttributesImpl attrs = new AttributesImpl(); |
| attrs.addAttribute("", XMLDB_QUERY_OID_ATTRIBUTE, |
| XMLDB_QUERY_OID_ATTRIBUTE, "CDATA", this.key); |
| attrs.addAttribute("", XMLDB_QUERY_TYPE_ATTRIBUTE, |
| XMLDB_QUERY_TYPE_ATTRIBUTE, "CDATA", this.operation); |
| attrs.addAttribute("", XMLDB_QUERY_RESULT_ATTRIBUTE, |
| XMLDB_QUERY_RESULT_ATTRIBUTE, "CDATA", result); |
| super.startElement(uri, loc, raw, attrs); |
| if (message != null) { |
| super.characters(message.toCharArray(), 0, message.length()); |
| } |
| super.endElement(uri, loc, raw); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.endElement(uri, loc, raw); |
| } |
| } |
| } |
| |
| /** |
| * Receive notification of character data. |
| * |
| * @param c The characters from the XML document. |
| * @param start The start position in the array. |
| * @param len The number of characters to read from the array. |
| */ |
| public void characters(char c[], int start, int len) throws SAXException { |
| if (!processing) { |
| super.characters(c,start,len); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.characters(c,start,len); |
| } |
| } |
| |
| /** |
| * Receive notification of ignorable whitespace in element content. |
| * |
| * @param c The characters from the XML document. |
| * @param start The start position in the array. |
| * @param len The number of characters to read from the array. |
| */ |
| public void ignorableWhitespace(char c[], int start, int len) throws SAXException { |
| if (!processing) { |
| super.ignorableWhitespace(c,start,len); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.ignorableWhitespace(c,start,len); |
| } |
| } |
| |
| /** |
| * Receive notification of a processing instruction. |
| * |
| * @param target The processing instruction target. |
| * @param data The processing instruction data, or null if none was |
| * supplied. |
| */ |
| public void processingInstruction(String target, String data) throws SAXException { |
| if (!processing) { |
| super.processingInstruction(target,data); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.processingInstruction(target,data); |
| } |
| } |
| |
| /** |
| * Receive notification of a skipped entity. |
| * |
| * @param name The name of the skipped entity. If it is a parameter |
| * entity, the name will begin with '%'. |
| */ |
| public void skippedEntity(String name) throws SAXException { |
| if (!processing) { |
| super.skippedEntity(name); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.skippedEntity(name); |
| } |
| } |
| |
| /** |
| * Report the start of DTD declarations, if any. |
| * |
| * @param name The document type name. |
| * @param publicId The declared public identifier for the external DTD |
| * subset, or null if none was declared. |
| * @param systemId The declared system identifier for the external DTD |
| * subset, or null if none was declared. |
| */ |
| public void startDTD(String name, String publicId, String systemId) throws SAXException { |
| if (!processing) { |
| super.startDTD(name, publicId, systemId); |
| } else { |
| throw new SAXException( |
| "Recieved startDTD after beginning SVG extraction process." |
| ); |
| } |
| } |
| |
| /** |
| * Report the end of DTD declarations. |
| */ |
| public void endDTD() throws SAXException { |
| if (!processing) { |
| super.endDTD(); |
| } else { |
| throw new SAXException("Recieved endDTD after xmldb element."); |
| } |
| } |
| |
| /** |
| * Report the beginning of an entity. |
| * |
| * @param name The name of the entity. If it is a parameter entity, the |
| * name will begin with '%'. |
| */ |
| public void startEntity(String name) throws SAXException { |
| if (!processing) { |
| super.startEntity(name); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.startEntity(name); |
| } |
| } |
| |
| /** |
| * Report the end of an entity. |
| * |
| * @param name The name of the entity that is ending. |
| */ |
| public void endEntity(String name) throws SAXException { |
| if (!processing) { |
| super.endEntity(name); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.endEntity(name); |
| } |
| } |
| |
| /** |
| * Report the start of a CDATA section. |
| */ |
| public void startCDATA() throws SAXException { |
| if (!processing) { |
| super.startCDATA(); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.startCDATA(); |
| } |
| } |
| |
| /** |
| * Report the end of a CDATA section. |
| */ |
| public void endCDATA() throws SAXException { |
| if (!processing) { |
| super.endCDATA(); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.endCDATA(); |
| } |
| } |
| |
| /** |
| * Report an XML comment anywhere in the document. |
| * |
| * @param ch An array holding the characters in the comment. |
| * @param start The starting position in the array. |
| * @param len The number of characters to use from the array. |
| */ |
| public void comment(char ch[], int start, int len) throws SAXException { |
| if (!processing) { |
| super.comment(ch, start, len); |
| } else if (this.queryHandler != null) { |
| this.queryHandler.comment(ch, start, len); |
| } |
| } |
| |
| public void recycle() { |
| this.prefixMap.clear(); |
| this.queryHandler = null; |
| this.queryWriter = null; |
| |
| try { |
| if (collection != null) { |
| collection.close(); |
| } |
| } catch (XMLDBException e) { |
| getLogger().error("Failed to close collection " + this.local_base + ". Error " + e.errorCode, e); |
| } |
| collection = null; |
| super.recycle(); |
| } |
| } |