/* * 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.util.Enumeration; import java.util.Map; import java.util.Properties; import javax.xml.transform.OutputKeys; import org.apache.avalon.framework.parameters.Parameters; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.caching.validity.EventValidity; import org.apache.cocoon.components.webdav.WebDAVEventFactory; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.xml.XMLUtils; import org.apache.cocoon.xml.dom.DOMStreamer; import org.apache.commons.httpclient.HttpConnection; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.HttpURL; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.AggregatedValidity; import org.apache.webdav.lib.BaseProperty; import org.apache.webdav.lib.WebdavResource; import org.apache.webdav.lib.methods.OptionsMethod; import org.apache.webdav.lib.methods.SearchMethod; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; /** * This transformer performs DASL queries on DASL-enabled WebDAV servers. * It expects a "query" element in the "http://cocoon.apache.org/webdav/dasl/1.0" * namespace containing the DASL query to execute, with a "target" attribute specifiyng * the webdav:// or http:// URL for the target WebDAV server. It will then replace * it with a "query-result" element containing the WebDAV results. * * Each result will be contained in a "result" element with a "path" attribute pointing at it * and all WebDAV properties presented as (namespaced) children elements. * * Sample invocation: * lt;dasl:query xmlns:dasl="http://cocoon.apache.org/webdav/dasl/1.0" * target="webdav://localhost/repos/"gt; * lt;D:searchrequest xmlns:D="DAV:"gt; * lt;D:basicsearchgt; * lt;D:selectgt; * lt;D:allprop/gt; * lt;/D:selectgt; * lt;D:fromgt; * lt;D:scopegt; * lt;D:hrefgt;/repos/lt;/D:hrefgt; * lt;D:depthgt;infinitylt;/D:depthgt; * lt;/D:scopegt; * lt;/D:fromgt; * lt;D:wheregt; * lt;D:eqgt; * lt;D:propgt;lt;D:getcontenttype/gt;lt;/D:propgt; * lt;D:literalgt;text/htmllt;/D:literalgt; * lt;/D:eqgt; * lt;/D:wheregt; * lt;D:orderbygt; * lt;D:ordergt; * lt;D:propgt; * lt;D:getcontentlength/gt; * lt;/D:propgt; * lt;D:ascending/gt; * lt;/D:ordergt; * lt;D:ordergt; * lt;D:propgt; * lt;D:href/gt; * lt;/D:propgt; * lt;D:ascending/gt; * lt;/D:ordergt; * lt;/D:orderbygt; * lt;/D:basicsearchgt; * lt;/D:searchrequestgt; * lt;/dasl:querygt; * * Features * - Substitution of a value: with this feature it's possible to pass value from sitemap * that are substituted into a query. * sitemap example: * lt;map:transformer type="dasl"gt; * lt;parameter name="repos" value="/repos/"gt; * lt;/map:transformergt; * query example: * .... * lt;D:hrefgt;lt;substitute-value name="repos"/gt;lt;/D:hrefgt; * .... * This feature is like substitute-value of SQLTransformer * * TODO: the SWCL Search method doesn't preserve the result order, which makes * order-by clauses useless. * * TODO: *much* better error handling. * * @author <a href="mailto: gianugo@apache.org">Gianugo Rabellino</a> * @author <a href="mailto:d.madama@pro-netics.com>Daniele Madama</a> * @version $Id$ */ public class DASLTransformer extends AbstractSAXTransformer implements CacheableProcessingComponent { /** The prefix for tag */ static final String PREFIX = "dasl"; /** The tag name identifying the query */ static final String QUERY_TAG = "query"; /** The tag namespace */ static final String DASL_QUERY_NS = "http://cocoon.apache.org/webdav/dasl/1.0"; /** The URL namespace */ static final String TARGET_URL = "target"; /** The WebDAV scheme*/ static final String WEBDAV_SCHEME = "webdav://"; /** The tag name of root_tag for result */ static final String RESULT_ROOT_TAG = "query-result"; /** The tag name of root_tag for errors */ static final String ERROR_ROOT_TAG = "error"; /** The tag name for substitution of query parameter */ static final String SUBSTITUTE_TAG = "substitute-value"; /** The tag name for substitution of query parameter */ static final String SUBSTITUTE_TAG_NAME_ATTRIBUTE = "name"; protected static final String PATH_NODE_NAME = "path"; protected static final String RESOURCE_NODE_NAME = "resource"; /** The target HTTP URL */ String targetUrl; /** The validity of this dasl transformation run */ private AggregatedValidity m_validity = null; /** The WebdavEventFactory to abstract Event creation */ private WebDAVEventFactory m_eventfactory = null; /** * Intercept the <dasl:query> start tag. * * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ public void startElement( String uri, String name, String raw, Attributes attr) throws SAXException { if (name.equals(QUERY_TAG) && uri.equals(DASL_QUERY_NS)) { this.startRecording(); if ((targetUrl = attr.getValue(TARGET_URL)) == null) { throw new IllegalStateException("The query element must contain a \"target\" attribute"); } //Sanityze target if (targetUrl.startsWith(WEBDAV_SCHEME)) targetUrl = "http://" + targetUrl.substring(WEBDAV_SCHEME.length()); if (!targetUrl.startsWith("http")) throw new SAXException("Illegal value for target, must be an http:// or webdav:// URL"); } else if (name.equals(SUBSTITUTE_TAG) && uri.equals(DASL_QUERY_NS)) { String parName = attr.getValue( DASL_QUERY_NS, SUBSTITUTE_TAG_NAME_ATTRIBUTE ); if ( parName == null ) { throw new IllegalStateException( "Substitute value elements must have a " + SUBSTITUTE_TAG_NAME_ATTRIBUTE + " attribute" ); } String substitute = this.parameters.getParameter( parName, null ); if (getLogger().isDebugEnabled()) { getLogger().debug( "SUBSTITUTE VALUE " + substitute ); } super.characters(substitute.toCharArray(), 0, substitute.length()); } else { super.startElement(uri, name, raw, attr); } } /** * Intercept the <dasl:query> end tag, convert buffered input to a String, build and execute the * DASL query. * * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String) */ public void endElement(String uri, String name, String raw) throws SAXException { String query; if (name.equals(QUERY_TAG) && uri.equals(DASL_QUERY_NS)) { DocumentFragment frag = this.endRecording(); try { Properties props = XMLUtils.createPropertiesForXML(false); props.put(OutputKeys.ENCODING, "ISO-8859-1"); query = XMLUtils.serializeNode(frag, props); // Perform the DASL query this.performSearchMethod(query); } catch (ProcessingException e) { throw new SAXException("Unable to fetch the query data:", e); } } else if (name.equals(SUBSTITUTE_TAG) && uri.equals(DASL_QUERY_NS)) { //Do nothing!!!! } else { super.endElement(uri, name, raw); } } protected void performSearchMethod(String query) throws SAXException { OptionsMethod optionsMethod = null; SearchMethod searchMethod = null; try { DOMStreamer propertyStreamer = new DOMStreamer(this.xmlConsumer); optionsMethod = new OptionsMethod(this.targetUrl); searchMethod = new SearchMethod(this.targetUrl, query); HttpURL url = new HttpURL(this.targetUrl); HttpState state = new HttpState(); state.setCredentials(null, new UsernamePasswordCredentials( url.getUser(), url.getPassword())); HttpConnection conn = new HttpConnection(url.getHost(), url.getPort()); // eventcaching stuff SourceValidity extraValidity = makeWebdavEventValidity(url); if(extraValidity!=null && m_validity!=null) m_validity.add(extraValidity); // end eventcaching stuff WebdavResource resource = new WebdavResource(new HttpURL(this.targetUrl)); if(!resource.exists()) { throw new SAXException("The WebDAV resource don't exist"); } optionsMethod.execute(state, conn); if(!optionsMethod.isAllowed("SEARCH")) { throw new SAXException("The server doesn't support the SEARCH method"); } int httpstatus = searchMethod.execute(state, conn); this.contentHandler.startElement(DASL_QUERY_NS, RESULT_ROOT_TAG, PREFIX + ":" + RESULT_ROOT_TAG, XMLUtils.EMPTY_ATTRIBUTES); // something might have gone wrong, report it // 207 = multistatus webdav response if(httpstatus != 207) { this.contentHandler.startElement(DASL_QUERY_NS, ERROR_ROOT_TAG, PREFIX + ":" + ERROR_ROOT_TAG, XMLUtils.EMPTY_ATTRIBUTES); // dump whatever the server said propertyStreamer.stream(searchMethod.getResponseDocument()); this.contentHandler.endElement(DASL_QUERY_NS, ERROR_ROOT_TAG, PREFIX + ":" + ERROR_ROOT_TAG); } else { // show results Enumeration enumeration = searchMethod.getAllResponseURLs(); while (enumeration.hasMoreElements()) { String path = (String) enumeration.nextElement(); Enumeration properties = searchMethod.getResponseProperties(path); AttributesImpl attr = new AttributesImpl(); attr.addAttribute(DASL_QUERY_NS, PATH_NODE_NAME, PREFIX + ":" + PATH_NODE_NAME, "CDATA",path); this.contentHandler.startElement(DASL_QUERY_NS, RESOURCE_NODE_NAME, PREFIX + ":" + RESOURCE_NODE_NAME, attr); while(properties.hasMoreElements()) { BaseProperty metadata = (BaseProperty) properties.nextElement(); Element propertyElement = metadata.getElement(); propertyStreamer.stream(propertyElement); } this.contentHandler.endElement(DASL_QUERY_NS, RESOURCE_NODE_NAME, PREFIX + ":" + RESOURCE_NODE_NAME); } } this.contentHandler.endElement(DASL_QUERY_NS, RESULT_ROOT_TAG, PREFIX + ":" + RESULT_ROOT_TAG); } catch (SAXException e) { throw new SAXException("Unable to fetch the query data:", e); } catch (HttpException e1) { this.getLogger().error("Unable to contact Webdav server", e1); throw new SAXException("Unable to connect with server: ", e1); } catch (IOException e2) { throw new SAXException("Unable to connect with server: ", e2); } catch (NullPointerException e) { throw new SAXException("Unable to fetch the query data:", e); } catch (Exception e) { throw new SAXException("Generic Error:", e); } finally { // cleanup if(searchMethod!=null) searchMethod.releaseConnection(); if(optionsMethod!=null) optionsMethod.releaseConnection(); } } /** * Helper method to do event caching * * @param methodurl The url to create the EventValidity for * @return an EventValidity object or null */ private SourceValidity makeWebdavEventValidity(HttpURL methodurl) { if(m_eventfactory == null) { return null; } SourceValidity evalidity = null; try { evalidity = new EventValidity(m_eventfactory.createEvent(methodurl)); if(getLogger().isDebugEnabled()) getLogger().debug("Created eventValidity for dasl: "+evalidity); } catch (Exception e) { if(getLogger().isErrorEnabled()) getLogger().error("could not create EventValidity!",e); } return evalidity; } public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException { super.setup(resolver, objectModel, src, par); if(m_eventfactory == null) { try { m_eventfactory = (WebDAVEventFactory)manager.lookup(WebDAVEventFactory.ROLE); } catch (Exception e) { if(getLogger().isErrorEnabled()) getLogger().error("Couldn't look up WebDAVEventFactory, event caching will not work!", e); } } } /** * Forget about previous aggregated validity object */ public void recycle() { super.recycle(); m_validity = null; } /** * generates the cachekey, which is the classname plus any possible COCOON parameters */ public Serializable getKey() { if(this.parameters.getNames().length == 0) { return getClass().getName(); } else { StringBuffer buf = new StringBuffer(); buf.append(getClass().getName()); // important for substitution // we don't know yet which ones are relevant, so include all String[] names = this.parameters.getNames(); for(int i=0; i<names.length; i++) { buf.append(";"); buf.append(names[i]); buf.append("="); try { buf.append(this.parameters.getParameter(names[i])); } catch (Exception e) { if(getLogger().isErrorEnabled()) getLogger().error("Could not read parameter '"+names[i]+"'!",e); } } return buf.toString(); } } /** * returns the validity which will be filled during processing of the requests */ public SourceValidity getValidity() { if(getLogger().isDebugEnabled()) getLogger().debug("getValidity() called!"); // dont do any caching when no event caching is set up if (m_eventfactory == null) { return null; } if (m_validity == null) { m_validity = new AggregatedValidity(); } return m_validity; } }