org.dspace.app.dav.DAVResource.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.app.dav.DAVResource.java

Source

/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.dav;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.content.DSpaceObject;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.eperson.EPerson;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.JDOMParseException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

/**
 * Superclass for all DSpace "resources" in the WebDAV interface. Maps DAV
 * operations onto DSpace objects. An instance is created for one HTTP request
 * and discarded thereafter.
 * <p>
 * This class has the high-level driver for each DAV (or SOAP) method. It calls
 * on subclasses for implementation details, e.g. how to get properties in and
 * out of each type of resource, PUT, COPY, etc.
 * <p>
 * All of the interpretation (and generation) of DAV Resource URIs should be in
 * this class, in methods such as <code>findResource()</code>,
 * <code>hrefURL()</code> etc, calling on the <code>matchResource()</code>
 * methods in some subclasses for help. NOTE: <code>matchResource</code>
 * should be an abstract method but it cannot be since it is necessarily static.
 * <p>
 * DAVResource instances are generally only created by the
 * <code>findResource()</code> method and the <code>children()</code>
 * methods in resource subclasses.
 */
abstract class DAVResource {

    /** log4j category. */
    private static Logger log = Logger.getLogger(DAVResource.class);

    /** The path elt. */
    protected String pathElt[] = null;

    /** The request. */
    protected HttpServletRequest request = null;

    /** The response. */
    protected HttpServletResponse response = null;

    /** The context. */
    protected Context context = null;

    /** Optional limit of resources traversed in a PROPFIND - initialized from PROPFIND_LIMIT_CONFIG in config properties. Names maximum number of HREF's in PROPFIND result or 0 for unlimited. */
    private static final String PROPFIND_LIMIT_CONFIG = "dav.propfind.limit";

    /** The propfind resource limit. */
    private static int propfindResourceLimit = ConfigurationManager.getIntProperty(PROPFIND_LIMIT_CONFIG);

    /** Resource type (i.e. DSpace object type) masks to implement type filter in PROPFIND. */
    protected static final int TYPE_OTHER = 0x20; // 100000

    /** The Constant TYPE_SITE. */
    protected static final int TYPE_SITE = 0x10; // 10000

    /** The Constant TYPE_COMMUNITY. */
    protected static final int TYPE_COMMUNITY = 0x08; // 01000

    /** The Constant TYPE_COLLECTION. */
    protected static final int TYPE_COLLECTION = 0x04; // 00100

    /** The Constant TYPE_ITEM. */
    protected static final int TYPE_ITEM = 0x02; // 00010

    /** The Constant TYPE_BITSTREAM. */
    protected static final int TYPE_BITSTREAM = 0x01; // 00001

    /** The Constant TYPE_ALL. */
    protected static final int TYPE_ALL = 0x1F; // 11111

    /** Type code, set by constructor in each subclass. */
    protected int type = 0;

    /** The output raw. */
    private static XMLOutputter outputRaw = new XMLOutputter();

    /** The output pretty. */
    private static XMLOutputter outputPretty = new XMLOutputter(Format.getPrettyFormat());

    // enable dumping XML for last PROPFIND or PROPPATCH transaction.
    /** The Constant debugXML. */
    private static final boolean debugXML = ConfigurationManager.getBooleanProperty("dav.debug.xml", false);

    /** DAV Properties common to all resources:. */
    protected static final Element displaynameProperty = new Element("displayname", DAV.NS_DAV);

    /** The Constant resourcetypeProperty. */
    protected static final Element resourcetypeProperty = new Element("resourcetype", DAV.NS_DAV);

    /** The Constant typeProperty. */
    protected static final Element typeProperty = new Element("type", DAV.NS_DSPACE);

    /** The Constant current_user_privilege_setProperty. */
    protected static final Element current_user_privilege_setProperty = new Element("current-user-privilege-set",
            DAV.NS_DAV);

    /** The common props. */
    protected static List<Element> commonProps = new ArrayList<Element>();
    static {
        commonProps.add(displaynameProperty);
        commonProps.add(resourcetypeProperty);
        commonProps.add(typeProperty);
        commonProps.add(current_user_privilege_setProperty);
    }

    /**
     * Instantiates a new DAV resource.
     * 
     * @param context the context
     * @param request the request
     * @param response the response
     * @param pathElt the path elt
     */
    protected DAVResource(Context context, HttpServletRequest request, HttpServletResponse response,
            String pathElt[]) {
        this.pathElt = (String[]) ArrayUtils.clone(pathElt);
        this.request = request;
        this.response = response;
        this.context = context;
    }

    /*----------------- Abstracts -----------------------*/

    /**
     * Returns an array of <code>DAVResource</code>'s considered the direct
     * children of this resource. Array can be empty but MUST NOT be null.
     * 
     * @return array of immediate children, or empty array if none.
     * 
     * @throws SQLException the SQL exception
     */
    protected abstract DAVResource[] children() throws SQLException;

    /**
     * Execute a PROPFIND method request on this Resource, and insert the
     * results into the <code>multistatus</code> XML element. This may be one
     * of many <code>propfind()</code> calls that build up the result of a
     * PROPFIND on a whole hierarchy. The depth option of the PROPFIND method is
     * managed by the driver that calls this, so it is only responsible for its
     * own resource.
     * 
     * @param property the property
     * 
     * @return the element
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract Element propfindInternal(Element property)
            throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /**
     * Gets the all properties.
     * 
     * @return list of all properties that resource wants known to a "propname"
     * request.
     */
    protected abstract List<Element> getAllProperties();

    /**
     * Execute a PROPPATCH method request on this Resource, and insert the
     * results into the <code>multistatus</code> XML element.
     * 
     * @param action either SET or REMOVE, taken from PROPERTYUPDATE element.
     * @param prop the PROP element from the request structure.
     * 
     * @return HTTP extended status code, e.g. 200 for success.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract int proppatchInternal(int action, Element prop)
            throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /**
     * Output the GET method results for the resource to the response, should
     * include content-type and length headers.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract void get() throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /**
     * Create a new resource out of the contents of this request. For example,
     * add a new Item under an existing Collection. Might also replace a
     * Bitstream (or install a new version).
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract void put() throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /**
     * Create a copy of a DAV resource under a different DAV "collection"
     * resource, within the limits of DSpace semantics. Use this to install an
     * Item in an additional Collection, for example. NOTE: copyInternal SHOULD
     * only return success status codes; throw a DAVStatusException to indicate
     * an error so the descriptive text can also be included.
     * 
     * @param destination the destination
     * @param depth the depth
     * @param overwrite the overwrite
     * @param keepProperties the keep properties
     * 
     * @return HTTP status code (201 or 204 for success)
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract int copyInternal(DAVResource destination, int depth, boolean overwrite,
            boolean keepProperties) throws DAVStatusException, SQLException, AuthorizeException, IOException;

    /**
     * Return value of dspace:type property, e.g. an empty element with the tag
     * "dspace:item".
     * 
     * @return JDOM Element to put in PROPFIND result as value of "dspace:type"
     */
    protected abstract Element typeValue();

    /**
     * Execute a DELETE method request on this Resource. Inserts nothing into
     * the <code>multistatus</code> XML element.
     * 
     * @return HTTP extended status code, e.g. 200 for success.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract int deleteInternal()
            throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /**
     * Execute a MKCOL method request on this Resource. Inserts nothing into the
     * <code>multistatus</code> XML element. Only makes sense to do a MKCOL
     * operation on Communities and Collections.
     * 
     * @param name the name
     * 
     * @return HTTP extended status code, e.g. 200 for success.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected abstract int mkcolInternal(String name)
            throws SQLException, AuthorizeException, IOException, DAVStatusException;

    /*----------------- Interpreting Resource URIs -----------------------*/

    /**
     * Get resource named by a DAV URI. The URI can be relative (from the Site)
     * or absolute if it starts with the WebDAV server's prefix.
     * 
     * @param uri the uri
     * 
     * @return resource object or null upon error.
     * 
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws SQLException the SQL exception
     * @throws DAVStatusException the DAV status exception
     * @throws AuthorizeException the authorize exception
     */
    protected DAVResource uriToResource(String uri)
            throws IOException, SQLException, DAVStatusException, AuthorizeException {
        String dest = uri;
        String prefix = hrefPrefix();
        if (dest.startsWith(prefix)) {
            dest = dest.substring(prefix.length());
        }
        if (dest.startsWith("/")) {
            dest = dest.substring(1);
        }
        return findResource(this.context, this.request, this.response, dest.split("/"));
    }

    /**
     * Creates a resource object corresponding to the path.
     * 
     * @param context the context
     * @param request the request
     * @param response the response
     * @param pathElt the path elt
     * 
     * @return resource, or null if path cannot be interpreted. Sends HTTP
     * status in the event of failure.
     * 
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws SQLException the SQL exception
     * @throws DAVStatusException the DAV status exception
     * @throws AuthorizeException the authorize exception
     */
    protected static DAVResource findResource(Context context, HttpServletRequest request,
            HttpServletResponse response, String pathElt[])
            throws IOException, SQLException, DAVStatusException, AuthorizeException {
        DAVResource result = DAVSite.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVLookup.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVWorkspace.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVWorkflow.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVEPerson.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVItem.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVBitstream.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        result = DAVDSpaceObject.matchResourceURI(context, request, response, pathElt);
        if (result != null) {
            return result;
        }

        throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Unrecognized DSpace resource URI");
    }

    /*----------------- Generating Resource URIs -----------------------*/

    /**
     * Construct URL prefix up to the resource path, for making up "href" values
     * in e.g. PROPFIND responses.
     * 
     * @return start of resource URI ending with a '/'.
     */
    protected String hrefPrefix() {
        // Note: contextPath and servletPath include leading slashes but no
        // trailing..
        if (this.request == null) {
            return "/";
        } else {
            return this.request.getScheme() + "://" + this.request.getServerName() + ":"
                    + String.valueOf(this.request.getServerPort()) + this.request.getContextPath()
                    + this.request.getServletPath() + "/";
        }
    }

    /**
     * Construct the whole absolute URL to resource - prefix plus path elements.
     * 
     * @return the string
     */
    protected String hrefURL() {
        StringBuffer result = new StringBuffer(hrefPrefix());
        for (int i = 0; i < this.pathElt.length; ++i) {
            if (i + 1 < this.pathElt.length) {
                result.append(this.pathElt[i]).append("/");
            } else {
                result.append(this.pathElt[i]);
            }
        }

        return result.toString();
    }

    /**
     * Href to E person.
     * 
     * @param ep the ep
     * 
     * @return fully-qualfied URL to resource of given EPerson.
     */
    protected String hrefToEPerson(EPerson ep) {
        return hrefPrefix() + DAVEPerson.getPath(ep);
    }

    /**
     * Construct path to child DSpaceObject.
     * 
     * @param child the child
     * 
     * @return the string[]
     */
    protected String[] makeChildPath(DSpaceObject child) {
        String bpath[] = makeChildPathInternal();
        bpath[this.pathElt.length] = DAVDSpaceObject.getPathElt(child);
        return bpath;
    }

    /**
     * Construct path to child with last element predetermined.
     * 
     * @param lastElt the last elt
     * 
     * @return the string[]
     */
    protected String[] makeChildPath(String lastElt) {
        String bpath[] = makeChildPathInternal();
        bpath[this.pathElt.length] = lastElt;
        return bpath;
    }

    // actaul work of building a child path.
    /**
     * Make child path internal.
     * 
     * @return the string[]
     */
    private String[] makeChildPathInternal() {
        String bpath[] = new String[this.pathElt.length + 1];
        for (int k = 0; k < this.pathElt.length; ++k) {
            bpath[k] = this.pathElt[k];
        }
        return bpath;
    }

    /*----------------- Handling DAV Requests ------------------*/

    /**
     * PROPFIND method service: Collect parameters and launch the recursive
     * generic propfind driver which in turn calls resource methods.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected void propfind() throws SQLException, AuthorizeException, IOException, DAVStatusException {
        // set all incoming encoding to UTF-8
        this.request.setCharacterEncoding("UTF-8");

        /*
         * FIXME:(?) this is technically wrong, wrt. WebDAV protocol, but it's
         * more efficient; default depth should be DAV.DAV_INFINITY; we cheat
         * and make it 0.
         */
        int depth = 0;

        String sdepth = this.request.getHeader("Depth");
        if (sdepth != null) {
            sdepth = sdepth.trim();
            try {
                if (sdepth.equalsIgnoreCase("infinity")) {
                    depth = DAV.DAV_INFINITY;
                } else {
                    depth = Integer.parseInt(sdepth);
                }
            } catch (NumberFormatException nfe) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Bad Depth header: " + sdepth,
                        nfe);
            }
        }

        // get object-type mask from request query args, e.g.
        // type=ITEM,type=BITSTREAM ...
        String types[] = this.request.getParameterValues("type");
        int typeMask = typesToMask(types);

        Document outdoc = propfindDriver(depth, this.request.getInputStream(), typeMask);
        if (outdoc != null) {
            this.response.setStatus(DAV.SC_MULTISTATUS);
            this.response.setContentType("text/xml");

            outputRaw.output(outdoc, this.response.getOutputStream());

            if (debugXML) {
                log.debug("PROPFIND response = " + outputPretty.outputString(outdoc));
            }
        }
    }

    /**
     * Inner logic for propfind. Shared with SOAP servlet
     * 
     * @param depth the depth
     * @param pfDoc the pf doc
     * @param typeMask the type mask
     * 
     * @return the document
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected Document propfindDriver(int depth, InputStream pfDoc, int typeMask)
            throws SQLException, AuthorizeException, IOException, DAVStatusException {
        // When there is no document, type defaults to <allprop>.
        int pfType = DAV.PROPFIND_ALLPROP;
        List<Element> pfProps = null;

        try {
            SAXBuilder builder = new SAXBuilder();
            Document reqdoc = builder.build(pfDoc);
            Element propfind = reqdoc.getRootElement();
            if (!propfind.getName().equals("propfind") || !propfind.getNamespace().equals(DAV.NS_DAV)) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Bad Root Element, must be propfind in DAV:");
            }

            // Propfind child is be ONE of: allprop | propname | prop
            // if "prop", also get list of type names.
            List<Element> pfChild = propfind.getChildren();
            if (pfChild.size() > 0) {
                Element child0 = pfChild.get(0);
                String rawType = child0.getName();
                if (rawType.equalsIgnoreCase("prop")) {
                    pfType = DAV.PROPFIND_PROP;
                    pfProps = child0.getChildren();
                } else if (rawType.equalsIgnoreCase("allprop")) {
                    pfType = DAV.PROPFIND_ALLPROP;
                } else if (rawType.equalsIgnoreCase("propname")) {
                    pfType = DAV.PROPFIND_PROPNAME;
                } else {
                    log.warn(LogManager.getHeader(this.context, "propfind",
                            "Unknown TYPE child of <propfind>, named: \"" + rawType + "\""));
                }
            }

            if (debugXML) {
                log.debug("PROPFIND request = " + outputPretty.outputString(reqdoc));
            }
        } catch (JDOMParseException je) {
            // if there is no document we get error at line -1, so let it pass.
            if (je.getLineNumber() >= 0) {
                log.error(LogManager.getHeader(this.context, "propfind", je.toString()));
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Could not parse request document: " + je.toString(), je);
            }
        } catch (JDOMException je) {
            log.error(LogManager.getHeader(this.context, "propfind", je.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Could not parse request document: " + je.toString(), je);
        }

        // At this point, pfProps, pfType and URI define the whole request.

        // Construct response XML
        Element multistatus = new Element("multistatus", DAV.NS_DAV);
        Document outdoc = new Document(multistatus);

        propfindCrawler(multistatus, pfType, pfProps, depth, typeMask, 0);
        return outdoc;
    }

    // Recursive propfind driver: each call to
    // resource.propfindInternal accumulates <response>s in multistatus.
    /**
     * Propfind crawler.
     * 
     * @param multistatus the multistatus
     * @param pfType the pf type
     * @param pfProps the pf props
     * @param depth the depth
     * @param typeMask the type mask
     * @param count the count
     * 
     * @return the int
     * 
     * @throws DAVStatusException the DAV status exception
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private int propfindCrawler(Element multistatus, int pfType, List<Element> pfProps, int depth, int typeMask,
            int count) throws DAVStatusException, SQLException, AuthorizeException, IOException {
        if ((this.type & typeMask) != 0 || this.type == TYPE_OTHER) {
            // check the count of resources visited
            ++count;
            if (propfindResourceLimit > 0 && count > propfindResourceLimit) {
                throw new DAVStatusException(HttpServletResponse.SC_CONFLICT,
                        "PROPFIND request exceeded server's limit on number of resources to query.");
            }

            String uri = hrefURL();
            log.debug("PROPFIND returning (count=" + String.valueOf(count) + ") href=" + uri);
            List<Element> notFound = new ArrayList<Element>();
            List<Element> forbidden = new ArrayList<Element>();
            Element responseElt = new Element("response", DAV.NS_DAV);
            multistatus.addContent(responseElt);

            Element href = new Element("href", DAV.NS_DAV);
            href.setText(uri);
            responseElt.addContent(href);

            // just get the names
            if (pfType == DAV.PROPFIND_PROPNAME) {
                responseElt.addContent(
                        makePropstat(copyElementList(getAllProperties()), HttpServletResponse.SC_OK, "OK"));
            } else {
                List<Element> success = new LinkedList<Element>();
                List<Element> props = (pfType == DAV.PROPFIND_ALLPROP) ? getAllProperties() : pfProps;
                ListIterator pi = props.listIterator();
                while (pi.hasNext()) {
                    Element property = (Element) pi.next();
                    try {
                        Element value = propfindInternal(property);
                        if (value != null) {
                            success.add(value);
                        } else {
                            notFound.add((Element) property.clone());
                        }
                    } catch (DAVStatusException se) {
                        if (se.getStatus() == HttpServletResponse.SC_NOT_FOUND) {
                            notFound.add((Element) property.clone());
                        } else {
                            responseElt.addContent(
                                    makePropstat((Element) property.clone(), se.getStatus(), se.getMessage()));
                        }
                    } catch (AuthorizeException ae) {
                        forbidden.add((Element) property.clone());
                    }
                }

                if (success.size() > 0) {
                    responseElt.addContent(makePropstat(success, HttpServletResponse.SC_OK, "OK"));
                }
                if (notFound.size() > 0) {
                    responseElt.addContent(makePropstat(notFound, HttpServletResponse.SC_NOT_FOUND, "Not found"));
                }
                if (forbidden.size() > 0) {
                    responseElt.addContent(
                            makePropstat(forbidden, HttpServletResponse.SC_FORBIDDEN, "Not authorized."));
                }
            }
        }

        // recurse on children; filter on types first.
        // child types are all lower bits than this type, so (type - 1) is
        // bit-mask of all the possible child types. Skip recursion if
        // all child types would be masked out anyway.
        if (depth != 0 && ((this.type - 1) & typeMask) != 0) {
            DAVResource[] kids = children();
            for (DAVResource element : kids) {
                count = element.propfindCrawler(multistatus, pfType, pfProps,
                        depth == DAV.DAV_INFINITY ? depth : depth - 1, typeMask, count);
            }
        }

        return count;
    }

    /**
     * Service routine for PROPPATCH method on a resource: Take apart the
     * PROPERTYUPDATE request document, call resource's proppatchInternal() for
     * each property, and accumulate the response document.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected void proppatch() throws SQLException, AuthorizeException, IOException, DAVStatusException {
        // set all incoming encoding to UTF-8
        this.request.setCharacterEncoding("UTF-8");

        Document outdoc = proppatchDriver(this.request.getInputStream());

        this.response.setStatus(DAV.SC_MULTISTATUS);
        this.response.setContentType("text/xml");
        outputRaw.output(outdoc, this.response.getOutputStream());

        if (debugXML) {
            log.debug("PROPPATCH response = " + outputPretty.outputString(outdoc));
        }
    }

    /**
     * Inner logic for proppatch. Shared with SOAP servlet
     * 
     * @param docStream the doc stream
     * 
     * @return the document
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected Document proppatchDriver(InputStream docStream)
            throws SQLException, AuthorizeException, IOException, DAVStatusException {

        Document reqdoc = null;
        try {
            SAXBuilder builder = new SAXBuilder();
            reqdoc = builder.build(docStream);
        } catch (JDOMParseException je) {
            // if there is no document we get error at line -1, so let it pass.
            if (je.getLineNumber() >= 0) {
                log.error(LogManager.getHeader(this.context, "proppatch", je.toString()));
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Could not parse PROPERTYUPDATE request document: " + je.toString(), je);
            }
        } catch (JDOMException je) {
            log.error(LogManager.getHeader(this.context, "proppatch", je.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Could not parse PROPERTYUPDATE request document: " + je.toString(), je);
        }
        if (reqdoc == null) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Failed to parse any valid PROPERTYUPDATE document in request.");
        }

        Element pupdate = reqdoc.getRootElement();
        if (!pupdate.getName().equals("propertyupdate") || !pupdate.getNamespace().equals(DAV.NS_DAV)) {
            log.warn(LogManager.getHeader(this.context, "proppatch",
                    "Got bad root element, XML=" + pupdate.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Bad Root Element, must be propertyupdate in DAV:");
        }

        Element multistatus = new Element("multistatus", DAV.NS_DAV);
        Element msResponse = new Element("response", DAV.NS_DAV);
        multistatus.addContent(msResponse);
        Element href = new Element("href", DAV.NS_DAV);
        msResponse.addContent(href);
        href.addContent(hrefURL());

        // result status and accumulation:
        boolean failing = false;
        List<Element> failedDep = new LinkedList<Element>();
        List<Element> success = new LinkedList<Element>();

        // process the SET and REMOVE elements under PROPERTYUPDATE:
        ListIterator ci = pupdate.getChildren().listIterator();
        while (ci.hasNext()) {
            int action = -1;
            Element e = (Element) ci.next();
            if (e.getName().equals("set") && e.getNamespace().equals(DAV.NS_DAV)) {
                action = DAV.PROPPATCH_SET;
            } else if (e.getName().equals("remove") && e.getNamespace().equals(DAV.NS_DAV)) {
                action = DAV.PROPPATCH_REMOVE;
            } else {
                log.warn(LogManager.getHeader(this.context, "proppatch",
                        "Got unrecognized PROPERTYUPDATE element:" + e.toString()));
                continue;
            }
            // PROP elements under SET or REMOVE
            ListIterator pi = e.getChildren().listIterator();
            while (pi.hasNext()) {
                Element p = (Element) pi.next();
                if (p.getName().equals("prop") && p.getNamespace().equals(DAV.NS_DAV)) {
                    ListIterator propi = p.getChildren().listIterator();
                    while (propi.hasNext()) {
                        Element thisprop = (Element) propi.next();

                        // if this PROPPATCH request is failing, just
                        // accumulate properties for Failed Dependency status
                        if (failing) {
                            failedDep.add(new Element(thisprop.getName(), thisprop.getNamespace()));
                        } else {
                            int status = 0;
                            String statusMsg = null;
                            try {
                                status = proppatchCommonInternal(action, thisprop);
                            } catch (DAVStatusException se) {
                                status = se.getStatus();
                                statusMsg = se.getMessage();
                            } catch (AuthorizeException ae) {
                                status = HttpServletResponse.SC_FORBIDDEN;
                                statusMsg = "Permission denied.";
                            }
                            if (status == HttpServletResponse.SC_OK) {
                                success.add(new Element(thisprop.getName(), thisprop.getNamespace()));
                                log.debug("proppatch SET/REMOVE OK, action=" + String.valueOf(action) + ", prop="
                                        + thisprop.toString());
                            } else {
                                failing = true;
                                msResponse.addContent(makePropstat(thisprop, status, statusMsg));
                                log.debug("proppatch SET/REMOVE FAILED with status=" + String.valueOf(status)
                                        + ", on prop=" + thisprop.toString());
                            }
                        }
                    }
                } else {
                    log.warn(LogManager.getHeader(this.context, "proppatch",
                            "No PROP element where expected, found:" + p.toString()));
                }
            }
        }

        // add success and failure propstat elements to response
        if (failing) {
            failedDep.addAll(success);
            if (failedDep.size() > 0) {
                msResponse.addContent(makePropstat(failedDep, DAV.SC_FAILED_DEPENDENCY,
                        "Failed because another property failed."));
            }
            this.context.abort();
        } else {
            if (success.size() > 0) {
                msResponse.addContent(makePropstat(success, HttpServletResponse.SC_OK, "OK"));
            }
            this.context.complete();
        }
        return new Document(multistatus);

    }

    // call proppatchInternal after taking care of "common" properties
    /**
     * Proppatch common internal.
     * 
     * @param action the action
     * @param prop the prop
     * 
     * @return the int
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    private int proppatchCommonInternal(int action, Element prop)
            throws SQLException, AuthorizeException, IOException, DAVStatusException {
        if (elementsEqualIsh(prop, resourcetypeProperty) || elementsEqualIsh(prop, typeProperty)
                || elementsEqualIsh(prop, current_user_privilege_setProperty)) {
            throw new DAVStatusException(DAV.SC_CONFLICT, "The " + prop.getName() + " property cannot be changed.");
        } else {
            return proppatchInternal(action, prop);
        }
    }

    /**
     * Returns value of one of the common properties. Returns null when property
     * isn't one of the common ones. Throws exception if property isn't
     * available (i.e. 404). <br>
     * NOTE: This MUST be called from the subclass's propfindInternal() method
     * so it be passed the content object as a DSpaceObject.
     * <p>
     * Although the displayname is common to all, it is computed differently by
     * each subclass so it's implemented there.
     * 
     * @param property the property
     * @param isCollection the is collection
     * 
     * @return the element
     * 
     * @throws DAVStatusException the DAV status exception
     * @throws SQLException the SQL exception
     */
    protected Element commonPropfindInternal(Element property, boolean isCollection)
            throws DAVStatusException, SQLException {
        // DSpace object type -- also a special case, see typeValue method.
        if (elementsEqualIsh(property, typeProperty)) {
            Element p = (Element) typeProperty.clone();
            p.addContent(typeValue());
            return p;
        }
        // resourcetype -- special case, sub-element: collection or nothing
        else if (elementsEqualIsh(property, resourcetypeProperty)) {
            Element p = (Element) resourcetypeProperty.clone();
            if (isCollection) {
                p.addContent(new Element("collection", DAV.NS_DAV));
            }
            return p;
        }

        // value is dspace:action element with allowable actions.
        else if (elementsEqualIsh(property, current_user_privilege_setProperty)) {
            Element c = (Element) current_user_privilege_setProperty.clone();

            // if we're an admin we have all privs everywhere.
            if (AuthorizeManager.isAdmin(this.context)) {
                addPrivilege(c, new Element("all", DAV.NS_DAV));
            } else {
                addPrivilege(c, new Element("read", DAV.NS_DAV));
            }

            return c;
        } else {
            return null;
        }
    }

    /**
     * Service routine for COPY HTTP request.
     * 
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws DAVStatusException the DAV status exception
     */
    protected void copy() throws IOException, SQLException, AuthorizeException, DAVStatusException {
        // Destination arg from header
        String destination = this.request.getHeader("Destination");
        if (destination == null) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Missing the required request header \"Destination\"");
        }

        // Fix a common misfeature in clients: they will append
        // the final pathname element of the "source" URI to the
        // "destination" URI, which is misleading in our URI scheme so
        // we have to strip it off:
        try {
            String srcPath = (new URI(this.request.getRequestURI())).getPath();
            String destPath = (new URI(destination)).getPath();
            int slash = srcPath.lastIndexOf('/');
            if (slash > -1) {
                String lastElt = srcPath.substring(slash);
                if (destPath.endsWith(lastElt)) {
                    destination = destination.substring(0, destination.length() - lastElt.length() + 1);
                }
            }
            log.debug("Copy dest. URI repair: srcPath=" + srcPath + ", destPath=" + destPath + ", final dest="
                    + destination);
        } catch (URISyntaxException e) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Illegal URI syntax in value of \"Destination\" header: " + destination, e);
        }

        // Depth arg from header
        int depth = DAV.DAV_INFINITY;
        String sdepth = this.request.getHeader("Depth");
        if (sdepth != null) {
            sdepth = sdepth.trim();
            try {
                if (sdepth.equalsIgnoreCase("infinity")) {
                    depth = DAV.DAV_INFINITY;
                } else {
                    depth = Integer.parseInt(sdepth);
                }
            } catch (NumberFormatException nfe) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Illegal value in Depth request header: " + sdepth, nfe);
            }
        }

        // overwrite header logic
        boolean overwrite = false;
        String soverwrite = this.request.getHeader("Overwrite");
        if (soverwrite != null && soverwrite.trim().equalsIgnoreCase("T")) {
            overwrite = true;
        }

        // keepProperties - extract from XML doc in request, if any..
        boolean keepProperties = false;
        Document reqdoc = null;
        try {
            SAXBuilder builder = new SAXBuilder();
            reqdoc = builder.build(this.request.getInputStream());
        } catch (JDOMParseException je) {
            // if there is no document we get error at line -1, so let it pass.
            if (je.getLineNumber() >= 0) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Error parsing XML document in COPY request.", je);
            }
        } catch (JDOMException je) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Error parsing XML document in COPY request: " + je.toString(), je);
        }
        if (reqdoc != null) {
            Element propertybehavior = reqdoc.getRootElement();
            Namespace ns = propertybehavior.getNamespace();
            if (!(ns != null && ns.equals(DAV.NS_DAV) && propertybehavior.getName().equals("propertybehavior"))) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Illegal XML document in COPY request, root= " + propertybehavior.toString());
            }

            // FIXME: (?) Punt on parsing exact list of properties to
            // "keepalive" since we don't implement it anyway.
            if (propertybehavior.getChild("keepalive", DAV.NS_DAV) != null) {
                keepProperties = true;
            } else if (propertybehavior.getChild("omit", DAV.NS_DAV) == null) {
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Illegal propertybehavior document in COPY request, no omit or keepalive child.");
            }
        }

        int result = copyDriver(destination, depth, overwrite, keepProperties);
        if (result >= 200 && result < 300) {
            this.response.setStatus(result);
        } else {
            throw new DAVStatusException(result, "COPY Failed.");
        }
    }

    /**
     * "copy" driver gets parameters from request and calls resource's method.
     * This is shared with SOAP servelet.
     * 
     * @param destination the destination
     * @param depth the depth
     * @param overwrite the overwrite
     * @param keepProperties the keep properties
     * 
     * @return HTTP success status code (201 or 204 for success), Does not
     * return errors, but throws exception.
     * 
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws DAVStatusException the DAV status exception
     */
    protected int copyDriver(String destination, int depth, boolean overwrite, boolean keepProperties)
            throws IOException, SQLException, AuthorizeException, DAVStatusException {
        DAVResource destResource = uriToResource(destination);
        if (destResource == null) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Destination is not a legal DAV resource: " + destination);
        }

        log.debug("Executing COPY method, depth=" + String.valueOf(depth) + ", overwrite="
                + String.valueOf(overwrite) + ", keepProperties=" + String.valueOf(keepProperties)
                + ", destination=\"" + destination + "\"");

        int result = copyInternal(destResource, depth, overwrite, keepProperties);
        this.context.commit();
        return result;
    }

    /**
     * Make bitmask out resource-type keywords. Used by DAV and SOAP interfaces.
     * 
     * @param types the types
     * 
     * @return binary mask of types to allow
     * 
     * @throws DAVStatusException if an unrecognized type keyword is given.
     */
    protected static int typesToMask(String types[]) throws DAVStatusException {
        int typeMask = 0;
        if (types == null || types.length == 0) {
            typeMask = TYPE_ALL;
        } else {
            for (String element : types) {
                String key = element.trim();
                if (key.equalsIgnoreCase("SITE")) {
                    typeMask |= TYPE_SITE;
                } else if (key.equalsIgnoreCase("COMMUNITY")) {
                    typeMask |= TYPE_COMMUNITY;
                } else if (key.equalsIgnoreCase("COLLECTION")) {
                    typeMask |= TYPE_COLLECTION;
                } else if (key.equalsIgnoreCase("ITEM")) {
                    typeMask |= TYPE_ITEM;
                } else if (key.equalsIgnoreCase("BITSTREAM")) {
                    typeMask |= TYPE_BITSTREAM;
                } else {
                    throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                            "Unrecognized type keyword: " + key);
                }
            }
        }
        return typeMask;
    }

    /**
     * Predicate, test if name and namespace of Elements match.
     * 
     * @param a the a
     * @param b the b
     * 
     * @return true if elements have same name and namespace.
     */
    protected boolean elementsEqualIsh(Element a, Element b) {
        Namespace nsa = a.getNamespace();
        Namespace nsb = b.getNamespace();
        return a.getName().equals(b.getName()) && (nsa == nsb || (nsa != null && nsb != null && nsa.equals(nsb)));
    }

    // assemble a PROPSTAT element for response document.
    /**
     * Make propstat.
     * 
     * @param property the property
     * @param status the status
     * @param message the message
     * 
     * @return the element
     */
    private Element makePropstat(Element property, int status, String message) {
        Element ps = makePropstatInternal(status, message);
        Element p = new Element("prop", DAV.NS_DAV);
        Element pp = new Element(property.getName(), property.getNamespace());
        p.addContent(pp);
        ps.addContent(0, p);
        return ps;
    }

    /**
     * Make propstat.
     * 
     * @param properties the properties
     * @param status the status
     * @param message the message
     * 
     * @return the element
     */
    private static Element makePropstat(List<Element> properties, int status, String message) {
        Element ps = makePropstatInternal(status, message);
        Element p = new Element("prop", DAV.NS_DAV);
        p.addContent(properties);
        ps.addContent(0, p);
        return ps;
    }

    /**
     * Make propstat internal.
     * 
     * @param status the status
     * @param message the message
     * 
     * @return the element
     */
    private static Element makePropstatInternal(int status, String message) {
        Element ps = new Element("propstat", DAV.NS_DAV);
        Element s = new Element("status", DAV.NS_DAV);
        s.addContent("HTTP/1.1 " + String.valueOf(status) + " " + message);
        ps.addContent(s);
        return ps;
    }

    /*----------------- Utility Functions -----------------------*/

    /**
     * Translate DSpace authorization "action" code into a WebDAV ACL privilege
     * element; make the most obvious mappings for ones that can be represented
     * accurately and allocate DSpace-namespace elements for the rest.
     * 
     * @param action the action
     * 
     * @return DAV privilege element, or null when there is no mapping.
     */
    protected static Element actionToPrivilege(int action) {
        if (action == Constants.ADD) {
            return new Element("bind", DAV.NS_DAV);
        } else if (action == Constants.COLLECTION_ADMIN) {
            return new Element("collection_admin", DAV.NS_DSPACE);
        } else if (action == Constants.DEFAULT_BITSTREAM_READ) {
            return new Element("default_bitstream_read", DAV.NS_DSPACE);
        } else if (action == Constants.DEFAULT_ITEM_READ) {
            return new Element("default_item_read", DAV.NS_DSPACE);
        } else if (action == Constants.DELETE || action == Constants.REMOVE) {
            return new Element("unbind", DAV.NS_DAV);
        } else if (action == Constants.READ) {
            return new Element("read", DAV.NS_DAV);
        } else if (action == Constants.WORKFLOW_ABORT) {
            return new Element("workflow_abort", DAV.NS_DSPACE);
        } else if (action == Constants.WORKFLOW_STEP_1) {
            return new Element("workflow_step_1", DAV.NS_DSPACE);
        } else if (action == Constants.WORKFLOW_STEP_2) {
            return new Element("workflow_step_2", DAV.NS_DSPACE);
        } else if (action == Constants.WORKFLOW_STEP_3) {
            return new Element("workflow_step_3", DAV.NS_DSPACE);
        } else if (action == Constants.WRITE) {
            return new Element("write", DAV.NS_DAV);
        } else {
            return null;
        }
    }

    /**
     * Add a privilege element to property like current-user-privilege-set,
     * wrapped in &lt;privilege&gt; first.
     * 
     * @param prop the prop
     * @param thePriv the the priv
     */
    protected static void addPrivilege(Element prop, Element thePriv) {
        Element priv = new Element("privilege", DAV.NS_DAV);
        priv.addContent(thePriv);
        prop.addContent(priv);
    }

    // make a "deep" copy of list of empty elements so we can
    // add "all props" list to a propstat with impunity..
    /**
     * Copy element list.
     * 
     * @param el the el
     * 
     * @return the list
     */
    private static List<Element> copyElementList(List<Element> el) {
        List<Element> result = new ArrayList<Element>(el.size());
        for (Element e : el) {
            result.add((Element) e.clone());
        }
        return result;
    }

    /**
     * Get canonical form of persistent identifier (Handle), but allow it to be
     * null. Canonical form is an URN or perhaps URL. For CNRI Handle System
     * handles, it is "hdl:" followed by handle.
     * 
     * @param handle object handle in bare form, e.g. "12345/xyz".
     * 
     * @return canonical form of handle, or null if arg was null.
     */
    protected static String canonicalizeHandle(String handle) {
        if (handle != null) {
            if (handle.startsWith("hdl:")) {
                return handle;
            } else {
                return "hdl:" + handle;
            }
        } else {
            return null;
        }
    }

    /**
     * Utility to filter out characters illegal XML characters when putting
     * something of random provenance into TEXT element.
     * <p>
     * See <a href="http://www.w3.org/TR/2004/REC-xml-20040204/#charsets">
     * http://www.w3.org/TR/2004/REC-xml-20040204/#charsets</a> for rules,
     * essentially, anything above 0x20 and 0x09 (\t, HT), 0x0a (\n, NL), 0x0d
     * (\r, CR).
     * <p>
     * FIXME: for now, just replace all control chars with '?' Maybe someday
     * attempt to do something more meaningful, once it's clear what that would
     * be.
     * 
     * @param in the in
     * 
     * @return the string
     */
    protected static String filterForXML(String in) {
        final Pattern illegals = Pattern.compile("[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]");
        Matcher m = illegals.matcher(in);
        if (m.find()) {
            return m.replaceAll("?");
        } else {
            return in;
        }
    }

    /**
     * Mostly sugar around catching the UnsupportedEncodingException.
     * 
     * @param pathFrag the path frag
     * 
     * @return URL-decoded version of an encoded handle
     */
    protected static String decodeHandle(String pathFrag) {
        try {
            String handle = URLDecoder.decode(pathFrag, "UTF-8");

            // XXX KLUDGE: WebDAV client cadaver double-encodes the %2f,
            // into "%252f" so look for a leftover %25 (== '%').
            if (pathFrag.indexOf("%25") >= 0) {
                handle = URLDecoder.decode(handle, "UTF-8");
            }

            return handle;
        } catch (java.io.UnsupportedEncodingException e) {
            return "";
        }
    }

    /**
     * Mostly sugar around catching the UnsupportedEncodingException.
     * 
     * @param handle the handle
     * 
     * @return URL-encoded version of handle
     */
    protected static String encodeHandle(String handle) {
        try {
            return java.net.URLEncoder.encode(handle, "UTF-8");
        } catch (java.io.UnsupportedEncodingException ue) {
            // highly unlikely.
        }
        return "";
    }

    /**
     * Service routine for DELETE method on a resource:.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected void delete() throws SQLException, AuthorizeException, IOException, DAVStatusException {
        // set all incoming encoding to UTF-8
        this.request.setCharacterEncoding("UTF-8");

        Document outdoc = deleteDriver(this.request.getInputStream());

        this.response.setStatus(DAV.SC_MULTISTATUS);
        this.response.setContentType("text/xml");
        outputRaw.output(outdoc, this.response.getOutputStream());

        if (debugXML) {
            log.debug("DELETE response = " + outputPretty.outputString(outdoc));
        }
    }

    /**
     * Inner logic for delete. Shared with SOAP servlet(??)
     * 
     * @param docStream the doc stream
     * 
     * @return the document
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected Document deleteDriver(InputStream docStream)
            throws SQLException, AuthorizeException, IOException, DAVStatusException {
        Document reqdoc = null;
        try {
            SAXBuilder builder = new SAXBuilder();
            reqdoc = builder.build(docStream);
        } catch (JDOMParseException je) {
            // if there is no document we get error at line -1, so let it pass.
            if (je.getLineNumber() >= 0) {
                log.error(LogManager.getHeader(this.context, "delete", je.toString()));
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Could not parse DELETE request document: " + je.toString(), je);
            }
        } catch (JDOMException je) {
            log.error(LogManager.getHeader(this.context, "delete", je.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Could not parse DELETE request document: " + je.toString(), je);
        }
        if (reqdoc == null) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Failed to parse any valid DELETE document in request.");
        }

        Element pupdate = reqdoc.getRootElement();
        if (!pupdate.getName().equals("delete")) {
            log.warn(LogManager.getHeader(this.context, "delete",
                    "Got bad root element, XML=" + pupdate.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Bad Root Element, must be delete");
        }

        deleteInternal();

        Element multistatus = new Element("multistatus", DAV.NS_DAV);
        Element msResponse = new Element("response", DAV.NS_DAV);
        multistatus.addContent(msResponse);
        Element href = new Element("href", DAV.NS_DAV);
        msResponse.addContent(href);
        href.addContent(hrefURL());
        return new Document(multistatus);
    }

    /**
     * Service routine for MKCOL method on a resource:.
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected void mkcol() throws SQLException, AuthorizeException, IOException, DAVStatusException {
        // set all incoming encoding to UTF-8
        this.request.setCharacterEncoding("UTF-8");

        Document outdoc = mkcolDriver(this.request.getInputStream());

        this.response.setStatus(DAV.SC_MULTISTATUS);
        this.response.setContentType("text/xml");
        outputRaw.output(outdoc, this.response.getOutputStream());

        if (debugXML) {
            log.debug("MKCOL response = " + outputPretty.outputString(outdoc));
        }
    }

    /**
     * Inner logic for mkcol. Shared with SOAP servlet(??)
     * 
     * @param docStream the doc stream
     * 
     * @return the document
     * 
     * @throws SQLException the SQL exception
     * @throws AuthorizeException the authorize exception
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws DAVStatusException the DAV status exception
     */
    protected Document mkcolDriver(InputStream docStream)
            throws SQLException, AuthorizeException, IOException, DAVStatusException {
        Document reqdoc = null;
        try {
            SAXBuilder builder = new SAXBuilder();
            reqdoc = builder.build(docStream);
        } catch (JDOMParseException je) {
            // if there is no document we get error at line -1, so let it pass.
            if (je.getLineNumber() >= 0) {
                log.error(LogManager.getHeader(this.context, "mkcol", je.toString()));
                throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                        "Could not parse MKCOL request document: " + je.toString(), je);
            }
        } catch (JDOMException je) {
            log.error(LogManager.getHeader(this.context, "mkcol", je.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Could not parse MKCOL request document: " + je.toString(), je);
        }
        if (reqdoc == null) {
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST,
                    "Failed to parse any valid MKCOL document in request.");
        }

        Element pupdate = reqdoc.getRootElement();
        String newNodeName = pupdate.getValue();
        if (!"mkcol".equals(pupdate.getName()) || newNodeName == null) {
            log.warn(
                    LogManager.getHeader(this.context, "mkcol", "Got bad root element, XML=" + pupdate.toString()));
            throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Bad Root Element, must be mkcol");
        }
        mkcolInternal(newNodeName);

        Element multistatus = new Element("multistatus", DAV.NS_DAV);
        Element msResponse = new Element("response", DAV.NS_DAV);
        multistatus.addContent(msResponse);
        Element href = new Element("href", DAV.NS_DAV);
        msResponse.addContent(href);
        href.addContent(hrefURL());
        return new Document(multistatus);
    }
}