/* * eXist Open Source Native XML Database * Copyright (C) 2001-08 Wolfgang M. Meier * wolfgang@exist-db.org * http://exist-db.org * * 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: XQueryURLRewrite.java 14946 2011-07-23 09:25:49Z wolfgang_m $ */ package org.exist.http.urlrewrite; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import org.apache.log4j.Logger; import org.exist.source.Source; import org.exist.source.DBSource; import org.exist.source.SourceFactory; import org.exist.source.FileSource; 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.*; import org.exist.xquery.value.Sequence; import org.exist.xquery.value.Item; import org.exist.xquery.value.Type; import org.exist.xquery.value.NodeValue; import org.exist.Namespaces; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.dom.DocumentImpl; import org.exist.dom.BinaryDocument; import org.exist.xmldb.XmldbURI; import org.exist.security.*; import org.exist.security.xacml.AccessContext; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; import org.exist.storage.XQueryPool; import org.exist.storage.lock.Lock; import org.exist.storage.serializers.Serializer; import org.exist.util.MimeType; import org.exist.util.serializer.SAXSerializer; import org.exist.util.serializer.SerializerPool; import org.exist.http.servlets.HttpRequestWrapper; import org.exist.http.servlets.HttpResponseWrapper; import org.exist.http.Descriptor; import org.exist.external.org.apache.commons.io.output.ByteArrayOutputStream; import org.w3c.dom.Node; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.xmldb.api.base.Database; import org.xmldb.api.DatabaseManager; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.FilterChain; import javax.servlet.ServletOutputStream; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponseWrapper; import javax.xml.transform.OutputKeys; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.regex.Pattern; import java.util.regex.Matcher; /** * A filter to redirect HTTP requests. Similar to the popular UrlRewriteFilter, but * based on XQuery. * * The request is passed to an XQuery whose return value determines where the request will be * redirected to. An empty return value means the request will be passed through the filter * untouched. Otherwise, the query should return a single XML element, which will instruct the filter * how to further process the request. Details about the format can be found in the main documentation. * * The request is forwarded via {@link javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}. * Contrary to HTTP forwarding, there is no additional roundtrip to the client. It all happens on * the server. The client will not notice the redirect. * * Please read the <a href="http://exist-db.org/urlrewrite.html">documentation</a> for further information. */ public class XQueryURLRewrite implements Filter { private static final Logger LOG = Logger.getLogger(XQueryURLRewrite.class); public final static String DEFAULT_USER = "guest"; public final static String DEFAULT_PASS = "guest"; public final static String RQ_ATTR = "org.exist.forward"; public final static String RQ_ATTR_REQUEST_URI = "org.exist.forward.request-uri"; public final static String RQ_ATTR_SERVLET_PATH = "org.exist.forward.servlet-path"; public final static String RQ_ATTR_RESULT = "org.exist.forward.result"; public final static String DRIVER = "org.exist.xmldb.DatabaseImpl"; private final static Pattern NAME_REGEX = Pattern.compile("^.*/([^/]+)$", 0); private FilterConfig config; private final Map<String, ModelAndView> urlCache = new HashMap<String, ModelAndView>(); protected User defaultUser = null; protected BrokerPool pool; // path to the query private String query = null; private final Map<Object, Source> sources = new HashMap<Object, Source>(); private boolean checkModified = true; private boolean compiledCache = true; private RewriteConfig rewriteConfig; //@Override public void init(FilterConfig filterConfig) throws ServletException { // save FilterConfig for later use this.config = filterConfig; query = filterConfig.getInitParameter("xquery"); String opt = filterConfig.getInitParameter("check-modified"); if (opt != null) checkModified = opt != null && opt.equalsIgnoreCase("true"); opt = filterConfig.getInitParameter("compiled-cache"); if (opt != null) compiledCache = opt != null && opt.equalsIgnoreCase("true"); } //@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if (rewriteConfig == null) { configure(); rewriteConfig = new RewriteConfig(this); } long start = System.currentTimeMillis(); HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if (LOG.isTraceEnabled()) LOG.trace(request.getRequestURI()); Descriptor descriptor = Descriptor.getDescriptorSingleton(); if(descriptor != null && descriptor.requestsFiltered()) { String attr = (String) request.getAttribute("XQueryURLRewrite.forwarded"); if (attr == null) { // request = new HttpServletRequestWrapper(request, /*formEncoding*/ "utf-8" ); //logs the request if specified in the descriptor descriptor.doLogRequestInReplayLog(request); request.setAttribute("XQueryURLRewrite.forwarded", "true"); } } User user = defaultUser; try { configure(); checkCache(user); RequestWrapper modifiedRequest = new RequestWrapper(request); URLRewrite staticRewrite = rewriteConfig.lookup(modifiedRequest); if (staticRewrite != null && !staticRewrite.isControllerForward()) { modifiedRequest.setPaths(staticRewrite.resolve(modifiedRequest), staticRewrite.getPrefix()); if (LOG.isTraceEnabled()) LOG.trace("Forwarding to target: " + staticRewrite.getTarget()); staticRewrite.doRewrite(modifiedRequest, response, filterChain); } else { if (LOG.isTraceEnabled()) LOG.trace("Processing request URI: " + request.getRequestURI()); if (staticRewrite != null) { // fix the request URI staticRewrite.updateRequest(modifiedRequest); } // check if the request URI is already in the url cache ModelAndView modelView = getFromCache(modifiedRequest.getHeader("Host") + modifiedRequest.getRequestURI(), user); // no: create a new model and view configuration if (modelView == null) { modelView = new ModelAndView(); // Execute the query Sequence result = Sequence.EMPTY_SEQUENCE; DBBroker broker = null; try { broker = pool.get(user); modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI()); Properties outputProperties = new Properties(); outputProperties.setProperty(OutputKeys.INDENT, "yes"); outputProperties.setProperty(OutputKeys.ENCODING, "UTF-8"); outputProperties.setProperty(OutputKeys.MEDIA_TYPE, MimeType.XML_TYPE.getName()); result = runQuery(broker, modifiedRequest, response, staticRewrite, outputProperties); logResult(broker, result); if (response.isCommitted()) { return; } // process the query result if (result.getItemCount() == 1) { Item resource = result.itemAt(0); if (!Type.subTypeOf(resource.getType(), Type.NODE)) throw new ServletException("XQueryURLRewrite: urlrewrite query should return an element!"); Node node = ((NodeValue) resource).getNode(); if (node.getNodeType() == Node.DOCUMENT_NODE) node = ((Document) node).getDocumentElement(); if (node.getNodeType() != Node.ELEMENT_NODE) { //throw new ServletException("Redirect XQuery should return an XML element!"); response(broker, response, outputProperties, result); return; } Element elem = (Element) node; if (!(Namespaces.EXIST_NS.equals(elem.getNamespaceURI()))) { response(broker, response, outputProperties, result); return; // throw new ServletException("Redirect XQuery should return an element in namespace " + Namespaces.EXIST_NS); } if (Namespaces.EXIST_NS.equals(elem.getNamespaceURI()) && "dispatch".equals(elem.getLocalName())) { node = elem.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) { Element action = (Element) node; if ("view".equals(action.getLocalName())) { parseViews(modifiedRequest, action, modelView); } else if ("error-handler".equals(action.getLocalName())) { parseErrorHandlers(modifiedRequest, action, modelView); } else if ("cache-control".equals(action.getLocalName())) { String option = action.getAttribute("cache"); modelView.setUseCache("yes".equals(option)); } else { URLRewrite urw = parseAction(modifiedRequest, action); if (urw != null) modelView.setModel(urw); } } node = node.getNextSibling(); } if (modelView.getModel() == null) modelView.setModel(new PassThrough(elem, modifiedRequest)); } else if (Namespaces.EXIST_NS.equals(elem.getNamespaceURI()) && "ignore".equals(elem.getLocalName())) { modelView.setModel(new PassThrough(elem, modifiedRequest)); NodeList nl = elem.getElementsByTagNameNS(Namespaces.EXIST_NS, "cache-control"); if (nl.getLength() > 0) { elem = (Element) nl.item(0); String option = elem.getAttribute("cache"); modelView.setUseCache("yes".equals(option)); } } else { response(broker, response, outputProperties, result); return; } } else if (result.getItemCount() > 1) { response(broker, response, outputProperties, result); return; } if (modelView.useCache()) { synchronized (urlCache) { urlCache.put(modifiedRequest.getHeader("Host") + request.getRequestURI(), modelView); } } } finally { pool.release(broker); } // store the original request URI to org.exist.forward.request-uri modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI()); modifiedRequest.setAttribute(RQ_ATTR_SERVLET_PATH, request.getServletPath()); } if (LOG.isTraceEnabled()) LOG.trace("URLRewrite took " + (System.currentTimeMillis() - start) + "ms."); HttpServletResponse wrappedResponse = new CachingResponseWrapper(response, modelView.hasViews() || modelView.hasErrorHandlers()); if (modelView.getModel() == null) modelView.setModel(new PassThrough(modifiedRequest)); if (staticRewrite != null) { if (modelView.getModel().doResolve()) staticRewrite.rewriteRequest(modifiedRequest); else modelView.getModel().setAbsolutePath(modifiedRequest); } modifiedRequest.allowCaching(!modelView.hasViews()); doRewrite(modelView.getModel(), modifiedRequest, wrappedResponse, filterChain); int status = ((CachingResponseWrapper) wrappedResponse).getStatus(); if (status == HttpServletResponse.SC_NOT_MODIFIED) { response.flushBuffer(); } else if (status < 400) { if (modelView.hasViews()) applyViews(modelView.views, response, modifiedRequest, wrappedResponse); else ((CachingResponseWrapper) wrappedResponse).flush(); } else { // HTTP response code indicates an error if (modelView.hasErrorHandlers()) { applyViews(modelView.errorHandlers, response, modifiedRequest, wrappedResponse); } else { flushError(response, wrappedResponse); } } } // Sequence result; // if ((result = (Sequence) request.getAttribute(RQ_ATTR_RESULT)) != null) { // writeResults(response, broker, result); // } } catch (EXistException e) { LOG.error(e.getMessage(), e); throw new ServletException("An error occurred while retrieving query results: " + e.getMessage(), e); } catch (XPathException e) { LOG.error(e.getMessage(), e); throw new ServletException("An error occurred while executing the urlrewrite query: " + e.getMessage(), e); // } catch (SAXException e) { // throw new ServletException("Error while serializing results: " + e.getMessage(), e); } catch (Throwable e){ LOG.error(e.getMessage(), e); throw new ServletException("An error occurred: " + e.getMessage(), e); } } private void applyViews(List<URLRewrite> views, HttpServletResponse response, RequestWrapper modifiedRequest, HttpServletResponse wrappedResponse) throws UnsupportedEncodingException, IOException, ServletException { int status; for (int i = 0; i < views.size(); i++) { URLRewrite view = (URLRewrite) views.get(i); RequestWrapper wrappedReq = new RequestWrapper(modifiedRequest); wrappedReq.allowCaching(false); wrappedReq.setMethod("POST"); wrappedReq.setCharacterEncoding(wrappedResponse.getCharacterEncoding()); wrappedReq.setContentType(wrappedResponse.getContentType()); byte[] data = ((CachingResponseWrapper) wrappedResponse).getData(); if (data != null) wrappedReq.setData(data); wrappedResponse = new CachingResponseWrapper(response, i < views.size() - 1); doRewrite(view, wrappedReq, wrappedResponse, null); wrappedResponse.flushBuffer(); // catch errors in the view status = ((CachingResponseWrapper) wrappedResponse).getStatus(); if (status >= 400) { flushError(response, wrappedResponse); break; } } } private void response(DBBroker broker, HttpServletResponse response, Properties outputProperties, Sequence resultSequence) throws IOException { String encoding = outputProperties.getProperty(OutputKeys.ENCODING); ServletOutputStream sout = response.getOutputStream(); PrintWriter output = new PrintWriter(new OutputStreamWriter(sout, encoding)); if (!response.containsHeader("Content-Type")){ String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE); if (mimeType != null) { int semicolon = mimeType.indexOf(';'); if (semicolon != Constants.STRING_NOT_FOUND) { mimeType = mimeType.substring(0, semicolon); } response.setContentType(mimeType + "; charset=" + encoding); } } // response.addHeader( "pragma", "no-cache" ); // response.addHeader( "Cache-Control", "no-cache" ); Serializer serializer = broker.getSerializer(); serializer.reset(); SerializerPool serializerPool = SerializerPool.getInstance(); SAXSerializer sax = (SAXSerializer) serializerPool.borrowObject(SAXSerializer.class); try { sax.setOutput(output, outputProperties); serializer.setProperties(outputProperties); serializer.setSAXHandlers(sax, sax); serializer.toSAX(resultSequence, 1, resultSequence.getItemCount(), false); } catch (SAXException e) { throw new IOException(e.getMessage()); } finally { serializerPool.returnObject(sax); } output.flush(); output.close(); } private void flushError(HttpServletResponse response, HttpServletResponse wrappedResponse) throws IOException { byte[] data = ((CachingResponseWrapper) wrappedResponse).getData(); if (data != null) { response.setContentType(wrappedResponse.getContentType()); response.setCharacterEncoding(wrappedResponse.getCharacterEncoding()); response.getOutputStream().write(data); response.flushBuffer(); } } private ModelAndView getFromCache(String url, User user) throws EXistException { /* Make sure we have a broker *before* we synchronize on urlCache or we may run * into a deadlock situation (with method checkCache) */ DBBroker broker = null; try { broker = pool.get(user); synchronized (urlCache) { return urlCache.get(url); } } finally { pool.release(broker); } } private void checkCache(User user) throws EXistException { if (checkModified) { // check if any of the currently used sources has been updated // if yes, clear the cache DBBroker broker = null; try { broker = pool.get(user); synchronized (urlCache) { for (Source source : sources.values()) { if (source instanceof DBSource) { // Check if the XQuery source changed. If yes, clear all caches. if (source.isValid(broker) != Source.VALID) { clearCaches(); break; } } else { if (source.isValid((DBBroker)null) != Source.VALID) { clearCaches(); break; } } } } } finally { pool.release(broker); } } } protected void clearCaches() throws EXistException { DBBroker broker = null; try { broker = pool.get(defaultUser); synchronized (urlCache) { urlCache.clear(); sources.clear(); } } finally { pool.release(broker); } } /** * Process a rewrite action. Method checks if the target path is mapped * to another action in controller-config.xml. If yes, replaces the current action * with the new action. * * @param action * @param request * @param response * @param filterChain * @throws IOException * @throws ServletException */ protected void doRewrite(URLRewrite action, RequestWrapper request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { if (action.getTarget() != null && !(action instanceof Redirect)) { String uri = action.resolve(request); URLRewrite staticRewrite = rewriteConfig.lookup(uri, request.getServerName(), true); if (staticRewrite != null) { staticRewrite.copyFrom(action); action = staticRewrite; RequestWrapper modifiedRequest = new RequestWrapper(request); modifiedRequest.setPaths(uri, action.getPrefix()); if (LOG.isTraceEnabled()) LOG.trace("Forwarding to : " + action.toString() + " url: " + action.getURI()); request = modifiedRequest; } } action.prepareRequest(request); action.doRewrite(request, response, filterChain); } protected FilterConfig getConfig() { return config; } private URLRewrite parseAction(HttpServletRequest request, Element action) throws ServletException { URLRewrite rewrite = null; if ("forward".equals(action.getLocalName())) { rewrite = new PathForward(config, action, request.getRequestURI()); } else if ("redirect".equals(action.getLocalName())) { rewrite = new Redirect(action, request.getRequestURI()); // } else if ("call".equals(action.getLocalName())) { // rewrite = new ModuleCall(action, queryContext, request.getRequestURI()); } return rewrite; } private void parseViews(HttpServletRequest request, Element view, ModelAndView modelView) throws ServletException { Node node = view.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) { URLRewrite urw = parseAction(request, (Element) node); if (urw != null) modelView.addView(urw); } node = node.getNextSibling(); } } private void parseErrorHandlers(HttpServletRequest request, Element view, ModelAndView modelView) throws ServletException { Node node = view.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) { URLRewrite urw = parseAction(request, (Element) node); if (urw != null) modelView.addErrorHandler(urw); } node = node.getNextSibling(); } } private void configure() throws ServletException { if (pool != null) return; try { Class<?> driver = Class.forName(DRIVER); Database database = (Database) driver.newInstance(); database.setProperty("create-database", "true"); DatabaseManager.registerDatabase(database); LOG.debug("Initialized database"); } catch(Exception e) { String errorMessage="Failed to initialize database driver"; LOG.error(errorMessage,e); throw new ServletException(errorMessage+": " + e.getMessage(), e); } String username = config.getInitParameter("user"); if(username == null) username = DEFAULT_USER; String password = config.getInitParameter("password"); if(password == null) password = DEFAULT_PASS; try { pool = BrokerPool.getInstance(); } catch (EXistException e) { throw new ServletException("Could not intialize db: " + e.getMessage(), e); } defaultUser = pool.getSecurityManager().getUser(username); if (!defaultUser.validate(password)) throw new ServletException("Could not initialize URL rewriting. Bad password for user " + username); } private void logResult(DBBroker broker, Sequence result) throws IOException, SAXException { if (LOG.isTraceEnabled() && result.getItemCount() > 0) { Serializer serializer = broker.getSerializer(); serializer.reset(); Item item = result.itemAt(0); if (Type.subTypeOf(item.getType(), Type.NODE)) { LOG.trace(serializer.serialize((NodeValue) item)); } } } //@Override public void destroy() { config = null; } private Sequence runQuery(DBBroker broker, RequestWrapper request, HttpServletResponse response, URLRewrite staticRewrite, Properties outputProperties) throws ServletException, XPathException, PermissionDeniedException { // Try to find the XQuery SourceInfo sourceInfo; String moduleLoadPath = config.getServletContext().getRealPath("."); String basePath = staticRewrite == null ? "." : staticRewrite.getTarget(); if (basePath == null) sourceInfo = getSource(broker, moduleLoadPath); else sourceInfo = findSource(request, broker, basePath); if (sourceInfo == null) return Sequence.EMPTY_SEQUENCE; // no controller found synchronized (urlCache) { sources.put(sourceInfo.source.getKey(), sourceInfo.source); } XQuery xquery = broker.getXQueryService(); XQueryPool xqyPool = xquery.getXQueryPool(); CompiledXQuery compiled = null; if (compiledCache) compiled = xqyPool.borrowCompiledXQuery(broker, sourceInfo.source); XQueryContext queryContext; if (compiled == null) { queryContext = xquery.newContext(AccessContext.REST); } else { queryContext = compiled.getContext(); } // Find correct module load path queryContext.setModuleLoadPath(sourceInfo.moduleLoadPath); declareVariables(queryContext, sourceInfo, staticRewrite, basePath, request, response); if (compiled == null) { try { compiled = xquery.compile(queryContext, sourceInfo.source); } catch (IOException e) { throw new ServletException("Failed to read query from " + query, e); } } // This used by controller.xql only ? // String xdebug = request.getParameter("XDEBUG_SESSION_START"); // if (xdebug != null) // compiled.getContext().setDebugMode(true); // outputProperties.put("base-uri", collectionURI.toString()); try { return xquery.execute(compiled, null, outputProperties); } finally { xqyPool.returnCompiledXQuery(sourceInfo.source, compiled); } } protected String adjustPathForSourceLookup(String basePath, String path) { LOG.trace("request path=" + path); if(basePath.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX) && path.startsWith(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""))) { path = path.replace(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""), ""); } else if(path.startsWith("/db/")) { path = path.substring(4); } if(path.startsWith("/")) { path = path.substring(1); } LOG.trace("adjusted request path=" + path); return path; } private SourceInfo findSource(HttpServletRequest request, DBBroker broker, String basePath) throws ServletException { String requestURI = request.getRequestURI(); String path = requestURI.substring(request.getContextPath().length()); LOG.trace("basePath=" + basePath); path = adjustPathForSourceLookup(basePath, path); String[] components = path.split("/"); SourceInfo sourceInfo = null; if (basePath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) { LOG.trace("Looking for controller.xql in the database, starting from: " + basePath); try { XmldbURI locationUri = XmldbURI.xmldbUriFor(basePath); Collection collection = broker.openCollection(locationUri, Lock.READ_LOCK); if (collection == null) { LOG.warn("Controller base collection not found: " + basePath); return null; } Collection subColl = collection; DocumentImpl controllerDoc = null; for (int i = 0; i < components.length; i++) { DocumentImpl doc = null; try { if (components[i].length() > 0 && subColl.hasChildCollection(XmldbURI.createInternal(components[i]))) { XmldbURI newSubCollURI = subColl.getURI().append(components[i]); LOG.trace("Inspecting sub-collection: " + newSubCollURI); subColl = broker.openCollection(newSubCollURI, Lock.READ_LOCK); if (subColl != null) { LOG.trace("Looking for controller.xql in " + subColl.getURI()); XmldbURI docUri = subColl.getURI().append("controller.xql"); doc = broker.getXMLResource(docUri, Lock.READ_LOCK); if (doc != null) controllerDoc = doc; } else break; } else { break; } } catch (PermissionDeniedException e) { LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: " + e.getMessage(), e); break; } catch (Exception e) { LOG.debug("Bad collection URI: " + path); break; } finally { if (doc != null && controllerDoc == null) doc.getUpdateLock().release(Lock.READ_LOCK); if (subColl != null && subColl != collection) subColl.getLock().release(Lock.READ_LOCK); } } collection.getLock().release(Lock.READ_LOCK); if (controllerDoc == null) { try { XmldbURI docUri = collection.getURI().append("controller.xql"); controllerDoc = broker.getXMLResource(docUri, Lock.READ_LOCK); } catch (PermissionDeniedException e) { LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: " + e.getMessage(), e); } } if (controllerDoc == null) { LOG.warn("XQueryURLRewrite controller could not be found"); return null; } if(LOG.isTraceEnabled()) { LOG.trace("Found controller file: " + controllerDoc.getURI()); } try { if (controllerDoc.getResourceType() != DocumentImpl.BINARY_FILE || !controllerDoc.getMetadata().getMimeType().equals("application/xquery")) { LOG.warn("XQuery resource: " + query + " is not an XQuery or " + "declares a wrong mime-type"); return null; } String controllerPath = controllerDoc.getCollection().getURI().getRawCollectionPath(); sourceInfo = new SourceInfo(new DBSource(broker, (BinaryDocument) controllerDoc, true), "xmldb:exist://" + controllerPath); sourceInfo.controllerPath = controllerPath.substring(locationUri.getCollectionPath().length()); return sourceInfo; } finally { if (controllerDoc != null) controllerDoc.getUpdateLock().release(Lock.READ_LOCK); } } catch (URISyntaxException e) { LOG.warn("Bad URI for base path: " + e.getMessage(), e); return null; } } else { LOG.trace("Looking for controller.xql in the filesystem, starting from: " + basePath); String realPath = config.getServletContext().getRealPath(basePath); File baseDir = new File(realPath); if (!baseDir.isDirectory()) { LOG.warn("Base path for XQueryURLRewrite does not point to a directory"); return null; } File controllerFile = null; File subDir = baseDir; for (int i = 0; i < components.length; i++) { if (components[i].length() > 0) { subDir = new File(subDir, components[i]); if (subDir.isDirectory()) { File cf = new File(subDir, "controller.xql"); if (cf.canRead()) controllerFile = cf; } else break; } } if (controllerFile == null) { File cf = new File(baseDir, "controller.xql"); if (cf.canRead()) controllerFile = cf; } if (controllerFile == null) { LOG.warn("XQueryURLRewrite controller could not be found"); return null; } if (LOG.isTraceEnabled()) LOG.trace("Found controller file: " + controllerFile.getAbsolutePath()); String parentPath = controllerFile.getParentFile().getAbsolutePath(); sourceInfo = new SourceInfo(new FileSource(controllerFile, "UTF-8", true), parentPath); sourceInfo.controllerPath = parentPath.substring(baseDir.getAbsolutePath().length()); // replace windows path separators sourceInfo.controllerPath = sourceInfo.controllerPath.replace('\\', '/'); return sourceInfo; } } private SourceInfo getSource(DBBroker broker, String moduleLoadPath) throws ServletException { SourceInfo sourceInfo; if (query.startsWith(XmldbURI.XMLDB_URI_PREFIX)) { // Is the module source stored in the database? try { XmldbURI locationUri = XmldbURI.xmldbUriFor(query); DocumentImpl sourceDoc = null; try { sourceDoc = broker.getXMLResource(locationUri.toCollectionPathURI(), Lock.READ_LOCK); if (sourceDoc == null) throw new ServletException("XQuery resource: " + query + " not found in database"); if (sourceDoc.getResourceType() != DocumentImpl.BINARY_FILE || !sourceDoc.getMetadata().getMimeType().equals("application/xquery")) throw new ServletException("XQuery resource: " + query + " is not an XQuery or " + "declares a wrong mime-type"); sourceInfo = new SourceInfo(new DBSource(broker, (BinaryDocument) sourceDoc, true), locationUri.toString()); } catch (PermissionDeniedException e) { throw new ServletException("permission denied to read module source from " + query); } finally { if(sourceDoc != null) sourceDoc.getUpdateLock().release(Lock.READ_LOCK); } } catch(URISyntaxException e) { throw new ServletException(e.getMessage(), e); } } else { try { sourceInfo = new SourceInfo(SourceFactory.getSource(broker, moduleLoadPath, query, true), moduleLoadPath); } catch (IOException e) { throw new ServletException("IO error while reading XQuery source: " + query); } catch (PermissionDeniedException e) { throw new ServletException("Permission denied while reading XQuery source: " + query); } } return sourceInfo; } private void declareVariables(XQueryContext context, SourceInfo sourceInfo, URLRewrite staticRewrite, String basePath, RequestWrapper request, HttpServletResponse response) throws XPathException { HttpRequestWrapper reqw = new HttpRequestWrapper(request, "UTF-8", "UTF-8", false); HttpResponseWrapper respw = new HttpResponseWrapper(response); // context.declareNamespace(RequestModule.PREFIX, // RequestModule.NAMESPACE_URI); context.declareVariable(RequestModule.PREFIX + ":request", reqw); context.declareVariable(ResponseModule.PREFIX + ":response", respw); context.declareVariable(SessionModule.PREFIX + ":session", reqw.getSession( false )); context.declareVariable("exist:controller", sourceInfo.controllerPath); context.declareVariable("exist:root", basePath); context.declareVariable("exist:context", request.getContextPath()); String prefix = staticRewrite == null ? null : staticRewrite.getPrefix(); context.declareVariable("exist:prefix", prefix == null ? "" : prefix); String path; if (sourceInfo.controllerPath.length() > 0 && !sourceInfo.controllerPath.equals("/")) path = request.getInContextPath().substring(sourceInfo.controllerPath.length()); else path = request.getInContextPath(); int p = path.lastIndexOf(';'); if(p != Constants.STRING_NOT_FOUND) path = path.substring(0, p); context.declareVariable("exist:path", path); String resource = ""; Matcher nameMatcher = NAME_REGEX.matcher(path); if (nameMatcher.matches()) { resource = nameMatcher.group(1); } context.declareVariable("exist:resource", resource); if (LOG.isTraceEnabled()) LOG.debug("\nexist:path = " + path + "\nexist:resource = " + resource + "\nexist:controller = " + sourceInfo.controllerPath); } private class ModelAndView { URLRewrite rewrite = null; List<URLRewrite> views = new LinkedList<URLRewrite>(); List<URLRewrite> errorHandlers = null; boolean useCache = false; private ModelAndView() { } public void setModel(URLRewrite model) { this.rewrite = model; } public URLRewrite getModel() { return rewrite; } public void addErrorHandler(URLRewrite handler) { if (errorHandlers == null) errorHandlers = new LinkedList<URLRewrite>(); errorHandlers.add(handler); } public void addView(URLRewrite view) { views.add(view); } public boolean hasViews() { return views.size() > 0; } public boolean hasErrorHandlers() { return errorHandlers != null && errorHandlers.size() > 0; } public boolean useCache() { return useCache; } public void setUseCache(boolean useCache) { this.useCache = useCache; } } private static class SourceInfo { Source source; String controllerPath = ""; String moduleLoadPath; private SourceInfo(Source source, String moduleLoadPath) { this.source = source; this.moduleLoadPath = moduleLoadPath; } } public static class RequestWrapper extends javax.servlet.http.HttpServletRequestWrapper { Map<String, List<String>> addedParams = new HashMap<String, List<String>>(); Map attributes = new HashMap(); ServletInputStream sis = null; BufferedReader reader = null; String contentType = null; int contentLength = 0; String characterEncoding = null; String method = null; String inContextPath = null; String servletPath; String basePath = null; boolean allowCaching = true; private void addNameValue(String name, String value, Map<String, List<String>> map) { List<String> values = map.get(name); if(values == null) { values = new ArrayList<String>(); } values.add(value); map.put(name, values); } protected RequestWrapper(HttpServletRequest request) { super(request); // copy parameters for(Map.Entry<String, String[]> param : (Set<Map.Entry<String, String[]>>)request.getParameterMap().entrySet()) { for(String paramValue : param.getValue()) { addNameValue(param.getKey(), paramValue, addedParams); } } /*for(Enumeration<String> e = request.getParameterNames(); e.hasMoreElements(); ) { String key = e.nextElement(); String[] value = request.getParameterValues(key); addedParams.put(key, value); }*/ contentType = request.getContentType(); } protected void allowCaching(boolean cache) { this.allowCaching = cache; } @Override public String getRequestURI() { return inContextPath == null ? super.getRequestURI() : getContextPath() + inContextPath; } public String getInContextPath() { if (inContextPath == null) return getRequestURI().substring(getContextPath().length()); return inContextPath; } public void setInContextPath(String path) { inContextPath = path; } @Override public String getMethod() { if (method == null) return super.getMethod(); return method; } public void setMethod(String method) { this.method = method; } /** * Change the requestURI and the servletPath * * @param requestURI the URI of the request without the context path * @param servletPath the servlet path */ public void setPaths(String requestURI, String servletPath) { this.inContextPath = requestURI; if (servletPath == null) this.servletPath = requestURI; else this.servletPath = servletPath; } public void setBasePath(String base) { this.basePath = base; } public String getBasePath() { return basePath; } /** * Change the base path of the request, e.g. if the original request pointed * to /fs/foo/baz, but the request should be forwarded to /foo/baz. * * @param base the base path to remove */ public void removePathPrefix(String base) { setPaths(getInContextPath().substring(base.length()), servletPath != null ? servletPath.substring(base.length()) : null); } @Override public String getServletPath() { return servletPath == null ? super.getServletPath() : servletPath; } @Override public String getPathInfo() { String path = getInContextPath(); String sp = getServletPath(); if (sp == null) return null; if (path.length() < sp.length()) { LOG.error("Internal error: servletPath = " + sp + " is longer than path = " + path); return null; } return path.length() == sp.length() ? null : path.substring(sp.length()); } protected void setData(byte[] data) { if (data == null) data = new byte[0]; contentLength = data.length; sis = new CachingServletInputStream(data); } public void addParameter(String name, String value) { addNameValue(name, value, addedParams); } @Override public String getParameter(String name) { List<String> paramValues = addedParams.get(name); if (paramValues != null && paramValues.size() > 0) return paramValues.get(0); return null; } @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> parameterMap = new HashMap<String, String[]>(); for(Entry<String, List<String>> param : addedParams.entrySet()) { List<String> values = param.getValue(); if(values != null) { parameterMap.put(param.getKey(), values.toArray(new String[values.size()])); } else { parameterMap.put(param.getKey(), new String[]{}); } } return parameterMap; } @Override public Enumeration<String> getParameterNames() { return Collections.enumeration(addedParams.keySet()); } @Override public String[] getParameterValues(String name) { List<String> values = addedParams.get(name); if(values != null) { return values.toArray(new String[values.size()]); } else { return null; } } @Override public ServletInputStream getInputStream() throws IOException { if (sis == null) return super.getInputStream(); return sis; } @Override public BufferedReader getReader() throws IOException { if (sis == null) return super.getReader(); if (reader == null) reader = new BufferedReader(new InputStreamReader(sis, getCharacterEncoding())); return reader; } @Override public String getContentType() { if (contentType == null) return super.getContentType(); return contentType; } protected void setContentType(String contentType) { this.contentType = contentType; } @Override public int getContentLength() { if (sis == null) return super.getContentLength(); return contentLength; } @Override public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException { this.characterEncoding = encoding; } @Override public String getCharacterEncoding() { if (characterEncoding == null) return super.getCharacterEncoding(); return characterEncoding; } @Override public String getHeader(String s) { if (s.equals("If-Modified-Since") && !allowCaching) return null; return super.getHeader(s); } @Override public long getDateHeader(String s) { if (s.equals("If-Modified-Since") && !allowCaching) return -1; return super.getDateHeader(s); } // public void setAttribute(String key, Object value) { // attributes.put(key, value); // } // // public Object getAttribute(String key) { // Object value = attributes.get(key); // if (value == null) // value = super.getAttribute(key); // return value; // } // // public Enumeration getAttributeNames() { // Vector v = new Vector(); // for (Enumeration e = super.getAttributeNames(); e.hasMoreElements();) { // v.add(e.nextElement()); // } // for (Iterator i = attributes.keySet().iterator(); i.hasNext();) { // v.add(i.next()); // } // return v.elements(); // } } private class CachingResponseWrapper extends HttpServletResponseWrapper { @SuppressWarnings("unused") protected HttpServletResponse origResponse; protected CachingServletOutputStream sos = null; protected PrintWriter writer = null; protected int status = HttpServletResponse.SC_OK; protected String contentType = null; protected boolean cache; public CachingResponseWrapper(HttpServletResponse servletResponse, boolean cache) { super(servletResponse); this.cache = cache; this.origResponse = servletResponse; } @Override public PrintWriter getWriter() throws IOException { if (!cache) return super.getWriter(); if (sos != null) throw new IOException("getWriter cannnot be called after getOutputStream"); sos = new CachingServletOutputStream(); if (writer == null) writer = new PrintWriter(new OutputStreamWriter(sos, getCharacterEncoding())); return writer; } @Override public ServletOutputStream getOutputStream() throws IOException { if (!cache) return super.getOutputStream(); if (writer != null) throw new IOException("getOutputStream cannnot be called after getWriter"); if (sos == null) sos = new CachingServletOutputStream(); return sos; } public byte[] getData() { return sos != null ? sos.getData() : null; } @Override public void setContentType(String type) { if (contentType != null) return; this.contentType = type; if (!cache) super.setContentType(type); } @Override public String getContentType() { return contentType != null ? contentType : super.getContentType(); } @Override public void setHeader(String name, String value) { if ("Content-Type".equals(name)) setContentType(value); else super.setHeader(name, value); } public int getStatus() { return status; } @Override public void setStatus(int i) { this.status = i; super.setStatus(i); } @Override public void setStatus(int i, String msg) { this.status = i; super.setStatus(i, msg); } @Override public void sendError(int i, String msg) throws IOException { this.status = i; super.sendError(i, msg); } @Override public void sendError(int i) throws IOException { this.status = i; super.sendError(i); } @Override public void setContentLength(int i) { if (!cache) super.setContentLength(i); } @Override public void flushBuffer() throws IOException { if (!cache) super.flushBuffer(); } public void flush() throws IOException { if (cache) { if (contentType != null) super.setContentType(contentType); } if (sos != null) { ServletOutputStream out = super.getOutputStream(); out.write(sos.getData()); out.flush(); } } } private class CachingServletOutputStream extends ServletOutputStream { protected ByteArrayOutputStream ostream = new ByteArrayOutputStream(512); protected byte[] getData() { return ostream.toByteArray(); } @Override public void write(int b) throws IOException { ostream.write(b); } @Override public void write(byte b[]) throws IOException { ostream.write(b); } @Override public void write(byte b[], int off, int len) throws IOException { ostream.write(b, off, len); } } private static class CachingServletInputStream extends ServletInputStream { protected ByteArrayInputStream istream; public CachingServletInputStream(byte[] data) { if (data == null) istream = new ByteArrayInputStream(new byte[0]); else istream = new ByteArrayInputStream(data); } @Override public int read() throws IOException { return istream.read(); } @Override public int read(byte b[]) throws IOException { return istream.read(b); } @Override public int read(byte b[], int off, int len) throws IOException { return istream.read(b, off, len); } @Override public int available() throws IOException { return istream.available(); } } }