/* * XMLDBTransformer.java - Mar 7, 2003 * * @author wolf */ package org.exist.cocoon; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Stack; 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.excalibur.pool.Poolable; 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.environment.Session; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.transformation.AbstractSAXTransformer; import org.apache.cocoon.xml.dom.DOMStreamer; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.storage.serializers.Serializer; import org.exist.xmldb.XPathQueryServiceImpl; import org.w3c.dom.DocumentFragment; 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.ResourceSet; import org.xmldb.api.base.XMLDBException; import org.xmldb.api.modules.XMLResource; import org.xmldb.api.modules.XPathQueryService; import org.xmldb.api.modules.XUpdateQueryService; /** * Transformer component for querying an XML database using the * XMLDB API. * * This component provides a limited set of tags to query collections * in the database. * * @author wolf * */ public class XMLDBTransformer extends AbstractSAXTransformer implements Poolable { public String DEFAULT_DRIVER = "org.exist.xmldb.DatabaseImpl"; public String DEFAULT_USER = "guest"; public String DEFAULT_PASSWORD = "guest"; public static final String NAMESPACE = "http://exist-db.org/transformer/1.0"; public static final String COLLECTION_ELEMENT = "collection"; public static final String FOR_EACH_ELEMENT = "for-each"; public static final String CURRENT_NODE_ELEMENT = "current-node"; public static final String SELECT_NODE = "select-node"; public static final String RESULT_SET_ELEMENT = "result-set"; public static final String XUPDATE_ELEMENT = "update"; public static final String ERROR_ELEMENT = "error"; public static final String ERRMSG_ELEMENT = "message"; public static final String STACKTRACE_ELEMENT = "stacktrace"; public static final String PREFIX = "xdb:"; public static final String FATAL_ERROR = "fatal"; public static final String WARNING = "warn"; public static final String INFO= "info"; public static final int IN_COLLECTION = 1; public static final int IN_QUERY = 2; private String driver = null; private String user = null; private String password = null; private String xpath = null; private Collection collection = null; private Stack commandStack = new Stack(); private boolean isRecording = false; private int nesting = 0; private int mode = 0; private XMLResource currentResource = null; private HashMap namespaces = new HashMap(20); private String prefix = null; private StringWriter queryWriter; private TransformerHandler queryHandler; /** The trax <code>TransformerFactory</code> used by this transformer. */ private SAXTransformerFactory tfactory = null; /** * Setup the component. Accepts parameters "driver", "user" and * "password". If specified, those parameters override the default- * settings or the settings specified during component setup. * * Example: * * <map:transform type="xmldb"> * <map:parameter name="driver" value="org.exist.xmldb.DatabaseImpl"/> * <map:parameter name="user" value="guest"/> * <map:parameter name="password" value="guest"/> * </map:transform> * * @see org.apache.cocoon.sitemap.SitemapModelComponent#setup(org.apache.cocoon.environment.SourceResolver, java.util.Map, java.lang.String, org.apache.avalon.framework.parameters.Parameters) */ public void setup(SourceResolver resolver, Map map, String src, Parameters parameters) throws ProcessingException, SAXException, IOException { super.setup(resolver, map, src, parameters); driver = parameters.getParameter("driver", DEFAULT_DRIVER); user = parameters.getParameter("user", DEFAULT_USER); password = parameters.getParameter("password", DEFAULT_PASSWORD); if (request == null) { throw new ProcessingException("no request object found"); } setupDatabase(); } public void startElement(String uri, String localName, String qname, Attributes attribs) throws SAXException { if(queryHandler!=null) { this.queryHandler.startElement(uri, localName, qname, attribs); } else { if (isRecording) { if (NAMESPACE.equals(uri) && FOR_EACH_ELEMENT.equals(localName)) ++nesting; super.startElement(uri, localName, qname, attribs); } else if (NAMESPACE.equals(uri)) { if (COLLECTION_ELEMENT.equals(localName)) { prefix = ( qname.endsWith(localName) ? qname.substring(0, qname.length() - localName.length()) : PREFIX ); startCollection(attribs); } else if (FOR_EACH_ELEMENT.equals(localName)) startForEach(attribs); else if (CURRENT_NODE_ELEMENT.equals(localName)) startCurrent(attribs); else if (SELECT_NODE.equals(localName)) startSelectNode(attribs); else if(XUPDATE_ELEMENT.equals(localName)) startXUpdate(attribs); } else { if (currentResource != null) { try { AttributesImpl a = new AttributesImpl(attribs); a.addAttribute( NAMESPACE, "document-id", prefix + "document-id", "CDATA", currentResource.getDocumentId()); a.addAttribute( NAMESPACE, "collection", prefix + "collection", "CDATA", currentResource.getParentCollection().getName()); super.startElement(uri, localName, qname, a); currentResource = null; } catch (XMLDBException e) { } } else super.startElement(uri, localName, qname, attribs); } } } protected void startCollection(Attributes attribs) throws SAXException { String uri = attribs.getValue("uri"); if (uri == null) { reportError(FATAL_ERROR, "element collection requires an uri-attribute"); return; } String pUser = attribs.getValue("user"); String pPassword = attribs.getValue("password"); // use default user and password if not specified if (pUser == null) pUser = user; if (pPassword == null) pPassword = password; try { collection = DatabaseManager.getCollection(uri, pUser, pPassword); if (collection == null) { reportError(WARNING, "collection " + uri + " not found"); return; } } catch (XMLDBException e) { reportError(WARNING, "failed to retrieve collection", e); } mode = IN_COLLECTION; } protected void startCurrent(Attributes attribs) throws SAXException { if (commandStack.isEmpty()) return; ForEach each = (ForEach) commandStack.peek(); try { if (each.currentResource != null) each.currentResource.getContentAsSAX(this); } catch (XMLDBException e) { } } /** * Helper for TransformerFactory. */ protected SAXTransformerFactory getTransformerFactory() { if (tfactory == null) { tfactory = (SAXTransformerFactory) TransformerFactory.newInstance(); //tfactory.setErrorListener(new TraxErrorHandler(getLogger())); } return tfactory; } protected void startXUpdate(Attributes attribs) throws SAXException { if (collection == null) { reportError(FATAL_ERROR, "no collection selected"); return; } 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 = namespaces.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = (Map.Entry)i.next(); this.queryHandler.startPrefixMapping((String)entry.getKey(), (String)entry.getValue()); } } protected void endXUpdate() throws SAXException { try { XUpdateQueryService service = (XUpdateQueryService) collection.getService("XUpdateQueryService", "1.0"); long count = service.update(queryWriter.toString()); } catch(XMLDBException e) { reportError(FATAL_ERROR, "Unable to perform update: "+e.getMessage() , e); } } protected void startSelectNode(Attributes attribs) throws SAXException { if (collection == null) { reportError(FATAL_ERROR, "no collection selected"); return; } XMLResource resource = null; if (!commandStack.isEmpty()) { ForEach last = (ForEach) commandStack.peek(); resource = last.currentResource; } xpath = attribs.getValue("query"); if (xpath == null) { reportError(FATAL_ERROR, "attribute 'query' is missing"); return; } String pHighlightElementMatches = attribs.getValue("match-tagging-elements"); boolean highlightElementMatches = true; if (pHighlightElementMatches != null) highlightElementMatches = pHighlightElementMatches.equals("true"); String pHighlightAttributeMatches = attribs.getValue("match-tagging-attributes"); boolean highlightAttributeMatches = false; if (pHighlightAttributeMatches != null) highlightAttributeMatches = pHighlightAttributeMatches.equals("true"); final long start = System.currentTimeMillis(); try { XPathQueryServiceImpl service = (XPathQueryServiceImpl) collection.getService("XPathQueryService", "1.0"); service.setProperty(Serializer.GENERATE_DOC_EVENTS, "false"); String highlighting = "none"; if (highlightElementMatches && highlightAttributeMatches) highlighting = "both"; else if (highlightElementMatches) highlighting = "elements"; else if (highlightAttributeMatches) highlighting = "attributes"; service.setProperty(EXistOutputKeys.HIGHLIGHT_MATCHES, highlighting); setQueryContext(service); ResourceSet queryResult = (resource == null) ? service.query(xpath) : service.query(resource, xpath); if (queryResult == null) { reportError(WARNING, "query returned null"); return; } long len = queryResult.getSize(); for (long i = 0; i < len; i++) { XMLResource res = (XMLResource) queryResult.getResource(i); res.getContentAsSAX(this); } } catch (XMLDBException e) { reportError(WARNING, "error during query-execution", e); } } protected void startForEach(Attributes attribs) throws SAXException { if (collection == null) { reportError(FATAL_ERROR, "no collection selected"); return; } ForEach each = new ForEach(); XMLResource resource = null; boolean nested = !commandStack.isEmpty(); if (nested) { ForEach last = (ForEach) commandStack.peek(); resource = last.currentResource; } commandStack.push(each); // process attributes xpath = attribs.getValue("query"); if (xpath == null) { reportError(FATAL_ERROR, "attribute 'query' is missing"); return; } each.query = xpath; String sortExpr = attribs.getValue("sort-by"); String pFrom = attribs.getValue("from"); if (pFrom != null) try { each.from = Integer.parseInt(pFrom); } catch (NumberFormatException e) { reportError(WARNING, "attribute 'from' requires numeric value"); } String pTo = attribs.getValue("to"); if (pTo != null) try { each.to = Integer.parseInt(pTo); } catch (NumberFormatException e) { reportError(WARNING, "attribute 'to' requires numeric value"); } String pHighlightElementMatches = attribs.getValue("match-tagging-elements"); boolean highlightElementMatches = true; if (pHighlightElementMatches != null) highlightElementMatches = pHighlightElementMatches.equals("true"); String pHighlightAttributeMatches = attribs.getValue("match-tagging-attributes"); boolean highlightAttributeMatches = false; if (pHighlightAttributeMatches != null) highlightAttributeMatches = pHighlightAttributeMatches.equals("true"); String pSession = attribs.getValue("use-session"); boolean createSession = false; if (pSession != null) createSession = pSession.equals("true"); Session session = null; if (createSession) session = request.getSession(true); final long start = System.currentTimeMillis(); try { ResourceSet queryResult = null; XPathQueryServiceImpl service = (XPathQueryServiceImpl) collection.getService("XPathQueryService", "1.0"); service.setProperty(Serializer.GENERATE_DOC_EVENTS, "false"); String highlighting = "none"; if (highlightElementMatches && highlightAttributeMatches) highlighting = "both"; else if (highlightElementMatches) highlighting = "elements"; else if (highlightAttributeMatches) highlighting = "attributes"; service.setProperty(EXistOutputKeys.HIGHLIGHT_MATCHES, highlighting); setQueryContext(service); // check if query result is already stored in the session if (createSession && resource == null) queryResult = (ResourceSet) session.getAttribute(xpath); if (queryResult == null) { queryResult = (resource == null) ? service.query(xpath, sortExpr) : service.query(resource, xpath, sortExpr); if (createSession) session.setAttribute(xpath, queryResult); } if (queryResult == null) { reportError(WARNING, "query returned null"); return; } each.queryResult = queryResult; int size = (int) each.queryResult.getSize(); if (each.from < 0) each.from = 0; if (each.to < 0 || each.to >= size) each.to = size - 1; if (!nested) { AttributesImpl atts = new AttributesImpl(); atts.addAttribute( "", "count", "count", "CDATA", queryResult == null ? "0" : Long.toString(queryResult.getSize())); atts.addAttribute("", "xpath", "xpath", "CDATA", xpath); atts.addAttribute( "", "query-time", "query-time", "CDATA", Long.toString((System.currentTimeMillis() - start))); atts.addAttribute("", "from", "from", "CDATA", Integer.toString(each.from)); atts.addAttribute("", "to", "to", "CDATA", Integer.toString(each.to)); super.startElement( NAMESPACE, RESULT_SET_ELEMENT, prefix + RESULT_SET_ELEMENT, atts); } } catch (XMLDBException e) { reportError(FATAL_ERROR, e.getMessage(), e); commandStack.pop(); return; } nesting++; isRecording = true; startRecording(); } protected void setupDatabase() throws ProcessingException { try { Class clazz = Class.forName(driver); Database database = (Database) clazz.newInstance(); database.setProperty("create-database", "true"); DatabaseManager.registerDatabase(database); } catch (Exception e) { throw new ProcessingException("failed to setup database", e); } } protected void reportError(String type, String message) throws SAXException { reportError(type, message, null); } protected void reportError(String type, String message, Exception cause) throws SAXException { AttributesImpl attribs = new AttributesImpl(); attribs.addAttribute("", "type", "type", "CDATA", type); super.startPrefixMapping(PREFIX, NAMESPACE); super.startElement(NAMESPACE, ERROR_ELEMENT, PREFIX + ERROR_ELEMENT, attribs); super.startElement( NAMESPACE, ERRMSG_ELEMENT, PREFIX + ERRMSG_ELEMENT, new AttributesImpl()); super.characters(message.toCharArray(), 0, message.length()); super.endElement(NAMESPACE, ERRMSG_ELEMENT, PREFIX + ERRMSG_ELEMENT); if (cause != null) { PrintWriter writer = new PrintWriter(new StringWriter()); cause.printStackTrace(writer); String trace = cause.toString(); super.startElement( NAMESPACE, STACKTRACE_ELEMENT, PREFIX + STACKTRACE_ELEMENT, new AttributesImpl()); super.characters(trace.toCharArray(), 0, trace.length()); super.endElement(NAMESPACE, STACKTRACE_ELEMENT, PREFIX + STACKTRACE_ELEMENT); } super.endElement(NAMESPACE, ERROR_ELEMENT, PREFIX + ERROR_ELEMENT); super.endPrefixMapping(PREFIX); } /* (non-Javadoc) * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String) */ public void endElement(String uri, String loc, String raw) throws SAXException { if (this.queryHandler != null && !(NAMESPACE.equals(uri) && XUPDATE_ELEMENT.equals(loc)) ) { this.queryHandler.endElement(uri, loc, raw); } else if(NAMESPACE.equals(uri) && XUPDATE_ELEMENT.equals(loc)) { Iterator i = namespaces.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = (Map.Entry) i.next(); this.queryHandler.endPrefixMapping((String)entry.getKey()); } endXUpdate(); this.queryHandler = null; } else { if (isRecording) { if (NAMESPACE.equals(uri) && FOR_EACH_ELEMENT.equals(loc) && --nesting == 0) endForEach(); else super.endElement(uri, loc, raw); } else if (NAMESPACE.equals(uri)) { if (COLLECTION_ELEMENT.equals(loc)) { collection = null; mode = 0; } else if (FOR_EACH_ELEMENT.equals(loc)) { endForEach(); } return; } else super.endElement(uri, loc, raw); } } protected void endForEach() throws SAXException { isRecording = false; if (commandStack.isEmpty()) return; ForEach each = (ForEach) commandStack.peek(); DocumentFragment fragment = endRecording(); if (each.queryResult == null) return; DOMStreamer streamer = new DOMStreamer(this); for (each.current = each.from; each.current <= each.to; ++each.current) { try { each.currentResource = (XMLResource) each.queryResult.getResource(each.current); currentResource = each.currentResource; streamer.stream(fragment); } catch (XMLDBException e) { reportError(WARNING, "error while retrieving resource " + each.current, e); } } commandStack.pop(); if (commandStack.isEmpty()) super.endElement(NAMESPACE, RESULT_SET_ELEMENT, prefix + RESULT_SET_ELEMENT); } /* (non-Javadoc) * @see org.apache.avalon.excalibur.pool.Recyclable#recycle() */ public void recycle() { collection = null; mode = 0; xpath = null; commandStack.clear(); nesting = 0; } public void characters(char[] p0, int p1, int p2) throws SAXException { if(queryHandler!=null) { this.queryHandler.characters(p0,p1,p2); } else { super.characters(p0, p1, p2); } } protected class ForEach { ResourceSet queryResult = null; String query = null; int from = -1; int to = -1; XMLResource currentResource = null; long current = 0; public ForEach() { } } /** * Try to read configuration parameters from the component setup. * * Example: * * <map:transformer name="xmldb" src="org.exist.cocoon.XMLDBTransformer"> * <driver>org.exist.xmldb.DatabaseImpl</driver> * <user>guest</user> * <password>guest</password> * </map:transformer> * * will set the default driver, user and password. Note that these * values may also be set as parameters in the pipeline. * * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration) */ public void configure(Configuration configuration) throws ConfigurationException { super.configure(configuration); Configuration child = configuration.getChild("user", false); if (child != null) DEFAULT_USER = child.getValue(); child = configuration.getChild("password", false); if (child != null) DEFAULT_PASSWORD = child.getValue(); child = configuration.getChild("driver", false); if (child != null) DEFAULT_DRIVER = child.getValue(); } /* (non-Javadoc) * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String) */ public void endPrefixMapping(String prefix) throws SAXException { namespaces.remove(prefix); super.endPrefixMapping(prefix); } /* (non-Javadoc) * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String) */ public void startPrefixMapping(String prefix, String namespaceURI) throws SAXException { namespaces.put(prefix, namespaceURI); super.startPrefixMapping(prefix, namespaceURI); } private void setQueryContext(XPathQueryService service) { Map.Entry entry; for(Iterator i = namespaces.entrySet().iterator(); i.hasNext(); ) { entry = (Map.Entry)i.next(); if(entry.getKey() == null || entry.getValue() == null) continue; try { service.setNamespace((String)entry.getKey(), (String)entry.getValue()); } catch (XMLDBException e) { } } } }