org.rapidcontext.app.web.WebDavRequest.java Source code

Java tutorial

Introduction

Here is the source code for org.rapidcontext.app.web.WebDavRequest.java

Source

/*
 * RapidContext <http://www.rapidcontext.com/>
 * Copyright (c) 2007-2011 Per Cederberg. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the BSD license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * See the RapidContext LICENSE for more details.
 */

package org.rapidcontext.app.web;

import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.rapidcontext.core.data.Dict;
import org.rapidcontext.core.web.Mime;
import org.rapidcontext.core.web.Request;
import org.rapidcontext.util.HttpUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

/**
 * A WebDAV request handler. This class is used to help analyzing a
 * PROPFIND request and generate the proper response.
 *
 * @author   Per Cederberg
 * @version  1.0
 */
public class WebDavRequest implements HttpUtil {

    /**
     * The class logger.
     */
    private static final Logger LOG = Logger.getLogger(WebDavRequest.class.getName());

    /**
     * The WebDAV display name property constant.
     */
    public static final String PROP_DISPLAY_NAME = "displayname";

    /**
     * The WebDAV creation datetime property constant.
     */
    public static final String PROP_CREATION_DATE = "creationdate";

    /**
     * The WebDAV last modified datetime property constant.
     */
    public static final String PROP_LAST_MODIFIED = "getlastmodified";

    /**
     * The WebDAV resource type property constant.
     */
    public static final String PROP_RESOURCE_TYPE = "resourcetype";

    /**
     * The WebDAV MIME content type property constant.
     */
    public static final String PROP_CONTENT_TYPE = "getcontenttype";

    /**
     * The WebDAV content length property constant.
     */
    public static final String PROP_CONTENT_LENGTH = "getcontentlength";

    /**
     * The WebDAV ETag property constant.
     */
    public static final String PROP_ETAG = "getetag";

    /**
     * The WebDAV supported lock property constant.
     */
    public static final String PROP_SUPPORTED_LOCK = "supportedlock";

    /**
     * The WebDAV lock discovery property constant.
     */
    public static final String PROP_LOCK_DISCOVERY = "lockdiscovery";

    /**
     * The WebDAV source property constant.
     */
    public static final String PROP_SOURCE = "source";

    /**
     * The WebDAV quota used bytes property constant (RFC 4331).
     */
    public static final String PROP_QUOTA_USED_BYTES = "quota-used-bytes";

    /**
     * The WebDAV quota available bytes property constant (RFC 4331).
     */
    public static final String PROP_QUOTA_AVAIL_BYTES = "quota-available-bytes";

    /**
     * The default properties for a collection.
     */
    private static LinkedHashMap PROPS_COLLECTION = new LinkedHashMap();

    /**
     * The default properties for a file.
     */
    private static LinkedHashMap PROPS_FILE = new LinkedHashMap();

    /**
     * The date format used for the creation date property.
     */
    public static final SimpleDateFormat CREATION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    /**
     * The date format used for the last modified date property.
     */
    public static final SimpleDateFormat LAST_MODIFIED_DATE_FORMAT = new SimpleDateFormat(
            "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);

    // Static initializer for property collections and time zones
    static {
        PROPS_COLLECTION.put(PROP_DISPLAY_NAME, "");
        PROPS_COLLECTION.put(PROP_RESOURCE_TYPE, "");
        PROPS_COLLECTION.put(PROP_CREATION_DATE, "");
        PROPS_COLLECTION.put(PROP_LAST_MODIFIED, "");
        PROPS_COLLECTION.put(PROP_SUPPORTED_LOCK, "");
        PROPS_COLLECTION.put(PROP_LOCK_DISCOVERY, "");
        PROPS_COLLECTION.put(PROP_SOURCE, "");
        PROPS_COLLECTION.put(PROP_QUOTA_USED_BYTES, "");
        PROPS_COLLECTION.put(PROP_QUOTA_AVAIL_BYTES, "");
        PROPS_FILE.putAll(PROPS_COLLECTION);
        PROPS_FILE.put(PROP_CONTENT_TYPE, "");
        PROPS_FILE.put(PROP_CONTENT_LENGTH, "");
        PROPS_FILE.put(PROP_ETAG, "");
        CREATION_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        LAST_MODIFIED_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    /**
     * The HTTP request being wrapped.
     */
    private Request request;

    /**
     * The property values flag. If set to true, the property values
     * should also be returned. Otherwise only the property names.
     */
    private boolean propertyValues = true;

    /**
     * The properties found in the query. This will contain all
     * properties if the query doesn't specify any properties.
     */
    private LinkedHashMap properties = new LinkedHashMap();

    /**
     * The property namespace URI to abbreviation map.
     */
    private LinkedHashMap propertyNS = null;

    /**
     * The lock request information (if parsed and available).
     */
    private Dict lockInfo = null;

    /**
     * The array with result XML fragments. Each resource added will
     * be converted into XML snipplet added here.
     */
    private ArrayList results = new ArrayList();

    /**
     * Creates a new WebDAV request.
     *
     * @param request        the HTTP request to read
     *
     * @throws Exception if the WebDAV request XML couldn't be parsed
     */
    public WebDavRequest(Request request) throws Exception {
        String xml = request.getInputString();
        boolean isCollection = request.getPath().endsWith("/");

        LOG.fine(request.getMethod() + " XML:\n" + xml);
        this.request = request;
        if (xml != null && xml.trim().length() > 0) {
            Element root = parseDOM(xml);
            if (request.hasMethod(METHOD.PROPFIND)) {
                if (parseChild(root, "propname") != null) {
                    propertyValues = false;
                }
                parsePropFind(parseChild(root, "prop"), isCollection);
            } else if (request.hasMethod(METHOD.LOCK)) {
                parseLockInfo(root);
            }
        } else if (request.hasMethod(METHOD.PROPFIND)) {
            parsePropFind(null, isCollection);
        }
    }

    /**
     * Parses the specified XML document and returns the document
     * element.
     *
     * @param xml            the XML string to parse
     *
     * @return the root document element
     *
     * @throws Exception if the XML string couldn't be parsed
     */
    private Element parseDOM(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new InputSource(new StringReader(xml)));
        return doc.getDocumentElement();
    }

    /**
     * Finds a child node by tag name.
     *
     * @param parent         the parent node
     * @param name           the child node name
     *
     * @return the child node found, or
     *         null if not found
     */
    private Element parseChild(Element parent, String name) {
        if (parent != null) {
            Node child = parent.getFirstChild();
            while (child != null) {
                String localName = child.getLocalName();
                if (child instanceof Element && localName.equals(name)) {
                    return (Element) child;
                }
                child = child.getNextSibling();
            }
        }
        return null;
    }

    /**
     * Parses a property find request. All properties found will be
     * added to the query properties map. If no properties node was
     * provided, all standard properties will be added.
     *
     * @param node           the properties node, or null
     * @param isCollection   the collection flag
     */
    private void parsePropFind(Element node, boolean isCollection) {
        LinkedHashMap defaults;
        String name;

        defaults = isCollection ? PROPS_COLLECTION : PROPS_FILE;
        if (node == null) {
            properties.putAll(defaults);
        } else {
            Node child = node.getFirstChild();
            while (child != null) {
                if (child.getNodeType() != Node.ELEMENT_NODE) {
                    // Ignore non-elements
                } else if ("DAV:".equals(child.getNamespaceURI())) {
                    if (defaults.containsKey(child.getLocalName())) {
                        properties.put(child.getLocalName(), "");
                    } else {
                        properties.put(child.getLocalName(), null);
                    }
                } else {
                    name = child.getNamespaceURI() + ":" + child.getLocalName();
                    properties.put(name, null);
                }
                child = child.getNextSibling();
            }
        }
    }

    /**
     * Parses the lock request. The lock details will be set in the
     * lock info dictionary.
     *
     * @param root           the XML document node
     */
    private void parseLockInfo(Element root) {
        Element node;

        lockInfo = new Dict();
        lockInfo.set("href", request.getAbsolutePath());
        lockInfo.set("token", null);
        lockInfo.setInt("depth", depth());
        lockInfo.set("timeout", timeout());
        node = parseChild(root, "lockscope");
        if (parseChild(node, "exclusive") != null) {
            lockInfo.set("scope", "exclusive");
        } else if (parseChild(node, "shared") != null) {
            lockInfo.set("scope", "shared");
        } else {
            lockInfo.set("scope", "unknown");
        }
        lockInfo.set("type", "write");
        node = parseChild(root, "owner");
        if (parseChild(node, "href") != null) {
            lockInfo.set("owner", parseChild(node, "href").getTextContent());
        } else {
            lockInfo.set("owner", request.getHeader(HEADER.USER_AGENT));
        }
    }

    /**
     * Returns the requested depth of properties.
     *
     * @return the requested depth of properties, or
     *         -1 for infinity
     */
    public int depth() {
        try {
            return Integer.parseInt(request.getHeader(HEADER.DEPTH));
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /**
     * Returns the requested lock timeout value.
     *
     * @return the requested lock timeout value, or
     *         null if not specified
     */
    public String timeout() {
        return request.getHeader(HEADER.TIMEOUT);
    }

    /**
     * Returns information about the requested lock (if applicable).
     *
     * @return the requested lock, or
     *         null if the request wasn't a valid lock request
     */
    public Dict lockInfo() {
        return lockInfo;
    }

    /**
     * Adds a resource to the result with the specified dates and size.
     *
     * @param href           the root-relative resource link
     * @param created        the resource creation date
     * @param modified       the resource modification date
     * @param size           the resource size (in bytes)
     */
    public void addResource(String href, Date created, Date modified, long size) {
        LinkedHashMap props = new LinkedHashMap();
        String name;
        String str;

        props.putAll(properties);
        name = StringUtils.removeEnd(href, "/");
        name = StringUtils.substringAfterLast(href, "/");
        if (props.containsKey(PROP_DISPLAY_NAME)) {
            props.put(PROP_DISPLAY_NAME, name);
        }
        if (props.containsKey(PROP_CREATION_DATE)) {
            str = CREATION_DATE_FORMAT.format(created);
            props.put(PROP_CREATION_DATE, str);
        }
        if (props.containsKey(PROP_LAST_MODIFIED)) {
            str = LAST_MODIFIED_DATE_FORMAT.format(modified);
            props.put(PROP_LAST_MODIFIED, str);
        }
        if (props.containsKey(PROP_CONTENT_TYPE)) {
            props.put(PROP_CONTENT_TYPE, href.endsWith("/") ? null : Mime.type(name));
        }
        if (href.endsWith("/")) {
            if (props.containsKey(PROP_RESOURCE_TYPE)) {
                props.put(PROP_RESOURCE_TYPE, "<D:collection/>");
            }
            if (props.containsKey(PROP_CONTENT_LENGTH)) {
                props.put(PROP_CONTENT_LENGTH, "0");
            }
            if (props.containsKey(PROP_ETAG)) {
                props.put(PROP_ETAG, null);
            }
        } else {
            if (props.containsKey(PROP_CONTENT_LENGTH)) {
                props.put(PROP_CONTENT_LENGTH, String.valueOf(size));
            }
            if (props.containsKey(PROP_ETAG)) {
                str = "W/\"" + size + "-" + modified.getTime() + "\"";
                props.put(PROP_ETAG, str);
            }
        }
        // Fake quota properties to enable read-write access
        if (props.containsKey(PROP_QUOTA_USED_BYTES)) {
            props.put(PROP_QUOTA_USED_BYTES, "0");
        }
        if (props.containsKey(PROP_QUOTA_AVAIL_BYTES)) {
            props.put(PROP_QUOTA_AVAIL_BYTES, "1000000000");
        }
        addResource(href, props);
    }

    /**
     * Adds a resource to the result with the specified properties.
     *
     * @param href           the root-relative resource link
     * @param props          the resource properties
     */
    private void addResource(String href, LinkedHashMap props) {
        StringBuilder buffer = new StringBuilder();
        Iterator iter = props.keySet().iterator();
        ArrayList fails = new ArrayList();
        String key;
        String value;

        xmlTagBegin(buffer, 1, "response");
        xmlTag(buffer, 2, "href", Helper.encodeUrl(href), false);
        xmlTagBegin(buffer, 2, "propstat");
        xmlTagBegin(buffer, 3, "prop");
        while (iter.hasNext()) {
            key = (String) iter.next();
            value = (String) props.get(key);
            if (value == null) {
                fails.add(key);
            } else if (propertyValues) {
                xmlTag(buffer, 4, key, value, !value.startsWith("<"));
            } else {
                xmlTag(buffer, 4, key);
            }
        }
        xmlTagEnd(buffer, 3, "prop");
        xmlStatus(buffer, 3, STATUS.OK);
        xmlTagEnd(buffer, 2, "propstat");
        if (fails.size() > 0) {
            xmlTagBegin(buffer, 2, "propstat");
            xmlTagBegin(buffer, 3, "prop");
            for (int i = 0; i < fails.size(); i++) {
                key = (String) fails.get(i);
                if (key.indexOf(':') > 0) {
                    value = StringUtils.substringBeforeLast(key, ":");
                    key = StringUtils.substringAfterLast(key, ":");
                    buffer.append(StringUtils.repeat("  ", 4));
                    buffer.append("<");
                    buffer.append(namespace(value));
                    buffer.append(":");
                    buffer.append(key);
                    buffer.append(" xmlns:");
                    buffer.append(namespace(value));
                    buffer.append("=\"");
                    buffer.append(value);
                    buffer.append("\"/>\n");
                } else {
                    xmlTag(buffer, 4, key);
                }
            }
            xmlTagEnd(buffer, 3, "prop");
            xmlStatus(buffer, 3, STATUS.NOT_FOUND);
            xmlTagEnd(buffer, 2, "propstat");
        }
        xmlTagEnd(buffer, 1, "response");
        results.add(buffer.toString());
    }

    /**
     * Returns the namespace abbreviation for the specified URL.
     *
     * @param href           the namespace URL
     *
     * @return the namespace abbreviation
     */
    private String namespace(String href) {
        if (propertyNS == null) {
            propertyNS = new LinkedHashMap();
        }
        if (!propertyNS.containsKey(href)) {
            char value = (char) ('E' + propertyNS.size());
            propertyNS.put(href, String.valueOf(value));
            LOG.fine("reserved namespace '" + propertyNS.get(href) + "' for " + href);
        }
        return (String) propertyNS.get(href);
    }

    /**
     * Sends a lock response with the specified information.
     *
     * @param lockInfo       the lock information
     * @param timeout        the lock timeout
     */
    public void sendLockResponse(Dict lockInfo, int timeout) {
        StringBuilder buffer = new StringBuilder();
        String str;

        buffer.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        buffer.append("<D:prop xmlns:D=\"DAV:\">\n");
        xmlTagBegin(buffer, 1, "lockdiscovery");
        xmlTagBegin(buffer, 2, "activelock");
        xmlTagBegin(buffer, 3, "lockroot");
        xmlTag(buffer, 4, "href", lockInfo.getString("href", ""), true);
        xmlTagEnd(buffer, 3, "lockroot");
        xmlTagBegin(buffer, 3, "locktoken");
        xmlTag(buffer, 4, "href", lockInfo.getString("token", ""), true);
        xmlTagEnd(buffer, 3, "locktoken");
        if (lockInfo.getInt("depth", -1) < 0) {
            xmlTag(buffer, 3, "depth", "infinity", true);
        } else {
            xmlTag(buffer, 3, "depth", lockInfo.getString("depth", ""), true);
        }
        str = "Seconds-" + timeout;
        xmlTag(buffer, 3, "timeout", str, false);
        str = "<D:" + lockInfo.get("type") + "/>";
        xmlTag(buffer, 3, "locktype", str, false);
        str = "<D:" + lockInfo.get("scope") + "/>";
        xmlTag(buffer, 3, "lockscope", str, false);
        xmlTagBegin(buffer, 3, "owner");
        xmlTag(buffer, 4, "href", lockInfo.getString("owner", ""), true);
        xmlTagEnd(buffer, 3, "owner");
        xmlTagEnd(buffer, 2, "activelock");
        xmlTagEnd(buffer, 1, "lockdiscovery");
        xmlTagEnd(buffer, 0, "prop");
        str = "<" + lockInfo.getString("token", "") + ">";
        request.setResponseHeader(HEADER.LOCK_TOKEN, str);
        request.sendText(STATUS.OK, Mime.XML[0], buffer.toString());
    }

    /**
     * Sends a multi-status response with the response fragments as
     * the request response.
     *
     * @see #addResource(String, Date, Date, long)
     */
    public void sendMultiResponse() {
        StringBuilder buffer = new StringBuilder();

        buffer.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        buffer.append("<D:multistatus xmlns:D=\"DAV:\">\n");
        for (int i = 0; i < results.size(); i++) {
            buffer.append(results.get(i));
        }
        xmlTagEnd(buffer, 0, "multistatus");
        request.sendText(STATUS.MULTI_STATUS, Mime.XML[0], buffer.toString());
    }

    /**
     * Sends a finite depth error as the request response.
     */
    public void sendErrorFiniteDepth() {
        StringBuilder buffer = new StringBuilder();

        buffer.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        buffer.append("<D:error xmlns:D=\"DAV:\">\n");
        xmlTag(buffer, 1, "propfind-finite-depth");
        xmlTagEnd(buffer, 0, "error");
        request.sendError(STATUS.FORBIDDEN, Mime.XML[0], buffer.toString());
    }

    /**
     * Writes a WebDAV status tag to the specified buffer.
     *
     * @param buffer         the buffer to write to
     * @param indent         the indentation level
     * @param status         the status code
     */
    private void xmlStatus(StringBuilder buffer, int indent, int status) {
        String content = "HTTP/1.1 " + status + " " + STATUS.asText(status);
        xmlTag(buffer, indent, "status", content, false);
    }

    /**
     * Writes an opening WebDAV XML tag to the specified buffer.
     *
     * @param buffer         the buffer to write to
     * @param indent         the indentation level
     * @param tag            the tag name (without namespace)
     */
    private void xmlTagBegin(StringBuilder buffer, int indent, String tag) {
        buffer.append(StringUtils.repeat("  ", indent));
        buffer.append("<D:");
        buffer.append(tag);
        buffer.append(">\n");
    }

    /**
     * Writes a closing WebDAV XML tag to the specified buffer.
     *
     * @param buffer         the buffer to write to
     * @param indent         the indentation level
     * @param tag            the tag name (without namespace)
     */
    private void xmlTagEnd(StringBuilder buffer, int indent, String tag) {
        buffer.append(StringUtils.repeat("  ", indent));
        buffer.append("</D:");
        buffer.append(tag);
        buffer.append(">\n");
    }

    /**
     * Writes a WebDAV XML tag without content to the specified buffer.
     *
     * @param buffer         the buffer to write to
     * @param indent         the indentation level
     * @param tag            the tag name (without namespace)
     */
    private void xmlTag(StringBuilder buffer, int indent, String tag) {
        buffer.append(StringUtils.repeat("  ", indent));
        buffer.append("<D:");
        buffer.append(tag);
        buffer.append("/>\n");
    }

    /**
     * Writes a WebDAV XML tag with content to the specified buffer.
     *
     * @param buffer         the buffer to write to
     * @param indent         the indentation level
     * @param tag            the tag name (without namespace)
     * @param content        the content data
     * @param escapeContent  the escape content flag
     */
    private void xmlTag(StringBuilder buffer, int indent, String tag, String content, boolean escapeContent) {

        buffer.append(StringUtils.repeat("  ", indent));
        buffer.append("<D:");
        buffer.append(tag);
        buffer.append(">");
        if (escapeContent) {
            buffer.append(StringEscapeUtils.escapeXml(content));
        } else {
            buffer.append(content);
        }
        buffer.append("</D:");
        buffer.append(tag);
        buffer.append(">\n");
    }
}