/**
 * 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);
    }
}
