/* * DAVResource.java * * Version: $Revision: 3705 $ * * Date: $Date: 2009-04-11 17:02:24 +0000 (Sat, 11 Apr 2009) $ * * Copyright (c) 2002-2007, Hewlett-Packard Company and Massachusetts * Institute of Technology. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of the Hewlett-Packard Company nor the name of the * Massachusetts Institute of Technology nor the names of their * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ 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.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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 commonProps = new Vector(); 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 = 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 */ abstract protected 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 */ abstract protected 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. */ abstract protected List 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 */ abstract protected 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 */ abstract protected 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 */ abstract protected 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 */ abstract protected 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 */ abstract protected 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 */ abstract protected 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 = null; if ((result = DAVSite.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVLookup.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVWorkspace.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVWorkflow.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVEPerson.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVItem.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVBitstream.matchResourceURI(context, request, response, pathElt)) == null && (result = DAVDSpaceObject.matchResourceURI(context, request, response, pathElt)) == null) { throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Unrecognized DSpace resource URI"); } return result; } /*----------------- 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] + "/"); } 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); } } // 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 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 pfChild = propfind.getChildren(); if (pfChild.size() > 0) { Element child0 = (Element) 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()); } } 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()); } // 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 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); Vector notFound = new Vector(); Vector forbidden = new Vector(); 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 success = new LinkedList(); List 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()); } } 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()); } 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 failedDep = new LinkedList(); List success = new LinkedList(); // 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); } // 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); } } // 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."); } } catch (JDOMException je) { throw new DAVStatusException(HttpServletResponse.SC_BAD_REQUEST, "Error parsing XML document in COPY request: " + je.toString()); } 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 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 <privilege> 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 copyElementList(List el) { Vector result = new Vector(el.size()); ListIterator li = el.listIterator(); while (li.hasNext()) { result.add(((Element) li.next()).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()); } } 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()); } 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()); } } 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()); } 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 = null; if (!pupdate.getName().equals("mkcol") || (newNodeName = pupdate.getValue()) == 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); } }