Java tutorial
/* * 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$ */ 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.http.servlets.Authenticator; import org.exist.http.servlets.BasicAuthenticator; import org.exist.security.internal.web.HttpAccount; 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.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.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServlet; 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.*; import java.util.Map.Entry; import java.util.regex.Pattern; import java.util.regex.Matcher; import static java.nio.charset.StandardCharsets.UTF_8; /** * A servlet 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 extends HttpServlet { private static final Logger LOG = Logger.getLogger(XQueryURLRewrite.class); 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 RQ_ATTR_ERROR = "org.exist.forward.error"; public final static String DRIVER = "org.exist.xmldb.DatabaseImpl"; private final static Pattern NAME_REGEX = Pattern.compile("^.*/([^/]+)$", 0); private ServletConfig config; private final Map<String, ModelAndView> urlCache = Collections .synchronizedMap(new TreeMap<String, ModelAndView>()); protected Subject defaultUser = null; protected BrokerPool pool; // path to the query private String query = null; //private boolean checkModified = true; private boolean compiledCache = true; private RewriteConfig rewriteConfig; private Authenticator authenticator; @Override public void init(ServletConfig 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"); final String opt = filterConfig.getInitParameter("compiled-cache"); if (opt != null) { compiledCache = opt != null && opt.equalsIgnoreCase("true"); } } @Override protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { if (rewriteConfig == null) { configure(); rewriteConfig = new RewriteConfig(this); } final long start = System.currentTimeMillis(); final HttpServletRequest request = servletRequest; final HttpServletResponse response = servletResponse; if (LOG.isTraceEnabled()) { LOG.trace(request.getRequestURI()); } final Descriptor descriptor = Descriptor.getDescriptorSingleton(); if (descriptor != null && descriptor.requestsFiltered()) { final 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"); } } Subject user = defaultUser; Subject requestUser = HttpAccount.getUserFromServletRequest(request); if (requestUser != null) { user = requestUser; } else { // Secondly try basic authentication final String auth = request.getHeader("Authorization"); if (auth != null) { requestUser = authenticator.authenticate(request, response); if (requestUser != null) { user = requestUser; } } } try { configure(); //checkCache(user); final RequestWrapper modifiedRequest = new RequestWrapper(request); final 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); } 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(request.getHeader("Host") + request.getRequestURI(), user); if (LOG.isDebugEnabled()) { LOG.debug("Checked cache for URI: " + modifiedRequest.getRequestURI() + " original: " + request.getRequestURI()); } // 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()); final 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, modelView, staticRewrite, outputProperties); logResult(broker, result); if (response.isCommitted()) { return; } // process the query result if (result.getItemCount() == 1) { final 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())) { final 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())) { final String option = action.getAttribute("cache"); modelView.setUseCache("yes".equals(option)); } else { final URLRewrite urw = parseAction(modifiedRequest, action); if (urw != null) { modelView.setModel(urw); } } } node = node.getNextSibling(); } if (modelView.getModel() == null) { modelView.setModel(new PassThrough(config, elem, modifiedRequest)); } } else if (Namespaces.EXIST_NS.equals(elem.getNamespaceURI()) && "ignore".equals(elem.getLocalName())) { modelView.setModel(new PassThrough(config, elem, modifiedRequest)); final NodeList nl = elem.getElementsByTagNameNS(Namespaces.EXIST_NS, "cache-control"); if (nl.getLength() > 0) { elem = (Element) nl.item(0); final 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()) { LOG.debug("Caching request to " + request.getRequestURI()); 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."); } final HttpServletResponse wrappedResponse = new CachingResponseWrapper(response, modelView.hasViews() || modelView.hasErrorHandlers()); if (modelView.getModel() == null) { modelView.setModel(new PassThrough(config, 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); int status = ((CachingResponseWrapper) wrappedResponse).getStatus(); if (status == HttpServletResponse.SC_NOT_MODIFIED) { response.flushBuffer(); } else if (status < 400) { if (modelView.hasViews()) { applyViews(modelView, modelView.views, response, modifiedRequest, wrappedResponse); } else { ((CachingResponseWrapper) wrappedResponse).flush(); } } else { // HTTP response code indicates an error if (modelView.hasErrorHandlers()) { final byte[] data = ((CachingResponseWrapper) wrappedResponse).getData(); if (data != null) { modifiedRequest.setAttribute(RQ_ATTR_ERROR, new String(data, UTF_8)); } applyViews(modelView, 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 (final Throwable e) { LOG.error("Error while processing " + servletRequest.getRequestURI() + ": " + e.getMessage(), e); throw new ServletException("An error occurred while processing request to " + servletRequest.getRequestURI() + ": " + e.getMessage(), e); } } private void applyViews(ModelAndView modelView, List<URLRewrite> views, HttpServletResponse response, RequestWrapper modifiedRequest, HttpServletResponse currentResponse) throws IOException, ServletException { int status; HttpServletResponse wrappedResponse = currentResponse; for (int i = 0; i < views.size(); i++) { final URLRewrite view = (URLRewrite) views.get(i); // get data returned from last action byte[] data = ((CachingResponseWrapper) wrappedResponse).getData(); // determine request method to use for calling view String method = view.getMethod(); if (method == null) { method = "POST"; // default is POST } final RequestWrapper wrappedReq = new RequestWrapper(modifiedRequest); wrappedReq.allowCaching(false); wrappedReq.setMethod(method); wrappedReq.setBasePath(modifiedRequest.getBasePath()); wrappedReq.setCharacterEncoding(wrappedResponse.getCharacterEncoding()); wrappedReq.setContentType(wrappedResponse.getContentType()); if (data != null) { wrappedReq.setData(data); } wrappedResponse = new CachingResponseWrapper(response, true); doRewrite(view, wrappedReq, wrappedResponse); // catch errors in the view status = ((CachingResponseWrapper) wrappedResponse).getStatus(); if (status >= 400) { if (modelView != null && modelView.hasErrorHandlers()) { data = ((CachingResponseWrapper) wrappedResponse).getData(); final String msg = data == null ? "" : new String(data, UTF_8); modifiedRequest.setAttribute(RQ_ATTR_ERROR, msg); applyViews(null, modelView.errorHandlers, response, modifiedRequest, wrappedResponse); break; } else { flushError(response, wrappedResponse); } break; } else if (i == views.size() - 1) { ((CachingResponseWrapper) wrappedResponse).flush(); } } } private void response(DBBroker broker, HttpServletResponse response, Properties outputProperties, Sequence resultSequence) throws IOException { final String encoding = outputProperties.getProperty(OutputKeys.ENCODING); final ServletOutputStream sout = response.getOutputStream(); final PrintWriter output = new PrintWriter(new OutputStreamWriter(sout, encoding)); if (!response.containsHeader("Content-Type")) { String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE); if (mimeType != null) { final 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" ); final Serializer serializer = broker.getSerializer(); serializer.reset(); final SerializerPool serializerPool = SerializerPool.getInstance(); final 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, false); } catch (final SAXException e) { throw new IOException(e); } finally { serializerPool.returnObject(sax); } output.flush(); output.close(); } private void flushError(HttpServletResponse response, HttpServletResponse wrappedResponse) throws IOException { if (!response.isCommitted()) { final 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, Subject user) throws EXistException, ServletException, PermissionDeniedException { /* Make sure we have a broker *before* we synchronize on urlCache or we may run * into a deadlock situation (with method checkCache) */ final ModelAndView model = urlCache.get(url); if (model == null) { return null; } DBBroker broker = null; try { broker = pool.get(user); model.getSourceInfo().source.validate(broker.getSubject(), Permission.EXECUTE); if (model.getSourceInfo().source.isValid(broker) != Source.VALID) { ModelAndView removed = urlCache.remove(url); return null; } if (LOG.isDebugEnabled()) { LOG.debug("Using cached entry for " + url); } return model; } finally { pool.release(broker); } } // private void checkCache(Subject user) throws EXistException { // if (checkModified) { // // check if any of the currently used sources has been updated // DBBroker broker = null; // try { // broker = pool.get(user); // // for (Entry<ModelAndView, Source> entry : sources.entrySet() ) // if (entry.getValue().isValid(broker) != Source.VALID) // urlCache.remove(entry.getKey()); // // } finally { // pool.release(broker); // } // } // } protected void clearCaches() throws EXistException { urlCache.clear(); } /** * 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 * @throws IOException * @throws ServletException */ protected void doRewrite(URLRewrite action, RequestWrapper request, HttpServletResponse response) throws IOException, ServletException { if (action.getTarget() != null && !(action instanceof Redirect)) { final String uri = action.resolve(request); URLRewrite staticRewrite = rewriteConfig.lookup(uri, request.getServerName(), true, action); 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); } protected ServletConfig 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())) { final 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())) { final 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 { final Class<?> driver = Class.forName(DRIVER); final Database database = (Database) driver.newInstance(); database.setProperty("create-database", "true"); DatabaseManager.registerDatabase(database); LOG.debug("Initialized database"); } catch (final Exception e) { final String errorMessage = "Failed to initialize database driver"; LOG.error(errorMessage, e); throw new ServletException(errorMessage + ": " + e.getMessage(), e); } try { pool = BrokerPool.getInstance(); } catch (final EXistException e) { throw new ServletException("Could not intialize db: " + e.getMessage(), e); } defaultUser = pool.getSecurityManager().getGuestSubject(); final String username = config.getInitParameter("user"); if (username != null) { final String password = config.getInitParameter("password"); try { Subject user = pool.getSecurityManager().authenticate(username, password); if (user != null && user.isAuthenticated()) { defaultUser = user; } } catch (final AuthenticationException e) { LOG.error("User can not be authenticated (" + username + "), using default user."); } } authenticator = new BasicAuthenticator(pool); } private void logResult(DBBroker broker, Sequence result) throws IOException, SAXException { if (LOG.isTraceEnabled() && result.getItemCount() > 0) { final Serializer serializer = broker.getSerializer(); serializer.reset(); final 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 SourceInfo getSourceInfo(DBBroker broker, RequestWrapper request, URLRewrite staticRewrite) throws ServletException { final String moduleLoadPath = config.getServletContext().getRealPath("/"); final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget(); if (basePath == null) { return getSource(broker, moduleLoadPath); } else { return findSource(request, broker, basePath); } } private Sequence runQuery(DBBroker broker, RequestWrapper request, HttpServletResponse response, ModelAndView model, URLRewrite staticRewrite, Properties outputProperties) throws ServletException, XPathException, PermissionDeniedException { // Try to find the XQuery final SourceInfo sourceInfo = getSourceInfo(broker, request, staticRewrite); if (sourceInfo == null) { return Sequence.EMPTY_SEQUENCE; // no controller found } final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget(); final XQuery xquery = broker.getXQueryService(); final 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 (final IOException e) { throw new ServletException("Failed to read query from " + query, e); } } model.setSourceInfo(sourceInfo); // 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 { queryContext.runCleanupTasks(); xqyPool.returnCompiledXQuery(sourceInfo.source, compiled); } } protected String adjustPathForSourceLookup(String basePath, String path) { if (LOG.isTraceEnabled()) { 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); } if (LOG.isTraceEnabled()) { LOG.trace("adjusted request path=" + path); } return path; } private SourceInfo findSource(HttpServletRequest request, DBBroker broker, String basePath) throws ServletException { final String requestURI = request.getRequestURI(); String path = requestURI.substring(request.getContextPath().length()); if (LOG.isTraceEnabled()) { LOG.trace("basePath=" + basePath); } path = adjustPathForSourceLookup(basePath, path); final String[] components = path.split("/"); SourceInfo sourceInfo = null; if (basePath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) { if (LOG.isTraceEnabled()) { LOG.trace("Looking for controller.xql in the database, starting from: " + basePath); } try { final XmldbURI locationUri = XmldbURI.xmldbUriFor(basePath); final 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(broker, XmldbURI.createInternal(components[i]))) { final XmldbURI newSubCollURI = subColl.getURI().append(components[i]); if (LOG.isTraceEnabled()) { LOG.trace("Inspecting sub-collection: " + newSubCollURI); } subColl = broker.openCollection(newSubCollURI, Lock.READ_LOCK); if (subColl != null) { if (LOG.isTraceEnabled()) { LOG.trace("Looking for controller.xql in " + subColl.getURI()); } final XmldbURI docUri = subColl.getURI().append("controller.xql"); doc = broker.getXMLResource(docUri, Lock.READ_LOCK); if (doc != null) { if (controllerDoc != null) { controllerDoc.getUpdateLock().release(Lock.READ_LOCK); } controllerDoc = doc; } } else { break; } } else { break; } } catch (final PermissionDeniedException e) { LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: " + e.getMessage(), e); break; } catch (final 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 { final XmldbURI docUri = collection.getURI().append("controller.xql"); controllerDoc = broker.getXMLResource(docUri, Lock.READ_LOCK); } catch (final 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 for path: " + path); return null; } if (LOG.isTraceEnabled()) { LOG.trace("Found controller file: " + controllerDoc.getURI()); } try { if (controllerDoc.getResourceType() != DocumentImpl.BINARY_FILE || !"application/xquery".equals(controllerDoc.getMetadata().getMimeType())) { LOG.warn("XQuery resource: " + query + " is not an XQuery or " + "declares a wrong mime-type"); return null; } final 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 (final URISyntaxException e) { LOG.warn("Bad URI for base path: " + e.getMessage(), e); return null; } catch (final PermissionDeniedException e) { LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: " + e.getMessage(), e); return null; } } else { if (LOG.isTraceEnabled()) { LOG.trace("Looking for controller.xql in the filesystem, starting from: " + basePath); } final String realPath = config.getServletContext().getRealPath(basePath); final 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()); } final 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 { final 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 || !"application/xquery".equals(sourceDoc.getMetadata().getMimeType())) { 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 (final PermissionDeniedException e) { throw new ServletException("permission denied to read module source from " + query); } finally { if (sourceDoc != null) { sourceDoc.getUpdateLock().release(Lock.READ_LOCK); } } } catch (final URISyntaxException e) { throw new ServletException(e.getMessage(), e); } } else { try { sourceInfo = new SourceInfo(SourceFactory.getSource(broker, moduleLoadPath, query, true), moduleLoadPath); } catch (final IOException e) { throw new ServletException("IO error while reading XQuery source: " + query); } catch (final 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 { final HttpRequestWrapper reqw = new HttpRequestWrapper(request, "UTF-8", "UTF-8", false); final 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); request.setAttribute("$exist:controller", sourceInfo.controllerPath); context.declareVariable("exist:root", basePath); request.setAttribute("$exist:root", basePath); context.declareVariable("exist:context", request.getContextPath()); request.setAttribute("$exist:context", request.getContextPath()); final String prefix = staticRewrite == null ? null : staticRewrite.getPrefix(); context.declareVariable("exist:prefix", prefix == null ? "" : prefix); request.setAttribute("$exist:prefix", prefix == null ? "" : prefix); String path; if (sourceInfo.controllerPath.length() > 0 && !"/".equals(sourceInfo.controllerPath)) { path = request.getInContextPath().substring(sourceInfo.controllerPath.length()); } else { path = request.getInContextPath(); } final int p = path.lastIndexOf(';'); if (p != Constants.STRING_NOT_FOUND) { path = path.substring(0, p); } context.declareVariable("exist:path", path); request.setAttribute("$exist:path", path); String resource = ""; final Matcher nameMatcher = NAME_REGEX.matcher(path); if (nameMatcher.matches()) { resource = nameMatcher.group(1); } context.declareVariable("exist:resource", resource); request.setAttribute("$exist:resource", resource); if (LOG.isDebugEnabled()) { 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; SourceInfo sourceInfo = null; private ModelAndView() { } public void setSourceInfo(SourceInfo sourceInfo) { this.sourceInfo = sourceInfo; } public SourceInfo getSourceInfo() { return sourceInfo; } 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 (final Map.Entry<String, String[]> param : (Set<Map.Entry<String, String[]>>) request .getParameterMap().entrySet()) { for (final 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() { String uri = inContextPath == null ? super.getRequestURI() : getContextPath() + inContextPath; // Strip jsessionid from uris. New behavior of jetty // see jira.codehaus.org/browse/JETTY-1146 final int pos = uri.indexOf(";jsessionid="); if (pos > 0) { uri = uri.substring(0, pos); } return uri; } 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() { final String path = getInContextPath(); final 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()); } @Override public String getPathTranslated() { final String pathInfo = getPathInfo(); if (pathInfo == null) { super.getPathTranslated(); } if (pathInfo == null) { return (null); } return super.getSession().getServletContext().getRealPath(pathInfo); } 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) { final List<String> paramValues = addedParams.get(name); if (paramValues != null && paramValues.size() > 0) { return paramValues.get(0); } return null; } @Override public Map<String, String[]> getParameterMap() { final Map<String, String[]> parameterMap = new HashMap<String, String[]>(); for (final Entry<String, List<String>> param : addedParams.entrySet()) { final 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) { final 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 ("If-Modified-Since".equals(s) && !allowCaching) { return null; } return super.getHeader(s); } @Override public long getDateHeader(String s) { if ("If-Modified-Since".equals(s) && !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) { final 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(); } } }