/* * eXist Open Source Native XML Database * Copyright (C) 2001-06 Wolfgang M. Meier * wolfgang@exist-db.org * http://exist.sourceforge.net * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * $id$ */ package org.exist.cocoon; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.net.URISyntaxException; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import javax.servlet.http.HttpServletRequest; 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.ParameterException; import org.apache.avalon.framework.parameters.Parameterizable; import org.apache.avalon.framework.parameters.Parameters; import org.apache.cocoon.ProcessingException; import org.apache.cocoon.caching.CacheableProcessingComponent; import org.apache.cocoon.environment.Context; import org.apache.cocoon.environment.ObjectModelHelper; import org.apache.cocoon.environment.Request; import org.apache.cocoon.environment.Response; import org.apache.cocoon.environment.Session; import org.apache.cocoon.environment.SourceResolver; import org.apache.cocoon.environment.http.HttpEnvironment; import org.apache.cocoon.generation.ServiceableGenerator; import org.apache.cocoon.xml.IncludeXMLConsumer; import org.apache.excalibur.source.Source; import org.apache.excalibur.source.SourceValidity; import org.apache.excalibur.source.impl.validity.AggregatedValidity; import org.apache.excalibur.source.impl.validity.ExpiresValidity; import org.exist.http.Descriptor; import org.exist.source.CocoonSource; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.storage.serializers.Serializer; import org.exist.xmldb.CollectionImpl; import org.exist.xmldb.XQueryService; import org.exist.xmldb.XmldbURI; import org.exist.xquery.Constants; import org.exist.xquery.XPathException; import org.exist.xquery.functions.request.RequestModule; import org.exist.xquery.functions.response.ResponseModule; import org.exist.xquery.functions.session.SessionModule; import org.exist.xquery.value.Sequence; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; 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; /** * A generator for Cocoon which reads an XQuery script, executes it and passes * the results into the Cocoon pipeline. * * The following optional attributes are accepted on the component declaration as default eXist settings: * <li><tt>collection</tt>: identifies the XML:DB root collection used to process * the request</li> * <li><tt>user</tt></li> * <li><tt>password</tt></li> *<li><tt>authen</tt></li>: if set to session, then use the user and password from the session. Otherwise use the parameter values. * <li><tt>create-session</tt>: if set to "true", indicates that an * HTTP session should be created upon the first invocation.</li> * <li><tt>expand-xincludes</tt></li> * <li><tt>cache-validity</tt>: if specified, the XQuery content is * cached until the specified delay expressed in milliseconds is elapsed * or until the XQuery file is modified. The identity of the cached content is * computed using the XQuery file URI and the list of all parameters passed to * the XQuery.</li> * * The component also accept default parameters that will be declared as implicit variables in the XQuery. * See below an example declaration of the XQueryGenerator component with default eXist settings, and an extra user-defined parameter: * * <map:generator logger="xmldb" name="xquery" * collection="xmldb:exist:///db/" * user="guest" * password="guest" * create-session="false" * expand-xincludes="false" authen="session" * cache-validity="-1" * src="org.exist.cocoon.XQueryGenerator"> * <parameter name="myProjectURI" value="/db/myproject"/> * </map:generator> * * These settings and parameters can be overriden on a per-pipeline basis with sitemap parameters, see below with default values and the extra user-defined parameter: * * <pre> * <map:parameter name="collection" value="xmldb:exist:///db"/> * <map:parameter name="user" value="guest"/> * <map:parameter name="password" value="guest"/> * <map:parameter name="create-session" value="false"/> * <map:parameter name="expand-xincludes" value="false"/> * <map:parameter name="cache-validity" value="-1quot;/> * <map:parameter name="myProjectURI" value="/db/myproject"/> * </pre> * * The last sitemap parameter overrides the value of the XQuery variable defined in the component parameters, * whereas others override the default eXist settings defined on the component attributes. * * @author wolf */ public class XQueryGenerator extends ServiceableGenerator implements Configurable, Parameterizable, CacheableProcessingComponent { public final static String DRIVER = "org.exist.xmldb.DatabaseImpl"; private Source inputSource = null; private Map objectModel = null; private boolean createSession; private boolean defaultCreateSession = false; private final static String CREATE_SESSION = "create-session"; private boolean expandXIncludes; private boolean defaultExpandXIncludes = false; private final static String EXPAND_XINCLUDES = "expand-xincludes"; private XmldbURI collectionURI; private XmldbURI defaultCollectionURI = XmldbURI.EMBEDDED_SERVER_URI.append(XmldbURI.ROOT_COLLECTION_URI); private final static String COLLECTION_URI = "collection"; private long cacheValidity; private long defaultCacheValidity = SourceValidity.INVALID; private final static String CACHE_VALIDITY = "cache-validity"; private String user; private String defaultUser = "guest"; private final static String USER = "user"; private String password; private String defaultPassword = "guest"; private final static String PASSWORD = "password"; private String authen; private String defaultAuthen = "session"; private final static String AUTHEN = "authen"; private Map optionalParameters; private Parameters componentParams; /* * (non-Javadoc) * * @see org.apache.cocoon.generation.AbstractGenerator#setup(org.apache.cocoon.environment.SourceResolver, * java.util.Map, java.lang.String, * org.apache.avalon.framework.parameters.Parameters) */ public void setup(SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws ProcessingException, SAXException, IOException { super.setup(resolver, objectModel, source, parameters); /* * We don't do this directly in parameterize() because setup() can be * called multiple times and optionalParameters needs resetting to forget * sitemap parameters that may have been removed inbetween * * The map must be sorted so that getKey() always returns the same * object for any given oder of parameters. */ this.optionalParameters = new TreeMap(); String paramNames[] = componentParams.getNames(); for (int i = 0; i < paramNames.length; i++) { String param = paramNames[i]; try { optionalParameters.put(param, componentParams.getParameter(param)); } catch (ParameterException e1) { // Cannot happen as we iterate through existing parameters } } this.objectModel = objectModel; this.inputSource = resolver.resolveURI(source); String paramCollectionURI = parameters.getParameter(COLLECTION_URI,null); if(paramCollectionURI != null) { try { this.collectionURI = XmldbURI.xmldbUriFor(paramCollectionURI); } catch(URISyntaxException e) { throw new ProcessingException("Invalid XmldbURI for parameter '"+COLLECTION_URI+"': "+e.getMessage(),e); } } else { this.collectionURI = this.defaultCollectionURI; } this.authen = parameters.getParameter(AUTHEN, this.defaultAuthen); this.user = parameters.getParameter(USER, this.defaultUser); this.password = parameters.getParameter(PASSWORD, this.defaultPassword); this.createSession = parameters.getParameterAsBoolean(CREATE_SESSION, this.defaultCreateSession); this.expandXIncludes = parameters.getParameterAsBoolean( EXPAND_XINCLUDES, this.defaultExpandXIncludes); this.cacheValidity = parameters.getParameterAsLong(CACHE_VALIDITY, defaultCacheValidity); paramNames = parameters.getNames(); for (int i = 0; i < paramNames.length; i++) { String param = paramNames[i]; if (!(param.equals(COLLECTION_URI) || param.equals(USER) || param.equals(PASSWORD) || param.equals(AUTHEN) || param.equals(CREATE_SESSION) || param .equals(EXPAND_XINCLUDES) || param.equals(CACHE_VALIDITY))) { this.optionalParameters.put(param, parameters .getParameter(param, "")); } } Context context = ObjectModelHelper.getContext(objectModel); String dbHome = context.getRealPath("WEB-INF"); try { Class driver = Class.forName(getDriverName()); Database database = (Database)driver.newInstance(); database.setProperty("create-database", "true"); database.setProperty("configuration", dbHome + File.separatorChar + "conf.xml"); DatabaseManager.registerDatabase(database); } catch(Exception e) { throw new ProcessingException("Failed to initialize database driver: " + e.getMessage(), e); } } /* * (non-Javadoc) * * @see org.apache.cocoon.generation.AbstractGenerator#recycle() */ public void recycle() { if (resolver != null) resolver.release(inputSource); inputSource = null; super.recycle(); } /** @see org.apache.cocoon.generation.Generator#generate() */ public void generate() throws IOException, SAXException, ProcessingException { ContentHandler includeContentHandler; if (inputSource == null) throw new ProcessingException("No input source"); Request request = ObjectModelHelper.getRequest(objectModel); Response response = ObjectModelHelper.getResponse(objectModel); Context context = ObjectModelHelper.getContext(objectModel); Session session = request.getSession(createSession); final String servletPath = request.getServletPath(); final String pathInfo = request.getPathInfo(); StringBuffer moduleLoadPathBuffer = new StringBuffer(servletPath); if (pathInfo != null) { moduleLoadPathBuffer.append(pathInfo); } int p = moduleLoadPathBuffer.lastIndexOf("/"); if (p != Constants.STRING_NOT_FOUND) { moduleLoadPathBuffer.delete(p, moduleLoadPathBuffer.length()); } final String moduleLoadPath = context.getRealPath(moduleLoadPathBuffer.toString()); XmldbURI baseUri = collectionURI; if(pathInfo!=null) { baseUri = baseUri.append(request.getPathInfo()); } // check if user and password can be read from the session if (session != null && authen.equals("session") && request.isRequestedSessionIdValid()) { String actualUser = getSessionAttribute(session, "user"); String actualPass = getSessionAttribute(session, "password"); user = actualUser == null ? null : String.valueOf(actualUser); password = actualPass == null ? null : String.valueOf(actualPass); } if (user == null) user = defaultUser; if (password == null) password = defaultPassword; try { Collection collection = DatabaseManager.getCollection( collectionURI.toString(), user, password); if (collection == null) { if (getLogger().isErrorEnabled()) getLogger().error( "Collection " + collectionURI + " not found"); throw new ProcessingException("Collection " + collectionURI + " not found"); } XQueryService service = (XQueryService) collection.getService( "XQueryService", "1.0"); service.setProperty(Serializer.GENERATE_DOC_EVENTS, "false"); service.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, expandXIncludes ? "yes" : "no"); service.setProperty("base-uri", baseUri.toString()); //service.setNamespace(RequestModule.PREFIX, RequestModule.NAMESPACE_URI); service.setModuleLoadPath(moduleLoadPath); if(!((CollectionImpl)collection).isRemoteCollection()) { HttpServletRequest httpRequest = (HttpServletRequest) objectModel .get(HttpEnvironment.HTTP_REQUEST_OBJECT); service.declareVariable(RequestModule.PREFIX + ":request", new CocoonRequestWrapper(request, httpRequest)); service.declareVariable(RequestModule.PREFIX + ":request", new CocoonRequestWrapper(request, httpRequest)); service.declareVariable(ResponseModule.PREFIX + ":response", new CocoonResponseWrapper(response)); if(session != null) service.declareVariable(SessionModule.PREFIX + ":session",new CocoonSessionWrapper(session)); includeContentHandler = this.contentHandler; } else { includeContentHandler = new IncludeXMLConsumer(this.contentHandler); } declareParameters(service); // String uri = inputSource.getURI(); log(request, service, this); ResourceSet result = service.execute(new CocoonSource(inputSource, true)); XMLResource resource; this.contentHandler.startDocument(); for (long i = 0; i < result.getSize(); i++) { resource = (XMLResource) result.getResource(i); resource.getContentAsSAX(includeContentHandler); } this.contentHandler.endDocument(); } catch (XMLDBException e) { throw new ProcessingException("XMLDBException occurred: " + e.getMessage(), e); } } private void declareParameters(XQueryService service) throws XMLDBException { for(Iterator i = optionalParameters.entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); service.declareVariable((String)entry.getKey(), entry.getValue()); } } private String getSessionAttribute(Session session, String attribute) { Object obj = session.getAttribute(attribute); if(obj == null) return null; if(obj instanceof Sequence) try { return ((Sequence)obj).getStringValue(); } catch (XPathException e) { return null; } return obj.toString(); } /** * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration) */ public void configure(Configuration config) throws ConfigurationException { String paramCollectionURI = config.getAttribute(COLLECTION_URI,null); if(paramCollectionURI != null) { try { this.defaultCollectionURI = XmldbURI.xmldbUriFor(paramCollectionURI); } catch(URISyntaxException e) { throw new ConfigurationException("Invalid XmldbURI for config attribute '"+COLLECTION_URI+"': "+e.getMessage(),e); } } this.defaultCreateSession = config.getAttributeAsBoolean(CREATE_SESSION, this.defaultCreateSession); this.defaultExpandXIncludes = config.getAttributeAsBoolean(EXPAND_XINCLUDES, this.defaultExpandXIncludes); this.defaultPassword = config.getAttribute(PASSWORD, this.defaultPassword); this.defaultUser = config.getAttribute(USER, this.defaultUser); this.defaultCacheValidity = config.getAttributeAsLong(CACHE_VALIDITY, this.defaultCacheValidity); } /** * @see org.apache.avalon.framework.parameters.Parameterizable#parameterize(org.apache.avalon.framework.parameters.Parameters) */ public void parameterize(Parameters params) throws ParameterException { this.componentParams = params; } public Serializable getKey() { StringBuffer key = new StringBuffer(); key.append(optionalParameters.toString()); key.append(inputSource.getURI()); return key.toString(); } public SourceValidity getValidity() { if (cacheValidity != SourceValidity.INVALID) { AggregatedValidity v = new AggregatedValidity(); if (inputSource.getValidity() != null) v.add(inputSource.getValidity()); v.add(new ExpiresValidity(cacheValidity)); return v; } return null; } HttpServletRequest getRequest() { return (HttpServletRequest) objectModel.get( "httprequest" ); } /** Static method to log the HTTP requests received by the {@link XQueryGenerator} */ private static void log(Request request, XQueryService service, XQueryGenerator generator) { Descriptor descriptor = Descriptor.getDescriptorSingleton(); if( descriptor.allowRequestLogging() ) { HttpServletRequest servletRequest = generator.getRequest(); descriptor.doLogRequestInReplayLog( servletRequest ); } } public String getDriverName() { return DRIVER; } }