<commit_before>/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2001-2008 The eXist Project
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *  
 *  $Id$
 */
package org.exist.http;

import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.Namespaces;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.BinaryDocument;
import org.exist.dom.DefaultDocumentSet;
import org.exist.dom.DocumentImpl;
import org.exist.dom.DocumentMetadata;
import org.exist.dom.MutableDocumentSet;
import org.exist.http.servlets.HttpRequestWrapper;
import org.exist.http.servlets.HttpResponseWrapper;
import org.exist.http.servlets.ResponseWrapper;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.xacml.AccessContext;
import org.exist.source.DBSource;
import org.exist.source.Source;
import org.exist.source.StringSource;
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.EXistOutputKeys;
import org.exist.storage.serializers.Serializer;
import org.exist.storage.txn.TransactionException;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.MimeTable;
import org.exist.util.MimeType;
import org.exist.util.serializer.SAXSerializer;
import org.exist.util.serializer.SerializerPool;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;
import org.exist.xquery.XQueryContext;
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.AnyURIValue;
import org.exist.xquery.value.DateTimeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xupdate.Modification;
import org.exist.xupdate.XUpdateProcessor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.AttributesImpl;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.Date;
import java.util.Iterator;
import java.util.Properties;

/**
 * 
 * @author wolf
 * 
 */

public class RESTServer {

	protected final static Logger LOG = Logger.getLogger(RESTServer.class);

	// Should we not obey the instance's defaults? /ljo
	protected final static Properties defaultProperties = new Properties();

	static {
		defaultProperties.setProperty(OutputKeys.INDENT, "yes");
		defaultProperties.setProperty(OutputKeys.ENCODING, "UTF-8");
		defaultProperties.setProperty(OutputKeys.MEDIA_TYPE, MimeType.XML_TYPE
				.getName());
		defaultProperties.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes");
		defaultProperties.setProperty(EXistOutputKeys.HIGHLIGHT_MATCHES,
				"elements");
		defaultProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI, "yes");
	}

	protected final static Properties defaultOutputKeysProperties = new Properties();

	static {
		defaultOutputKeysProperties.setProperty(OutputKeys.INDENT, "yes");
		defaultOutputKeysProperties.setProperty(OutputKeys.ENCODING, "UTF-8");
		defaultOutputKeysProperties.setProperty(OutputKeys.MEDIA_TYPE,
				MimeType.XML_TYPE.getName());
	}

	private final static String QUERY_ERROR_HEAD = "<html>" + "<head>"
			+ "<title>Query Error</title>" + "<style type=\"text/css\">"
			+ ".errmsg {" + "  border: 1px solid black;" + "  padding: 15px;"
			+ "  margin-left: 20px;" + "  margin-right: 20px;" + "}"
			+ "h1 { color: #C0C0C0; }" + ".path {" + "  padding-bottom: 10px;"
			+ "}" + ".high { " + "  color: #666699; " + "  font-weight: bold;"
			+ "}" + "</style>" + "</head>" + "<body>" + "<h1>XQuery Error</h1>";

	private String formEncoding; // TODO: we may be able to remove this
	// eventually, in favour of
	// HttpServletRequestWrapper being setup in
	// EXistServlet, currently used for doPost()
	// but perhaps could be used for other
	// Request Methods? - deliriumsky
	private String containerEncoding;
	private boolean useDynamicContentType;

	private SessionManager sessionManager;

	// Constructor
	public RESTServer(BrokerPool pool, String formEncoding,
			String containerEncoding, boolean useDynamicContentType) {
		this.formEncoding = formEncoding;
		this.containerEncoding = containerEncoding;
		this.useDynamicContentType = useDynamicContentType;
		this.sessionManager = new SessionManager(pool);
	}

	/**
	 * Handle GET request. In the simplest case just returns the document or
	 * binary resource specified in the path. If the path leads to a collection,
	 * a listing of the collection contents is returned. If it resolves to a
	 * binary resource with mime-type "application/xquery", this resource will
	 * be loaded and executed by the XQuery engine.
	 * 
	 * The method also recognizes a number of predefined parameters:
	 * 
	 * <ul>
	 * <li>_xpath or _query: if specified, the given query is executed on the
	 * current resource or collection.</li>
	 * 
	 * <li>_howmany: defines how many items from the query result will be
	 * returned.</li>
	 * 
	 * <li>_start: a start offset into the result set.</li>
	 * 
	 * <li>_wrap: if set to "yes", the query results will be wrapped into a
	 * exist:result element.</li>
	 * 
	 * <li>_indent: if set to "yes", the returned XML will be pretty-printed.
	 * </li>
	 * 
	 * <li>_source: if set to "yes" and a resource with mime-type
	 * "application/xquery" is requested then the xquery will not be executed,
	 * instead the source of the document will be returned. Must be enabled in
	 * descriptor.xml with the following syntax <xquery-app><allow-source><xquery
	 * path="/db/mycollection/myquery.xql"/></allow-source></xquery-app> </li>
	 * 
	 * <li>_xsl: an URI pointing to an XSL stylesheet that will be applied to
	 * the returned XML.</li>
	 * 
	 * @param broker
	 * @param request
	 * @param response
	 * @param path
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 * @throws NotFoundException
	 */
	public void doGet(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException,
			NotFoundException, IOException {

		// if required, set character encoding
		if (request.getCharacterEncoding() == null)
			request.setCharacterEncoding(formEncoding);

		String option;
		if ((option = request.getParameter("_release")) != null) {
			int sessionId = Integer.parseInt(option);
			sessionManager.release(sessionId);
			if (LOG.isDebugEnabled())
				LOG.debug("Released session " + sessionId);
			response.setStatus(HttpServletResponse.SC_OK);
			return;
		}

		// Process special parameters

		int howmany = 10;
		int start = 1;
		boolean wrap = true;
		boolean source = false;
		boolean cache = false;
		Properties outputProperties = new Properties(
				defaultOutputKeysProperties);

		String query = request.getParameter("_xpath");
		if (query == null)
			query = request.getParameter("_query");

		if ((option = request.getParameter("_howmany")) != null) {
			try {
				howmany = Integer.parseInt(option);
			} catch (NumberFormatException nfe) {
				throw new BadRequestException(
						"Parameter _howmany should be an int");
			}
		}
		if ((option = request.getParameter("_start")) != null) {
			try {
				start = Integer.parseInt(option);
			} catch (NumberFormatException nfe) {
				throw new BadRequestException(
						"Parameter _start should be an int");
			}
		}
		if ((option = request.getParameter("_wrap")) != null) {
			wrap = option.equals("yes");
		}
		if ((option = request.getParameter("_cache")) != null) {
			cache = option.equals("yes");
		}
		if ((option = request.getParameter("_indent")) != null) {
			outputProperties.setProperty(OutputKeys.INDENT, option);
		}
		if ((option = request.getParameter("_source")) != null) {
			source = option.equals("yes");
		}
		if ((option = request.getParameter("_session")) != null) {
			outputProperties
					.setProperty(Serializer.PROPERTY_SESSION_ID, option);
		}
		String stylesheet;
		if ((stylesheet = request.getParameter("_xsl")) != null) {
			if (stylesheet.equals("no")) {
				outputProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI,
						"no");
				outputProperties.remove(EXistOutputKeys.STYLESHEET);
				stylesheet = null;
			} else {
				outputProperties.setProperty(EXistOutputKeys.STYLESHEET,
						stylesheet);
			}
		} else {
			outputProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI, "yes");
		}
		LOG.debug("stylesheet = " + stylesheet);
		LOG.debug("query = " + query);
		String encoding;
		if ((encoding = request.getParameter("_encoding")) != null)
			outputProperties.setProperty(OutputKeys.ENCODING, encoding);
		else
			encoding = "UTF-8";

		String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);

		if (query != null) {
			// query parameter specified, search method does all the rest of the
			// work
			try {
				search(broker, query, path, howmany, start, outputProperties,
						wrap, cache, request, response);

			} catch (XPathException e) {
				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				if (MimeType.XML_TYPE.getName().equals(mimeType)) {
					writeXPathException(response, encoding, query, path, e);
				} else {
					writeXPathExceptionHtml(response, encoding, query, path, e);
				}
			}
			return;
		}
		// Process the request
		DocumentImpl resource = null;
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			// check if path leads to an XQuery resource
			String xquery_mime_type = MimeType.XQUERY_TYPE.getName();
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);

			if (null != resource
					&& (!xquery_mime_type.equals(resource.getMetadata()
							.getMimeType()) || // not xquery mime time
					(resource.getResourceType() != DocumentImpl.BINARY_FILE))) { // not
				// a
				// binary
				// file
				// return regular resource that is not an xquery
				writeResourceAs(resource, broker, stylesheet, encoding, null,
						outputProperties, response);
				return;
			}
			if (resource == null) { // could be request for a Collection
				// no document: check if path points to a collection
				Collection collection = broker.getCollection(pathUri);
				if (collection != null) {
					if (!collection.getPermissions().validate(broker.getUser(),
							Permission.READ))
						throw new PermissionDeniedException(
								"Not allowed to read collection");
					// return a listing of the collection contents
					writeCollection(response, encoding, broker, collection);
					return;
				} else if (source) {
					// didn't find regular resource, or user wants source
					// on a possible xquery resource that was not found
					throw new NotFoundException("Document " + path
							+ " not found");
				}
			}

			XmldbURI servletPath = pathUri;

			// if resource is still null, work up the url path to find an
			// xquery resource
			while (null == resource) {
				// traverse up the path looking for xquery objects
				servletPath = servletPath.removeLastSegment();
				if (servletPath == XmldbURI.EMPTY_URI)
					break;

				resource = broker.getXMLResource(servletPath, Lock.READ_LOCK);
				if (null != resource
						&& resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {
					break; // found a binary file with mime-type xquery

				} else if (null != resource) {
					// not an xquery resource. This means we have a path
					// that cannot contain an xquery object even if we keep
					// moving up the path, so bail out now
					throw new NotFoundException("Document " + path
							+ " not found");
				}
			}

			if (null == resource) { // path search failed
				throw new NotFoundException("Document " + path + " not found");
			}

			// found an XQuery resource, fixup request values
			String pathInfo = pathUri.trimFromBeginning(servletPath).toString();

			// Should we display the source of the XQuery or execute it
			Descriptor descriptor = Descriptor.getDescriptorSingleton();
			if (source) {
				// show the source

				// check are we allowed to show the xquery source -
				// descriptor.xml
				if ((null != descriptor) && descriptor.allowSourceXQuery(path)) {
					// TODO: change writeResourceAs to use a serializer
					// that will serialize xquery to syntax coloured
					// xhtml, replace the asMimeType parameter with a
					// method for specifying the serializer, or split
					// the code into two methods. - deliriumsky

					// Show the source of the XQuery
					writeResourceAs(resource, broker, stylesheet, encoding,
							MimeType.TEXT_TYPE.getName(), outputProperties,
							response);
				} else {
					// we are not allowed to show the source - query not
					// allowed in descriptor.xml
					// or descriptor not found, so assume source view not
					// allowed
					response
							.sendError(
									HttpServletResponse.SC_FORBIDDEN,
									"Permission to view XQuery source for: "
											+ path
											+ " denied. Must be explicitly defined in descriptor.xml");
					return;
				}
			} else { // Execute the XQuery
				try {
					executeXQuery(broker, resource, request, response,
							outputProperties, servletPath.toString(), pathInfo);
				} catch (XPathException e) {
					if (LOG.isDebugEnabled())
						LOG.debug(e.getMessage(), e);
					response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
					if (MimeType.XML_TYPE.getName().equals(mimeType)) {
						writeXPathException(response, encoding, query, path, e);
					} else {
						writeXPathExceptionHtml(response, encoding, query,
								path, e);
					}
				}
			}
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}
	}

	public void doHead(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException,
			NotFoundException, IOException {
		DocumentImpl resource = null;
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);
			if (resource == null) {
				throw new NotFoundException("Resource " + pathUri
						+ " not found");
			}
			if (!resource.getPermissions().validate(broker.getUser(),
					Permission.READ)) {
				throw new PermissionDeniedException(
						"Permission to read resource " + path + " denied");
			}
			DocumentMetadata metadata = resource.getMetadata();
			response.setContentType(metadata.getMimeType());
			response.setContentLength(resource.getContentLength());
			response.addDateHeader("Last-Modified", metadata.getLastModified());
			response.addDateHeader("Created", metadata.getCreated());
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}
	}

	/**
	 * Handles POST requests. If the path leads to a binary resource with
	 * mime-type "application/xquery", that resource will be read and executed
	 * by the XQuery engine. Otherwise, the request content is loaded and parsed
	 * as XML. It may either contain an XUpdate or a query request.
	 * 
	 * @param broker
	 * @param request
	 * @param response
	 * @param path
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 */
	public void doPost(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException, IOException {
		// if required, set character encoding
		if (request.getCharacterEncoding() == null)
			request.setCharacterEncoding(formEncoding);

		Properties outputProperties = new Properties(
				defaultOutputKeysProperties);
		XmldbURI pathUri = XmldbURI.create(path);
		DocumentImpl resource = null;

		String encoding = outputProperties.getProperty(OutputKeys.ENCODING);
		String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);
		try {
			// check if path leads to an XQuery resource.
			// if yes, the resource is loaded and the XQuery executed.
			String xquery_mime_type = MimeType.XQUERY_TYPE.getName();
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);

			XmldbURI servletPath = pathUri;

			// if resource is still null, work up the url path to find an
			// xquery resource
			while (null == resource) {
				// traverse up the path looking for xquery objects
				servletPath = servletPath.removeLastSegment();
				if (servletPath == XmldbURI.EMPTY_URI)
					break;

				resource = broker.getXMLResource(servletPath, Lock.READ_LOCK);
				if (null != resource
						&& resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {
					break; // found a binary file with mime-type xquery

				} else if (null != resource) {
					// not an xquery resource. This means we have a path
					// that cannot contain an xquery object even if we keep
					// moving up the path, so bail out now
					resource.getUpdateLock().release(Lock.READ_LOCK);
					resource = null;
					break;
				}
			}

			if (resource != null) {
				if (resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {

					// found an XQuery resource, fixup request values
					String pathInfo = pathUri.trimFromBeginning(servletPath)
							.toString();
					try {
						executeXQuery(broker, resource, request, response,
								outputProperties, servletPath.toString(),
								pathInfo.toString());
					} catch (XPathException e) {
						response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
						if (MimeType.XML_TYPE.getName().equals(mimeType)) {
							writeXPathException(response, encoding, null, path,
									e);
						} else {
							writeXPathExceptionHtml(response, encoding, null,
									path, e);
						}
					}
					return;
				}
			}
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}

		// third, normal POST: read the request content and check if
		// it is an XUpdate or a query request.
		int howmany = 10;
		int start = 1;
		boolean enclose = true;
		boolean cache = false;
		String mime = MimeType.XML_TYPE.getName();
		String query = null;
		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn transaction = transact.beginTransaction();
		try {
			String content = getRequestContent(request);
			InputSource src = new InputSource(new StringReader(content));
			DocumentBuilderFactory docFactory = DocumentBuilderFactory
					.newInstance();
			docFactory.setNamespaceAware(true);
			DocumentBuilder docBuilder;
			try {
				docBuilder = docFactory.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				LOG.warn(e);
				transact.abort(transaction);
				throw new BadRequestException(e.getMessage());
			}
			Document doc = docBuilder.parse(src);
			Element root = doc.getDocumentElement();
			String rootNS = root.getNamespaceURI();
			if (rootNS != null && rootNS.equals(Namespaces.EXIST_NS)) {
				if (root.getLocalName().equals("query")) {
					// process <query>xpathQuery</query>
					String option = root.getAttribute("start");
					if (option != null)
						try {
							start = Integer.parseInt(option);
						} catch (NumberFormatException e) {
						}
					option = root.getAttribute("max");
					if (option != null)
						try {
							howmany = Integer.parseInt(option);
						} catch (NumberFormatException e) {
						}

					option = root.getAttribute("enclose");
					if (option != null) {
						if (option.equals("no"))
							enclose = false;
					}

					option = root.getAttribute("mime");
					mime = MimeType.XML_TYPE.getName();
					if ((option != null) && (!option.equals(""))) {
						mime = option;
					}

					if ((option = root.getAttribute("cache")) != null) {
						cache = option.equals("yes");
					}

					if ((option = root.getAttribute("session")) != null
							&& option.length() > 0) {
						outputProperties.setProperty(
								Serializer.PROPERTY_SESSION_ID, option);
					}
					NodeList children = root.getChildNodes();
					for (int i = 0; i < children.getLength(); i++) {
						Node child = children.item(i);
						if (child.getNodeType() == Node.ELEMENT_NODE
								&& child.getNamespaceURI().equals(
										Namespaces.EXIST_NS)) {
							if (child.getLocalName().equals("text")) {
								StringBuffer buf = new StringBuffer();
								Node next = child.getFirstChild();
								while (next != null) {
									if (next.getNodeType() == Node.TEXT_NODE
											|| next.getNodeType() == Node.CDATA_SECTION_NODE)
										buf.append(next.getNodeValue());
									next = next.getNextSibling();
								}
								query = buf.toString();
							} else if (child.getLocalName()
									.equals("properties")) {
								Node node = child.getFirstChild();
								while (node != null) {
									if (node.getNodeType() == Node.ELEMENT_NODE
											&& node.getNamespaceURI().equals(
													Namespaces.EXIST_NS)
											&& node.getLocalName().equals(
													"property")) {
										Element property = (Element) node;
										String key = property
												.getAttribute("name");
										String value = property
												.getAttribute("value");
										LOG.debug(key + " = " + value);
										if (key != null && value != null)
											outputProperties.setProperty(key,
													value);
									}
									node = node.getNextSibling();
								}
							}
						}
					}
				}
				// execute query
				if (query != null) {
					String result;
					try {
						search(broker, query, path, howmany, start,
								outputProperties, enclose, cache, request,
								response);
					} catch (Exception e) {
						response.setStatus(HttpServletResponse.SC_ACCEPTED);
						result = e.getMessage();
						// TODO: write error response
					}

				} else {
					transact.abort(transaction);
					throw new BadRequestException("No query specified");
				}
			} else if (rootNS != null
					&& rootNS.equals(XUpdateProcessor.XUPDATE_NS)) {
				LOG.debug("Got xupdate request: " + content);
				MutableDocumentSet docs = new DefaultDocumentSet();
				Collection collection = broker.getCollection(pathUri);
				if (collection != null) {
					collection.allDocs(broker, docs, true, true);
				} else {
					DocumentImpl xupdateDoc = (DocumentImpl) broker
							.getXMLResource(pathUri);
					if (xupdateDoc != null) {
						if (!xupdateDoc.getPermissions().validate(
								broker.getUser(), Permission.READ)) {
							transact.abort(transaction);
							throw new PermissionDeniedException(
									"Not allowed to read collection");
						}
						docs.add(xupdateDoc);
					} else
						broker.getAllXMLResources(docs);
				}

				XUpdateProcessor processor = new XUpdateProcessor(broker, docs,
						AccessContext.REST);
				Modification modifications[] = processor.parse(new InputSource(
						new StringReader(content)));
				long mods = 0;
				for (int i = 0; i < modifications.length; i++) {
					mods += modifications[i].process(transaction);
					broker.flush();
				}
				transact.commit(transaction);

				// FD : Returns an XML doc
				writeXUpdateResult(response, encoding, mods);
				// END FD
			} else {
				transact.abort(transaction);
				throw new BadRequestException("Unknown XML root element: "
						+ root.getNodeName());
			}
		} catch (SAXException e) {
			transact.abort(transaction);
			Exception cause = e;
			if (e.getException() != null)
				cause = e.getException();
			LOG.debug("SAX exception while parsing request: "
					+ cause.getMessage(), cause);
			throw new BadRequestException(
					"SAX exception while parsing request: "
							+ cause.getMessage());
		} catch (ParserConfigurationException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"Parser exception while parsing request: " + e.getMessage());
		} catch (XPathException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"Query exception while parsing request: " + e.getMessage());
		} catch (IOException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"IO exception while parsing request: " + e.getMessage());
		} catch (EXistException e) {
			transact.abort(transaction);
			throw new BadRequestException(e.getMessage());
		} catch (LockException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		}
	}

	/**
	 * Creates an input source from a URL location with an optional known
	 * charset.
	 */
	private InputSource createInputSource(String charset, URI location)
			throws java.io.IOException {
		if (charset == null) {
			return new InputSource(location.toASCIIString());
		} else {
			InputSource source = new InputSource(new InputStreamReader(location
					.toURL().openStream(), charset));
			source.setSystemId(location.toASCIIString());
			return source;
		}
	}

	/**
	 * Handles PUT requests. The request content is stored as a new resource at
	 * the specified location. If the resource already exists, it is overwritten
	 * if the user has write permissions.
	 * 
	 * The resource type depends on the content type specified in the HTTP
	 * header. The content type will be looked up in the global mime table. If
	 * the corresponding mime type is not a know XML mime type, the resource
	 * will be stored as a binary resource.
	 * 
	 * @param broker
	 * @param tempFile
	 *            The temp file from which the PUT will get its content
	 * @param path
	 *            The path to which the file should be stored
	 * @param request
	 * @param response
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 */
	public void doPut(DBBroker broker, File tempFile, XmldbURI path,
			HttpServletRequest request, HttpServletResponse response)
			throws BadRequestException, PermissionDeniedException, IOException {
		if (tempFile == null)
			throw new BadRequestException("No request content found for PUT");

		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn transaction = transact.beginTransaction();
		try {
			XmldbURI docUri = path.lastSegment();
			XmldbURI collUri = path.removeLastSegment();

			if (docUri == null || collUri == null) {
				transact.abort(transaction);
				throw new BadRequestException("Bad path: " + path);
			}
			// TODO : use getOrCreateCollection() right now ?
			Collection collection = broker.getCollection(collUri);
			if (collection == null) {
				LOG.debug("creating collection " + collUri);
				collection = broker.getOrCreateCollection(transaction, collUri);
				broker.saveCollection(transaction, collection);
			}
			MimeType mime;
			String contentType = request.getContentType();
			String charset = null;
			if (contentType != null) {
				int semicolon = contentType.indexOf(';');
				if (semicolon > 0) {
					contentType = contentType.substring(0, semicolon).trim();
					int equals = contentType.indexOf('=', semicolon);
					if (equals > 0) {
						String param = contentType.substring(semicolon + 1,
								equals).trim();
						if (param.compareToIgnoreCase("charset=") == 0) {
							charset = param.substring(equals + 1).trim();
						}
					}
				}
				mime = MimeTable.getInstance().getContentType(contentType);
			} else {
				mime = MimeTable.getInstance().getContentTypeFor(docUri);
				if (mime != null)
					contentType = mime.getName();
			}
			if (mime == null)
				mime = MimeType.BINARY_TYPE;

			if (mime.isXMLType()) {
				URI url = tempFile.toURI();
				IndexInfo info = collection.validateXMLResource(transaction,
						broker, docUri, createInputSource(charset, url));
				info.getDocument().getMetadata().setMimeType(contentType);
				collection.store(transaction, broker, info, createInputSource(
						charset, url), false);
				response.setStatus(HttpServletResponse.SC_CREATED);
			} else {

				FileInputStream is = new FileInputStream(tempFile);
				collection.addBinaryResource(transaction, broker, docUri, is,
						contentType, (int) tempFile.length());
				is.close();
				response.setStatus(HttpServletResponse.SC_CREATED);
			}

			transact.commit(transaction);
		} catch (SAXParseException e) {
			transact.abort(transaction);
			throw new BadRequestException("Parsing exception at "
					+ e.getLineNumber() + "/" + e.getColumnNumber() + ": "
					+ e.toString());
		} catch (SAXException e) {
			transact.abort(transaction);
			Exception o = e.getException();
			if (o == null)
				o = e;
			throw new BadRequestException("Parsing exception: "
					+ o.getMessage());
		} catch (EXistException e) {
			transact.abort(transaction);
			throw new BadRequestException("Internal error: " + e.getMessage());
		} catch (TriggerException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		} catch (LockException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		}
		return;
	}

	public void doDelete(DBBroker broker, XmldbURI path,
			HttpServletResponse response) throws PermissionDeniedException,
			NotFoundException, IOException {
		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn txn = transact.beginTransaction();
		try {
			Collection collection = broker.getCollection(path);
			if (collection != null) {
				// remove the collection
				LOG.debug("removing collection " + path);
				broker.removeCollection(txn, collection);
				response.setStatus(HttpServletResponse.SC_OK);
			} else {
				DocumentImpl doc = (DocumentImpl) broker.getXMLResource(path);
				if (doc == null) {
					transact.abort(txn);
					throw new NotFoundException(
							"No document or collection found " + "for path: "
									+ path);
				} else {
					// remove the document
					LOG.debug("removing document " + path);
					if (doc.getResourceType() == DocumentImpl.BINARY_FILE)
						doc.getCollection().removeBinaryResource(txn, broker,
								path.lastSegment());
					else
						doc.getCollection().removeXMLResource(txn, broker,
								path.lastSegment());
					response.setStatus(HttpServletResponse.SC_OK);
				}
			}
			transact.commit(txn);
		} catch (TriggerException e) {
			transact.abort(txn);
			throw new PermissionDeniedException("Trigger failed: "
					+ e.getMessage());
		} catch (LockException e) {
			transact.abort(txn);
			throw new PermissionDeniedException("Could not acquire lock: "
					+ e.getMessage());
		} catch (TransactionException e) {
			transact.abort(txn);
			LOG.warn("Transaction aborted: " + e.getMessage(), e);
		}
	}

	private String getRequestContent(HttpServletRequest request)
			throws IOException {
		String encoding = request.getCharacterEncoding();
		if (encoding == null)
			encoding = "UTF-8";
		InputStream is = request.getInputStream();
		Reader reader = new InputStreamReader(is, encoding);
		StringWriter content = new StringWriter();
		char ch[] = new char[4096];
		int len = 0;
		while ((len = reader.read(ch)) > -1)
			content.write(ch, 0, len);
		String xml = content.toString();
		return xml;
	}

	/**
	 * TODO: pass request and response objects to XQuery.
	 * 
	 * @throws XPathException
	 */
	protected void search(DBBroker broker, String query, String path,
			int howmany, int start, Properties outputProperties, boolean wrap,
			boolean cache, HttpServletRequest request,
			HttpServletResponse response) throws BadRequestException,
			PermissionDeniedException, XPathException {
		String sessionIdParam = outputProperties
				.getProperty(Serializer.PROPERTY_SESSION_ID);
		if (sessionIdParam != null) {
			try {
				int sessionId = Integer.parseInt(sessionIdParam);
				if (sessionId > -1) {
					Sequence cached = sessionManager.get(query, sessionId);
					if (cached != null) {
						LOG.debug("Returning cached query result");
						writeResults(response, broker, cached, howmany, start,
								outputProperties, wrap);
					} else {
						LOG
								.debug("Cached query result not found. Probably timed out. Repeating query.");
					}
				}
			} catch (NumberFormatException e) {
				throw new BadRequestException(
						"Invalid session id passed in query request: "
								+ sessionIdParam);
			}
		}
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			Source source = new StringSource(query);
			XQuery xquery = broker.getXQueryService();
			XQueryPool pool = xquery.getXQueryPool();
			CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source);
			XQueryContext context;
			if (compiled == null)
				context = xquery.newContext(AccessContext.REST);
			else
				context = compiled.getContext();
			context.setStaticallyKnownDocuments(new XmldbURI[] { pathUri });
			context.setBaseURI(new AnyURIValue(pathUri.toString()));
			declareVariables(context, request, response);

			if (compiled == null)
				compiled = xquery.compile(context, source);
			else
				compiled.getContext().updateContext(context);
			context.checkOptions(outputProperties);
			try {
				long startTime = System.currentTimeMillis();
				Sequence resultSequence = xquery.execute(compiled, null);
				long queryTime = System.currentTimeMillis() - startTime;
				if (LOG.isDebugEnabled())
					LOG.debug("Found " + resultSequence.getItemCount() + " in "
							+ queryTime + "ms.");

				if (cache) {
					int sessionId = sessionManager.add(query, resultSequence);
					outputProperties.setProperty(
							Serializer.PROPERTY_SESSION_ID, Integer
									.toString(sessionId));
					if (!response.isCommitted())
						response.setIntHeader("X-Session-Id", sessionId);
				}

				writeResults(response, broker, resultSequence, howmany, start,
						outputProperties, wrap);
			} finally {
				pool.returnCompiledXQuery(source, compiled);
			}
		} catch (IOException e) {
			throw new BadRequestException(e.getMessage(), e);
		}
	}

	/**
	 * Pass the request, response and session objects to the XQuery context.
	 * 
	 * @param context
	 * @param request
	 * @param response
	 * @throws XPathException
	 */
	private HttpRequestWrapper declareVariables(XQueryContext context,
			HttpServletRequest request, HttpServletResponse response)
			throws XPathException {
		HttpRequestWrapper reqw = new HttpRequestWrapper(request, formEncoding,
				containerEncoding);
		ResponseWrapper 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());
		return reqw;
	}

	/**
	 * Directly execute an XQuery stored as a binary document in the database.
	 */
	private void executeXQuery(DBBroker broker, DocumentImpl resource,
			HttpServletRequest request, HttpServletResponse response,
			Properties outputProperties, String servletPath, String pathInfo)
			throws XPathException, BadRequestException {
		Source source = new DBSource(broker, (BinaryDocument) resource, true);
		XQuery xquery = broker.getXQueryService();
		XQueryPool pool = xquery.getXQueryPool();
		XQueryContext context;
		CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source);
		if (compiled == null) {
			// special header to indicate that the query is not returned from
			// cache
			response.setHeader("X-XQuery-Cached", "false");
			context = xquery.newContext(AccessContext.REST);
		} else {
			response.setHeader("X-XQuery-Cached", "true");
			context = compiled.getContext();
		}
		// TODO: don't hardcode this?
		context.setModuleLoadPath(XmldbURI.EMBEDDED_SERVER_URI.append(
				resource.getCollection().getURI()).toString());
		context.setStaticallyKnownDocuments(new XmldbURI[] { resource
				.getCollection().getURI() });
		HttpRequestWrapper reqw = declareVariables(context, request, response);
		reqw.setServletPath(servletPath);
		reqw.setPathInfo(pathInfo);
		if (compiled == null) {
			try {
				compiled = xquery.compile(context, source);
			} catch (IOException e) {
				throw new BadRequestException("Failed to read query from "
						+ resource.getURI(), e);
			}
		}
		context.checkOptions(outputProperties);
		try {
			Sequence result = xquery.execute(compiled, null);
			writeResults(response, broker, result, -1, 1, outputProperties,
					false);
		} finally {
			pool.returnCompiledXQuery(source, compiled);
		}
	}

	// writes out a resource, uses asMimeType as the specified mime-type or if
	// null uses the type of the resource
	private void writeResourceAs(DocumentImpl resource, DBBroker broker,
			String stylesheet, String encoding, String asMimeType,
			Properties outputProperties, HttpServletResponse response)
			throws BadRequestException, PermissionDeniedException, IOException {

		// Do we have permission to read the resource
		if (!resource.getPermissions().validate(broker.getUser(),
				Permission.READ)) {
			throw new PermissionDeniedException("Not allowed to read resource");
		}

		if (resource.getResourceType() == DocumentImpl.BINARY_FILE) {
			// binary resource

			if (asMimeType != null) { // was a mime-type specified?

				response.setContentType(asMimeType);
			} else {
				response.setContentType(resource.getMetadata().getMimeType());
			}
			OutputStream os = response.getOutputStream();
			broker.readBinaryResource((BinaryDocument) resource, os);
			os.flush();
		} else {
			// xml resource

			SAXSerializer sax = null;
			Serializer serializer = broker.getSerializer();
			serializer.reset();

			// Serialize the document
			try {
				sax = (SAXSerializer) SerializerPool.getInstance()
						.borrowObject(SAXSerializer.class);

				// use a stylesheet if specified in query parameters
				if (stylesheet != null) {
					serializer.setStylesheet(resource, stylesheet);
				}
				serializer.setProperties(outputProperties);
				serializer.prepareStylesheets(resource);

				if (asMimeType != null) { // was a mime-type specified?
					response.setContentType(asMimeType + "; charset="
							+ encoding);
				} else {
					if (serializer.isStylesheetApplied()
							|| serializer.hasXSLPi(resource) != null) {
						asMimeType = serializer.getStylesheetProperty(OutputKeys.MEDIA_TYPE);
						if (!useDynamicContentType || asMimeType == null)
							asMimeType = MimeType.HTML_TYPE.getName();
						LOG.debug("media-type: " + asMimeType);
						response.setContentType(asMimeType + "; charset="
								+ encoding);
					} else {
						asMimeType = resource.getMetadata().getMimeType();
						response.setContentType(asMimeType + "; charset="
								+ encoding);
					}
				}
				if (asMimeType.equals(MimeType.HTML_TYPE.getName())) {
					serializer.setProperty("method", "xhtml");
					serializer.setProperty("media-type", "text/html");
					serializer.setProperty("ident", "yes");
					serializer.setProperty("omit-xml-declaration", "no");
				}

				OutputStreamWriter writer = new OutputStreamWriter(response
						.getOutputStream(), encoding);
				sax.setOutput(writer, outputProperties);
				serializer.setSAXHandlers(sax, sax);

				serializer.toSAX(resource);

				writer.flush();
				writer.close();
			} catch (SAXException saxe) {
				LOG.warn(saxe);
				throw new BadRequestException("Error while serializing XML: "
						+ saxe.getMessage());
			} catch (TransformerConfigurationException e) {
				LOG.warn(e);
				throw new BadRequestException(e.getMessageAndLocation());
			} finally {
				if (sax != null) {
					SerializerPool.getInstance().returnObject(sax);
				}
			}
		}
	}

	/**<commit_msg>[bugfix] REST interface: use output properties set in the xsl if stylesheet is applied via processing instruction.<commit_after>/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2001-2008 The eXist Project
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *  
 *  $Id$
 */
package org.exist.http;

import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.Namespaces;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.BinaryDocument;
import org.exist.dom.DefaultDocumentSet;
import org.exist.dom.DocumentImpl;
import org.exist.dom.DocumentMetadata;
import org.exist.dom.MutableDocumentSet;
import org.exist.http.servlets.HttpRequestWrapper;
import org.exist.http.servlets.HttpResponseWrapper;
import org.exist.http.servlets.ResponseWrapper;
import org.exist.security.Permission;
import org.exist.security.PermissionDeniedException;
import org.exist.security.xacml.AccessContext;
import org.exist.source.DBSource;
import org.exist.source.Source;
import org.exist.source.StringSource;
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.EXistOutputKeys;
import org.exist.storage.serializers.Serializer;
import org.exist.storage.txn.TransactionException;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.MimeTable;
import org.exist.util.MimeType;
import org.exist.util.serializer.SAXSerializer;
import org.exist.util.serializer.SerializerPool;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.CompiledXQuery;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;
import org.exist.xquery.XQueryContext;
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.AnyURIValue;
import org.exist.xquery.value.DateTimeValue;
import org.exist.xquery.value.Sequence;
import org.exist.xupdate.Modification;
import org.exist.xupdate.XUpdateProcessor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.AttributesImpl;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.Date;
import java.util.Iterator;
import java.util.Properties;

/**
 * 
 * @author wolf
 * 
 */

public class RESTServer {

	protected final static Logger LOG = Logger.getLogger(RESTServer.class);

	// Should we not obey the instance's defaults? /ljo
	protected final static Properties defaultProperties = new Properties();

	static {
		defaultProperties.setProperty(OutputKeys.INDENT, "yes");
		defaultProperties.setProperty(OutputKeys.ENCODING, "UTF-8");
		defaultProperties.setProperty(OutputKeys.MEDIA_TYPE, MimeType.XML_TYPE
				.getName());
		defaultProperties.setProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes");
		defaultProperties.setProperty(EXistOutputKeys.HIGHLIGHT_MATCHES,
				"elements");
		defaultProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI, "yes");
	}

	protected final static Properties defaultOutputKeysProperties = new Properties();

	static {
		defaultOutputKeysProperties.setProperty(OutputKeys.INDENT, "yes");
		defaultOutputKeysProperties.setProperty(OutputKeys.ENCODING, "UTF-8");
		defaultOutputKeysProperties.setProperty(OutputKeys.MEDIA_TYPE,
				MimeType.XML_TYPE.getName());
	}

	private final static String QUERY_ERROR_HEAD = "<html>" + "<head>"
			+ "<title>Query Error</title>" + "<style type=\"text/css\">"
			+ ".errmsg {" + "  border: 1px solid black;" + "  padding: 15px;"
			+ "  margin-left: 20px;" + "  margin-right: 20px;" + "}"
			+ "h1 { color: #C0C0C0; }" + ".path {" + "  padding-bottom: 10px;"
			+ "}" + ".high { " + "  color: #666699; " + "  font-weight: bold;"
			+ "}" + "</style>" + "</head>" + "<body>" + "<h1>XQuery Error</h1>";

	private String formEncoding; // TODO: we may be able to remove this
	// eventually, in favour of
	// HttpServletRequestWrapper being setup in
	// EXistServlet, currently used for doPost()
	// but perhaps could be used for other
	// Request Methods? - deliriumsky
	private String containerEncoding;
	private boolean useDynamicContentType;

	private SessionManager sessionManager;

	// Constructor
	public RESTServer(BrokerPool pool, String formEncoding,
			String containerEncoding, boolean useDynamicContentType) {
		this.formEncoding = formEncoding;
		this.containerEncoding = containerEncoding;
		this.useDynamicContentType = useDynamicContentType;
		this.sessionManager = new SessionManager(pool);
	}

	/**
	 * Handle GET request. In the simplest case just returns the document or
	 * binary resource specified in the path. If the path leads to a collection,
	 * a listing of the collection contents is returned. If it resolves to a
	 * binary resource with mime-type "application/xquery", this resource will
	 * be loaded and executed by the XQuery engine.
	 * 
	 * The method also recognizes a number of predefined parameters:
	 * 
	 * <ul>
	 * <li>_xpath or _query: if specified, the given query is executed on the
	 * current resource or collection.</li>
	 * 
	 * <li>_howmany: defines how many items from the query result will be
	 * returned.</li>
	 * 
	 * <li>_start: a start offset into the result set.</li>
	 * 
	 * <li>_wrap: if set to "yes", the query results will be wrapped into a
	 * exist:result element.</li>
	 * 
	 * <li>_indent: if set to "yes", the returned XML will be pretty-printed.
	 * </li>
	 * 
	 * <li>_source: if set to "yes" and a resource with mime-type
	 * "application/xquery" is requested then the xquery will not be executed,
	 * instead the source of the document will be returned. Must be enabled in
	 * descriptor.xml with the following syntax <xquery-app><allow-source><xquery
	 * path="/db/mycollection/myquery.xql"/></allow-source></xquery-app> </li>
	 * 
	 * <li>_xsl: an URI pointing to an XSL stylesheet that will be applied to
	 * the returned XML.</li>
	 * 
	 * @param broker
	 * @param request
	 * @param response
	 * @param path
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 * @throws NotFoundException
	 */
	public void doGet(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException,
			NotFoundException, IOException {

		// if required, set character encoding
		if (request.getCharacterEncoding() == null)
			request.setCharacterEncoding(formEncoding);

		String option;
		if ((option = request.getParameter("_release")) != null) {
			int sessionId = Integer.parseInt(option);
			sessionManager.release(sessionId);
			if (LOG.isDebugEnabled())
				LOG.debug("Released session " + sessionId);
			response.setStatus(HttpServletResponse.SC_OK);
			return;
		}

		// Process special parameters

		int howmany = 10;
		int start = 1;
		boolean wrap = true;
		boolean source = false;
		boolean cache = false;
		Properties outputProperties = new Properties(
				defaultOutputKeysProperties);

		String query = request.getParameter("_xpath");
		if (query == null)
			query = request.getParameter("_query");

		if ((option = request.getParameter("_howmany")) != null) {
			try {
				howmany = Integer.parseInt(option);
			} catch (NumberFormatException nfe) {
				throw new BadRequestException(
						"Parameter _howmany should be an int");
			}
		}
		if ((option = request.getParameter("_start")) != null) {
			try {
				start = Integer.parseInt(option);
			} catch (NumberFormatException nfe) {
				throw new BadRequestException(
						"Parameter _start should be an int");
			}
		}
		if ((option = request.getParameter("_wrap")) != null) {
			wrap = option.equals("yes");
		}
		if ((option = request.getParameter("_cache")) != null) {
			cache = option.equals("yes");
		}
		if ((option = request.getParameter("_indent")) != null) {
			outputProperties.setProperty(OutputKeys.INDENT, option);
		}
		if ((option = request.getParameter("_source")) != null) {
			source = option.equals("yes");
		}
		if ((option = request.getParameter("_session")) != null) {
			outputProperties
					.setProperty(Serializer.PROPERTY_SESSION_ID, option);
		}
		String stylesheet;
		if ((stylesheet = request.getParameter("_xsl")) != null) {
			if (stylesheet.equals("no")) {
				outputProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI,
						"no");
				outputProperties.remove(EXistOutputKeys.STYLESHEET);
				stylesheet = null;
			} else {
				outputProperties.setProperty(EXistOutputKeys.STYLESHEET,
						stylesheet);
			}
		} else {
			outputProperties.setProperty(EXistOutputKeys.PROCESS_XSL_PI, "yes");
		}
		LOG.debug("stylesheet = " + stylesheet);
		LOG.debug("query = " + query);
		String encoding;
		if ((encoding = request.getParameter("_encoding")) != null)
			outputProperties.setProperty(OutputKeys.ENCODING, encoding);
		else
			encoding = "UTF-8";

		String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);

		if (query != null) {
			// query parameter specified, search method does all the rest of the
			// work
			try {
				search(broker, query, path, howmany, start, outputProperties,
						wrap, cache, request, response);

			} catch (XPathException e) {
				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				if (MimeType.XML_TYPE.getName().equals(mimeType)) {
					writeXPathException(response, encoding, query, path, e);
				} else {
					writeXPathExceptionHtml(response, encoding, query, path, e);
				}
			}
			return;
		}
		// Process the request
		DocumentImpl resource = null;
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			// check if path leads to an XQuery resource
			String xquery_mime_type = MimeType.XQUERY_TYPE.getName();
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);

			if (null != resource
					&& (!xquery_mime_type.equals(resource.getMetadata()
							.getMimeType()) || // not xquery mime time
					(resource.getResourceType() != DocumentImpl.BINARY_FILE))) { // not
				// a
				// binary
				// file
				// return regular resource that is not an xquery
				writeResourceAs(resource, broker, stylesheet, encoding, null,
						outputProperties, response);
				return;
			}
			if (resource == null) { // could be request for a Collection
				// no document: check if path points to a collection
				Collection collection = broker.getCollection(pathUri);
				if (collection != null) {
					if (!collection.getPermissions().validate(broker.getUser(),
							Permission.READ))
						throw new PermissionDeniedException(
								"Not allowed to read collection");
					// return a listing of the collection contents
					writeCollection(response, encoding, broker, collection);
					return;
				} else if (source) {
					// didn't find regular resource, or user wants source
					// on a possible xquery resource that was not found
					throw new NotFoundException("Document " + path
							+ " not found");
				}
			}

			XmldbURI servletPath = pathUri;

			// if resource is still null, work up the url path to find an
			// xquery resource
			while (null == resource) {
				// traverse up the path looking for xquery objects
				servletPath = servletPath.removeLastSegment();
				if (servletPath == XmldbURI.EMPTY_URI)
					break;

				resource = broker.getXMLResource(servletPath, Lock.READ_LOCK);
				if (null != resource
						&& resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {
					break; // found a binary file with mime-type xquery

				} else if (null != resource) {
					// not an xquery resource. This means we have a path
					// that cannot contain an xquery object even if we keep
					// moving up the path, so bail out now
					throw new NotFoundException("Document " + path
							+ " not found");
				}
			}

			if (null == resource) { // path search failed
				throw new NotFoundException("Document " + path + " not found");
			}

			// found an XQuery resource, fixup request values
			String pathInfo = pathUri.trimFromBeginning(servletPath).toString();

			// Should we display the source of the XQuery or execute it
			Descriptor descriptor = Descriptor.getDescriptorSingleton();
			if (source) {
				// show the source

				// check are we allowed to show the xquery source -
				// descriptor.xml
				if ((null != descriptor) && descriptor.allowSourceXQuery(path)) {
					// TODO: change writeResourceAs to use a serializer
					// that will serialize xquery to syntax coloured
					// xhtml, replace the asMimeType parameter with a
					// method for specifying the serializer, or split
					// the code into two methods. - deliriumsky

					// Show the source of the XQuery
					writeResourceAs(resource, broker, stylesheet, encoding,
							MimeType.TEXT_TYPE.getName(), outputProperties,
							response);
				} else {
					// we are not allowed to show the source - query not
					// allowed in descriptor.xml
					// or descriptor not found, so assume source view not
					// allowed
					response
							.sendError(
									HttpServletResponse.SC_FORBIDDEN,
									"Permission to view XQuery source for: "
											+ path
											+ " denied. Must be explicitly defined in descriptor.xml");
					return;
				}
			} else { // Execute the XQuery
				try {
					executeXQuery(broker, resource, request, response,
							outputProperties, servletPath.toString(), pathInfo);
				} catch (XPathException e) {
					if (LOG.isDebugEnabled())
						LOG.debug(e.getMessage(), e);
					response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
					if (MimeType.XML_TYPE.getName().equals(mimeType)) {
						writeXPathException(response, encoding, query, path, e);
					} else {
						writeXPathExceptionHtml(response, encoding, query,
								path, e);
					}
				}
			}
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}
	}

	public void doHead(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException,
			NotFoundException, IOException {
		DocumentImpl resource = null;
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);
			if (resource == null) {
				throw new NotFoundException("Resource " + pathUri
						+ " not found");
			}
			if (!resource.getPermissions().validate(broker.getUser(),
					Permission.READ)) {
				throw new PermissionDeniedException(
						"Permission to read resource " + path + " denied");
			}
			DocumentMetadata metadata = resource.getMetadata();
			response.setContentType(metadata.getMimeType());
			response.setContentLength(resource.getContentLength());
			response.addDateHeader("Last-Modified", metadata.getLastModified());
			response.addDateHeader("Created", metadata.getCreated());
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}
	}

	/**
	 * Handles POST requests. If the path leads to a binary resource with
	 * mime-type "application/xquery", that resource will be read and executed
	 * by the XQuery engine. Otherwise, the request content is loaded and parsed
	 * as XML. It may either contain an XUpdate or a query request.
	 * 
	 * @param broker
	 * @param request
	 * @param response
	 * @param path
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 */
	public void doPost(DBBroker broker, HttpServletRequest request,
			HttpServletResponse response, String path)
			throws BadRequestException, PermissionDeniedException, IOException {
		// if required, set character encoding
		if (request.getCharacterEncoding() == null)
			request.setCharacterEncoding(formEncoding);

		Properties outputProperties = new Properties(
				defaultOutputKeysProperties);
		XmldbURI pathUri = XmldbURI.create(path);
		DocumentImpl resource = null;

		String encoding = outputProperties.getProperty(OutputKeys.ENCODING);
		String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);
		try {
			// check if path leads to an XQuery resource.
			// if yes, the resource is loaded and the XQuery executed.
			String xquery_mime_type = MimeType.XQUERY_TYPE.getName();
			resource = broker.getXMLResource(pathUri, Lock.READ_LOCK);

			XmldbURI servletPath = pathUri;

			// if resource is still null, work up the url path to find an
			// xquery resource
			while (null == resource) {
				// traverse up the path looking for xquery objects
				servletPath = servletPath.removeLastSegment();
				if (servletPath == XmldbURI.EMPTY_URI)
					break;

				resource = broker.getXMLResource(servletPath, Lock.READ_LOCK);
				if (null != resource
						&& resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {
					break; // found a binary file with mime-type xquery

				} else if (null != resource) {
					// not an xquery resource. This means we have a path
					// that cannot contain an xquery object even if we keep
					// moving up the path, so bail out now
					resource.getUpdateLock().release(Lock.READ_LOCK);
					resource = null;
					break;
				}
			}

			if (resource != null) {
				if (resource.getResourceType() == DocumentImpl.BINARY_FILE
						&& xquery_mime_type.equals(resource.getMetadata()
								.getMimeType())) {

					// found an XQuery resource, fixup request values
					String pathInfo = pathUri.trimFromBeginning(servletPath)
							.toString();
					try {
						executeXQuery(broker, resource, request, response,
								outputProperties, servletPath.toString(),
								pathInfo.toString());
					} catch (XPathException e) {
						response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
						if (MimeType.XML_TYPE.getName().equals(mimeType)) {
							writeXPathException(response, encoding, null, path,
									e);
						} else {
							writeXPathExceptionHtml(response, encoding, null,
									path, e);
						}
					}
					return;
				}
			}
		} finally {
			if (resource != null)
				resource.getUpdateLock().release(Lock.READ_LOCK);
		}

		// third, normal POST: read the request content and check if
		// it is an XUpdate or a query request.
		int howmany = 10;
		int start = 1;
		boolean enclose = true;
		boolean cache = false;
		String mime = MimeType.XML_TYPE.getName();
		String query = null;
		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn transaction = transact.beginTransaction();
		try {
			String content = getRequestContent(request);
			InputSource src = new InputSource(new StringReader(content));
			DocumentBuilderFactory docFactory = DocumentBuilderFactory
					.newInstance();
			docFactory.setNamespaceAware(true);
			DocumentBuilder docBuilder;
			try {
				docBuilder = docFactory.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				LOG.warn(e);
				transact.abort(transaction);
				throw new BadRequestException(e.getMessage());
			}
			Document doc = docBuilder.parse(src);
			Element root = doc.getDocumentElement();
			String rootNS = root.getNamespaceURI();
			if (rootNS != null && rootNS.equals(Namespaces.EXIST_NS)) {
				if (root.getLocalName().equals("query")) {
					// process <query>xpathQuery</query>
					String option = root.getAttribute("start");
					if (option != null)
						try {
							start = Integer.parseInt(option);
						} catch (NumberFormatException e) {
						}
					option = root.getAttribute("max");
					if (option != null)
						try {
							howmany = Integer.parseInt(option);
						} catch (NumberFormatException e) {
						}

					option = root.getAttribute("enclose");
					if (option != null) {
						if (option.equals("no"))
							enclose = false;
					}

					option = root.getAttribute("mime");
					mime = MimeType.XML_TYPE.getName();
					if ((option != null) && (!option.equals(""))) {
						mime = option;
					}

					if ((option = root.getAttribute("cache")) != null) {
						cache = option.equals("yes");
					}

					if ((option = root.getAttribute("session")) != null
							&& option.length() > 0) {
						outputProperties.setProperty(
								Serializer.PROPERTY_SESSION_ID, option);
					}
					NodeList children = root.getChildNodes();
					for (int i = 0; i < children.getLength(); i++) {
						Node child = children.item(i);
						if (child.getNodeType() == Node.ELEMENT_NODE
								&& child.getNamespaceURI().equals(
										Namespaces.EXIST_NS)) {
							if (child.getLocalName().equals("text")) {
								StringBuffer buf = new StringBuffer();
								Node next = child.getFirstChild();
								while (next != null) {
									if (next.getNodeType() == Node.TEXT_NODE
											|| next.getNodeType() == Node.CDATA_SECTION_NODE)
										buf.append(next.getNodeValue());
									next = next.getNextSibling();
								}
								query = buf.toString();
							} else if (child.getLocalName()
									.equals("properties")) {
								Node node = child.getFirstChild();
								while (node != null) {
									if (node.getNodeType() == Node.ELEMENT_NODE
											&& node.getNamespaceURI().equals(
													Namespaces.EXIST_NS)
											&& node.getLocalName().equals(
													"property")) {
										Element property = (Element) node;
										String key = property
												.getAttribute("name");
										String value = property
												.getAttribute("value");
										LOG.debug(key + " = " + value);
										if (key != null && value != null)
											outputProperties.setProperty(key,
													value);
									}
									node = node.getNextSibling();
								}
							}
						}
					}
				}
				// execute query
				if (query != null) {
					String result;
					try {
						search(broker, query, path, howmany, start,
								outputProperties, enclose, cache, request,
								response);
					} catch (Exception e) {
						response.setStatus(HttpServletResponse.SC_ACCEPTED);
						result = e.getMessage();
						// TODO: write error response
					}

				} else {
					transact.abort(transaction);
					throw new BadRequestException("No query specified");
				}
			} else if (rootNS != null
					&& rootNS.equals(XUpdateProcessor.XUPDATE_NS)) {
				LOG.debug("Got xupdate request: " + content);
				MutableDocumentSet docs = new DefaultDocumentSet();
				Collection collection = broker.getCollection(pathUri);
				if (collection != null) {
					collection.allDocs(broker, docs, true, true);
				} else {
					DocumentImpl xupdateDoc = (DocumentImpl) broker
							.getXMLResource(pathUri);
					if (xupdateDoc != null) {
						if (!xupdateDoc.getPermissions().validate(
								broker.getUser(), Permission.READ)) {
							transact.abort(transaction);
							throw new PermissionDeniedException(
									"Not allowed to read collection");
						}
						docs.add(xupdateDoc);
					} else
						broker.getAllXMLResources(docs);
				}

				XUpdateProcessor processor = new XUpdateProcessor(broker, docs,
						AccessContext.REST);
				Modification modifications[] = processor.parse(new InputSource(
						new StringReader(content)));
				long mods = 0;
				for (int i = 0; i < modifications.length; i++) {
					mods += modifications[i].process(transaction);
					broker.flush();
				}
				transact.commit(transaction);

				// FD : Returns an XML doc
				writeXUpdateResult(response, encoding, mods);
				// END FD
			} else {
				transact.abort(transaction);
				throw new BadRequestException("Unknown XML root element: "
						+ root.getNodeName());
			}
		} catch (SAXException e) {
			transact.abort(transaction);
			Exception cause = e;
			if (e.getException() != null)
				cause = e.getException();
			LOG.debug("SAX exception while parsing request: "
					+ cause.getMessage(), cause);
			throw new BadRequestException(
					"SAX exception while parsing request: "
							+ cause.getMessage());
		} catch (ParserConfigurationException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"Parser exception while parsing request: " + e.getMessage());
		} catch (XPathException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"Query exception while parsing request: " + e.getMessage());
		} catch (IOException e) {
			transact.abort(transaction);
			throw new BadRequestException(
					"IO exception while parsing request: " + e.getMessage());
		} catch (EXistException e) {
			transact.abort(transaction);
			throw new BadRequestException(e.getMessage());
		} catch (LockException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		}
	}

	/**
	 * Creates an input source from a URL location with an optional known
	 * charset.
	 */
	private InputSource createInputSource(String charset, URI location)
			throws java.io.IOException {
		if (charset == null) {
			return new InputSource(location.toASCIIString());
		} else {
			InputSource source = new InputSource(new InputStreamReader(location
					.toURL().openStream(), charset));
			source.setSystemId(location.toASCIIString());
			return source;
		}
	}

	/**
	 * Handles PUT requests. The request content is stored as a new resource at
	 * the specified location. If the resource already exists, it is overwritten
	 * if the user has write permissions.
	 * 
	 * The resource type depends on the content type specified in the HTTP
	 * header. The content type will be looked up in the global mime table. If
	 * the corresponding mime type is not a know XML mime type, the resource
	 * will be stored as a binary resource.
	 * 
	 * @param broker
	 * @param tempFile
	 *            The temp file from which the PUT will get its content
	 * @param path
	 *            The path to which the file should be stored
	 * @param request
	 * @param response
	 * @throws BadRequestException
	 * @throws PermissionDeniedException
	 */
	public void doPut(DBBroker broker, File tempFile, XmldbURI path,
			HttpServletRequest request, HttpServletResponse response)
			throws BadRequestException, PermissionDeniedException, IOException {
		if (tempFile == null)
			throw new BadRequestException("No request content found for PUT");

		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn transaction = transact.beginTransaction();
		try {
			XmldbURI docUri = path.lastSegment();
			XmldbURI collUri = path.removeLastSegment();

			if (docUri == null || collUri == null) {
				transact.abort(transaction);
				throw new BadRequestException("Bad path: " + path);
			}
			// TODO : use getOrCreateCollection() right now ?
			Collection collection = broker.getCollection(collUri);
			if (collection == null) {
				LOG.debug("creating collection " + collUri);
				collection = broker.getOrCreateCollection(transaction, collUri);
				broker.saveCollection(transaction, collection);
			}
			MimeType mime;
			String contentType = request.getContentType();
			String charset = null;
			if (contentType != null) {
				int semicolon = contentType.indexOf(';');
				if (semicolon > 0) {
					contentType = contentType.substring(0, semicolon).trim();
					int equals = contentType.indexOf('=', semicolon);
					if (equals > 0) {
						String param = contentType.substring(semicolon + 1,
								equals).trim();
						if (param.compareToIgnoreCase("charset=") == 0) {
							charset = param.substring(equals + 1).trim();
						}
					}
				}
				mime = MimeTable.getInstance().getContentType(contentType);
			} else {
				mime = MimeTable.getInstance().getContentTypeFor(docUri);
				if (mime != null)
					contentType = mime.getName();
			}
			if (mime == null)
				mime = MimeType.BINARY_TYPE;

			if (mime.isXMLType()) {
				URI url = tempFile.toURI();
				IndexInfo info = collection.validateXMLResource(transaction,
						broker, docUri, createInputSource(charset, url));
				info.getDocument().getMetadata().setMimeType(contentType);
				collection.store(transaction, broker, info, createInputSource(
						charset, url), false);
				response.setStatus(HttpServletResponse.SC_CREATED);
			} else {

				FileInputStream is = new FileInputStream(tempFile);
				collection.addBinaryResource(transaction, broker, docUri, is,
						contentType, (int) tempFile.length());
				is.close();
				response.setStatus(HttpServletResponse.SC_CREATED);
			}

			transact.commit(transaction);
		} catch (SAXParseException e) {
			transact.abort(transaction);
			throw new BadRequestException("Parsing exception at "
					+ e.getLineNumber() + "/" + e.getColumnNumber() + ": "
					+ e.toString());
		} catch (SAXException e) {
			transact.abort(transaction);
			Exception o = e.getException();
			if (o == null)
				o = e;
			throw new BadRequestException("Parsing exception: "
					+ o.getMessage());
		} catch (EXistException e) {
			transact.abort(transaction);
			throw new BadRequestException("Internal error: " + e.getMessage());
		} catch (TriggerException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		} catch (LockException e) {
			transact.abort(transaction);
			throw new PermissionDeniedException(e.getMessage());
		}
		return;
	}

	public void doDelete(DBBroker broker, XmldbURI path,
			HttpServletResponse response) throws PermissionDeniedException,
			NotFoundException, IOException {
		TransactionManager transact = broker.getBrokerPool()
				.getTransactionManager();
		Txn txn = transact.beginTransaction();
		try {
			Collection collection = broker.getCollection(path);
			if (collection != null) {
				// remove the collection
				LOG.debug("removing collection " + path);
				broker.removeCollection(txn, collection);
				response.setStatus(HttpServletResponse.SC_OK);
			} else {
				DocumentImpl doc = (DocumentImpl) broker.getXMLResource(path);
				if (doc == null) {
					transact.abort(txn);
					throw new NotFoundException(
							"No document or collection found " + "for path: "
									+ path);
				} else {
					// remove the document
					LOG.debug("removing document " + path);
					if (doc.getResourceType() == DocumentImpl.BINARY_FILE)
						doc.getCollection().removeBinaryResource(txn, broker,
								path.lastSegment());
					else
						doc.getCollection().removeXMLResource(txn, broker,
								path.lastSegment());
					response.setStatus(HttpServletResponse.SC_OK);
				}
			}
			transact.commit(txn);
		} catch (TriggerException e) {
			transact.abort(txn);
			throw new PermissionDeniedException("Trigger failed: "
					+ e.getMessage());
		} catch (LockException e) {
			transact.abort(txn);
			throw new PermissionDeniedException("Could not acquire lock: "
					+ e.getMessage());
		} catch (TransactionException e) {
			transact.abort(txn);
			LOG.warn("Transaction aborted: " + e.getMessage(), e);
		}
	}

	private String getRequestContent(HttpServletRequest request)
			throws IOException {
		String encoding = request.getCharacterEncoding();
		if (encoding == null)
			encoding = "UTF-8";
		InputStream is = request.getInputStream();
		Reader reader = new InputStreamReader(is, encoding);
		StringWriter content = new StringWriter();
		char ch[] = new char[4096];
		int len = 0;
		while ((len = reader.read(ch)) > -1)
			content.write(ch, 0, len);
		String xml = content.toString();
		return xml;
	}

	/**
	 * TODO: pass request and response objects to XQuery.
	 * 
	 * @throws XPathException
	 */
	protected void search(DBBroker broker, String query, String path,
			int howmany, int start, Properties outputProperties, boolean wrap,
			boolean cache, HttpServletRequest request,
			HttpServletResponse response) throws BadRequestException,
			PermissionDeniedException, XPathException {
		String sessionIdParam = outputProperties
				.getProperty(Serializer.PROPERTY_SESSION_ID);
		if (sessionIdParam != null) {
			try {
				int sessionId = Integer.parseInt(sessionIdParam);
				if (sessionId > -1) {
					Sequence cached = sessionManager.get(query, sessionId);
					if (cached != null) {
						LOG.debug("Returning cached query result");
						writeResults(response, broker, cached, howmany, start,
								outputProperties, wrap);
					} else {
						LOG
								.debug("Cached query result not found. Probably timed out. Repeating query.");
					}
				}
			} catch (NumberFormatException e) {
				throw new BadRequestException(
						"Invalid session id passed in query request: "
								+ sessionIdParam);
			}
		}
		XmldbURI pathUri = XmldbURI.create(path);
		try {
			Source source = new StringSource(query);
			XQuery xquery = broker.getXQueryService();
			XQueryPool pool = xquery.getXQueryPool();
			CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source);
			XQueryContext context;
			if (compiled == null)
				context = xquery.newContext(AccessContext.REST);
			else
				context = compiled.getContext();
			context.setStaticallyKnownDocuments(new XmldbURI[] { pathUri });
			context.setBaseURI(new AnyURIValue(pathUri.toString()));
			declareVariables(context, request, response);

			if (compiled == null)
				compiled = xquery.compile(context, source);
			else
				compiled.getContext().updateContext(context);
			context.checkOptions(outputProperties);
			try {
				long startTime = System.currentTimeMillis();
				Sequence resultSequence = xquery.execute(compiled, null);
				long queryTime = System.currentTimeMillis() - startTime;
				if (LOG.isDebugEnabled())
					LOG.debug("Found " + resultSequence.getItemCount() + " in "
							+ queryTime + "ms.");

				if (cache) {
					int sessionId = sessionManager.add(query, resultSequence);
					outputProperties.setProperty(
							Serializer.PROPERTY_SESSION_ID, Integer
									.toString(sessionId));
					if (!response.isCommitted())
						response.setIntHeader("X-Session-Id", sessionId);
				}

				writeResults(response, broker, resultSequence, howmany, start,
						outputProperties, wrap);
			} finally {
				pool.returnCompiledXQuery(source, compiled);
			}
		} catch (IOException e) {
			throw new BadRequestException(e.getMessage(), e);
		}
	}

	/**
	 * Pass the request, response and session objects to the XQuery context.
	 * 
	 * @param context
	 * @param request
	 * @param response
	 * @throws XPathException
	 */
	private HttpRequestWrapper declareVariables(XQueryContext context,
			HttpServletRequest request, HttpServletResponse response)
			throws XPathException {
		HttpRequestWrapper reqw = new HttpRequestWrapper(request, formEncoding,
				containerEncoding);
		ResponseWrapper 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());
		return reqw;
	}

	/**
	 * Directly execute an XQuery stored as a binary document in the database.
	 */
	private void executeXQuery(DBBroker broker, DocumentImpl resource,
			HttpServletRequest request, HttpServletResponse response,
			Properties outputProperties, String servletPath, String pathInfo)
			throws XPathException, BadRequestException {
		Source source = new DBSource(broker, (BinaryDocument) resource, true);
		XQuery xquery = broker.getXQueryService();
		XQueryPool pool = xquery.getXQueryPool();
		XQueryContext context;
		CompiledXQuery compiled = pool.borrowCompiledXQuery(broker, source);
		if (compiled == null) {
			// special header to indicate that the query is not returned from
			// cache
			response.setHeader("X-XQuery-Cached", "false");
			context = xquery.newContext(AccessContext.REST);
		} else {
			response.setHeader("X-XQuery-Cached", "true");
			context = compiled.getContext();
		}
		// TODO: don't hardcode this?
		context.setModuleLoadPath(XmldbURI.EMBEDDED_SERVER_URI.append(
				resource.getCollection().getURI()).toString());
		context.setStaticallyKnownDocuments(new XmldbURI[] { resource
				.getCollection().getURI() });
		HttpRequestWrapper reqw = declareVariables(context, request, response);
		reqw.setServletPath(servletPath);
		reqw.setPathInfo(pathInfo);
		if (compiled == null) {
			try {
				compiled = xquery.compile(context, source);
			} catch (IOException e) {
				throw new BadRequestException("Failed to read query from "
						+ resource.getURI(), e);
			}
		}
		context.checkOptions(outputProperties);
		try {
			Sequence result = xquery.execute(compiled, null);
			writeResults(response, broker, result, -1, 1, outputProperties,
					false);
		} finally {
			pool.returnCompiledXQuery(source, compiled);
		}
	}

	// writes out a resource, uses asMimeType as the specified mime-type or if
	// null uses the type of the resource
	private void writeResourceAs(DocumentImpl resource, DBBroker broker,
			String stylesheet, String encoding, String asMimeType,
			Properties outputProperties, HttpServletResponse response)
			throws BadRequestException, PermissionDeniedException, IOException {

		// Do we have permission to read the resource
		if (!resource.getPermissions().validate(broker.getUser(),
				Permission.READ)) {
			throw new PermissionDeniedException("Not allowed to read resource");
		}

		if (resource.getResourceType() == DocumentImpl.BINARY_FILE) {
			// binary resource

			if (asMimeType != null) { // was a mime-type specified?

				response.setContentType(asMimeType);
			} else {
				response.setContentType(resource.getMetadata().getMimeType());
			}
			OutputStream os = response.getOutputStream();
			broker.readBinaryResource((BinaryDocument) resource, os);
			os.flush();
		} else {
			// xml resource

			SAXSerializer sax = null;
			Serializer serializer = broker.getSerializer();
			serializer.reset();

			// Serialize the document
			try {
				sax = (SAXSerializer) SerializerPool.getInstance()
						.borrowObject(SAXSerializer.class);

				// use a stylesheet if specified in query parameters
				if (stylesheet != null) {
					serializer.setStylesheet(resource, stylesheet);
				}
				serializer.setProperties(outputProperties);
                serializer.prepareStylesheets(resource);

				if (asMimeType != null) { // was a mime-type specified?
					response.setContentType(asMimeType + "; charset="
							+ encoding);
				} else {
					if (serializer.isStylesheetApplied()
							|| serializer.hasXSLPi(resource) != null) {
						asMimeType = serializer.getStylesheetProperty(OutputKeys.MEDIA_TYPE);
						if (!useDynamicContentType || asMimeType == null)
							asMimeType = MimeType.HTML_TYPE.getName();
						LOG.debug("media-type: " + asMimeType);
						response.setContentType(asMimeType + "; charset="
								+ encoding);
					} else {
						asMimeType = resource.getMetadata().getMimeType();
						response.setContentType(asMimeType + "; charset="
								+ encoding);
					}
				}
				if (asMimeType.equals(MimeType.HTML_TYPE.getName())) {
					outputProperties.setProperty("method", "xhtml");
					outputProperties.setProperty("media-type", "text/html");
					outputProperties.setProperty("ident", "yes");
					outputProperties.setProperty("omit-xml-declaration", "no");
				}

				OutputStreamWriter writer = new OutputStreamWriter(response
						.getOutputStream(), encoding);
				sax.setOutput(writer, outputProperties);
				serializer.setSAXHandlers(sax, sax);

				serializer.toSAX(resource);

				writer.flush();
				writer.close();
			} catch (SAXException saxe) {
				LOG.warn(saxe);
				throw new BadRequestException("Error while serializing XML: "
						+ saxe.getMessage());
			} catch (TransformerConfigurationException e) {
				LOG.warn(e);
				throw new BadRequestException(e.getMessageAndLocation());
			} finally {
				if (sax != null) {
					SerializerPool.getInstance().returnObject(sax);
				}
			}
		}
	}

	/**
{'fraction_non_alphanumeric': 0.11112322395435881, 'fraction_numerical': 0.0018411521736497209, 'mean_word_length': 3.562727976124682, 'pattern_counts': {'":': 8, '<': 71, '<?xml version=': 0, '>': 77, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 36}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6796200', 'n_tokens_mistral': 29226, 'n_tokens_neox': 23429, 'n_words': 6192}