org.artifactory.repo.webdav.WebdavServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.repo.webdav.WebdavServiceImpl.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.repo.webdav;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang.StringUtils;
import org.artifactory.api.common.MoveMultiStatusHolder;
import org.artifactory.api.repo.BaseBrowsableItem;
import org.artifactory.api.repo.BrowsableItem;
import org.artifactory.api.repo.BrowsableItemCriteria;
import org.artifactory.api.repo.RepositoryBrowsingService;
import org.artifactory.api.repo.exception.RepoRejectException;
import org.artifactory.api.request.ArtifactoryResponse;
import org.artifactory.api.security.AuthorizationService;
import org.artifactory.api.webdav.WebdavService;
import org.artifactory.common.StatusHolder;
import org.artifactory.descriptor.repo.VirtualRepoDescriptor;
import org.artifactory.mime.MimeType;
import org.artifactory.mime.NamingUtils;
import org.artifactory.model.common.RepoPathImpl;
import org.artifactory.repo.InternalRepoPathFactory;
import org.artifactory.repo.LocalRepo;
import org.artifactory.repo.RepoPath;
import org.artifactory.repo.service.InternalRepositoryService;
import org.artifactory.request.ArtifactoryRequest;
import org.artifactory.sapi.fs.VfsFolder;
import org.artifactory.util.HttpUtils;
import org.artifactory.util.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

/**
 * Service class to handle webdav protocol.<p/> Webdav RFCc at: <a href="http://www.ietf.org/rfc/rfc2518.txt">rfc2518</a>,
 * <a href="http://www.ietf.org/rfc/rfc4918.txt">rfc4918</a>.
 *
 * @author Yossi Shaul
 */
@Service
public class WebdavServiceImpl implements WebdavService {
    private static final Logger log = LoggerFactory.getLogger(WebdavServiceImpl.class);

    /**
     * Default depth is infinity. And it is limited no purpose to 3 level deep.
     */
    private static final int INFINITY = 3;

    /**
     * PROPFIND - Specify a property mask.
     */
    private static final int FIND_BY_PROPERTY = 0;

    /**
     * PROPFIND - Display all properties.
     */
    private static final int FIND_ALL_PROP = 1;

    /**
     * PROPFIND - Return property names.
     */
    private static final int FIND_PROPERTY_NAMES = 2;

    /**
     * Default namespace.
     */
    protected static final String DEFAULT_NAMESPACE = "DAV:";

    /**
     * Simple date format for the creation date ISO representation (partial).
     */
    protected static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    static {
        //GMT timezone - all HTTP dates are on GMT
        creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    @Autowired
    private AuthorizationService authService;

    @Autowired
    private InternalRepositoryService repoService;

    @Autowired
    private RepositoryBrowsingService repoBrowsing;

    @Override
    @SuppressWarnings({ "OverlyComplexMethod" })
    public void handlePropfind(ArtifactoryRequest request, ArtifactoryResponse response) throws IOException {
        // Retrieve the resources
        int depth = INFINITY;
        String depthStr = request.getHeader("Depth");
        if (depthStr != null) {
            if ("0".equals(depthStr)) {
                depth = 0;
            } else if ("1".equals(depthStr)) {
                depth = 1;
            } else if ("infinity".equals(depthStr)) {
                depth = INFINITY;
            }
        }
        List<String> properties = null;
        int propertyFindType = FIND_ALL_PROP;
        Node propNode = null;
        //get propertyNode and type
        if (request.getContentLength() > 0) {
            DocumentBuilder documentBuilder = getDocumentBuilder();
            try {
                Document document = documentBuilder.parse(new InputSource(request.getInputStream()));
                logWebdavRequest(document);
                // Get the root element of the document
                Element rootElement = document.getDocumentElement();
                NodeList childList = rootElement.getChildNodes();

                for (int i = 0; i < childList.getLength(); i++) {
                    Node currentNode = childList.item(i);
                    switch (currentNode.getNodeType()) {
                    case Node.TEXT_NODE:
                        break;
                    case Node.ELEMENT_NODE:
                        if (currentNode.getNodeName().endsWith("prop")) {
                            propertyFindType = FIND_BY_PROPERTY;
                            propNode = currentNode;
                        }
                        if (currentNode.getNodeName().endsWith("propname")) {
                            propertyFindType = FIND_PROPERTY_NAMES;
                        }
                        if (currentNode.getNodeName().endsWith("allprop")) {
                            propertyFindType = FIND_ALL_PROP;
                        }
                        break;
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("Webdav propfind failed.", e);
            }
        }

        if (propertyFindType == FIND_BY_PROPERTY) {
            properties = getPropertiesFromXml(propNode);
        }

        response.setStatus(WebdavStatus.SC_MULTI_STATUS);
        response.setContentType("text/xml; charset=UTF-8");

        // Create multistatus object
        Writer writer = response.getWriter();
        if (log.isDebugEnabled()) {
            writer = new StringWriter(); // write to memory so we'll be able to log the result as string
        }
        XmlWriter generatedXml = new XmlWriter(writer);
        generatedXml.writeXMLHeader();
        generatedXml.writeElement(null, "multistatus xmlns=\"" + DEFAULT_NAMESPACE + "\"", XmlWriter.OPENING);

        RepoPath repoPath = request.getRepoPath();
        BrowsableItem rootItem = null;
        if (repoService.exists(repoPath)) {
            rootItem = repoBrowsing.getLocalRepoBrowsableItem(repoPath);
        }
        if (rootItem != null) {
            recursiveParseProperties(request, response, generatedXml, rootItem, propertyFindType, properties,
                    depth);
        } else {
            log.warn("Item '" + request.getRepoPath() + "' not found.");
        }
        generatedXml.writeElement(null, "multistatus", XmlWriter.CLOSING);
        generatedXml.sendData();
        if (log.isDebugEnabled()) {
            log.debug("Webdav response:\n" + writer.toString());
            //response.setContentLength(writer.toString().getBytes().length);
            response.getWriter().append(writer.toString());
        }
        response.flush();
    }

    @Override
    public void handleMkcol(ArtifactoryRequest request, ArtifactoryResponse response) throws IOException {
        RepoPath repoPath = request.getRepoPath();
        String repoKey = request.getRepoKey();
        LocalRepo repo = repoService.localOrCachedRepositoryByKey(repoKey);
        if (repo == null) {
            response.sendError(HttpStatus.SC_NOT_FOUND, "Could not find repo '" + repoKey + "'.", log);
            return;
        }

        //Return 405 if called on root or the folder already exists
        String path = repoPath.getPath();
        if (StringUtils.isBlank(path) || repo.itemExists(path)) {
            response.sendError(HttpStatus.SC_METHOD_NOT_ALLOWED,
                    "MKCOL can only be executed on non-existent resource: " + repoPath, log);
            return;
        }
        //Check that we are allowed to write
        try {
            // Servlet container doesn't support long values so we take it manually from the header
            String contentLengthHeader = request.getHeader("Content-Length");
            long contentLength = StringUtils.isBlank(contentLengthHeader) ? -1
                    : Long.parseLong(contentLengthHeader);
            repoService.assertValidDeployPath(repo, path, contentLength);
        } catch (RepoRejectException rre) {
            response.sendError(rre.getErrorCode(), rre.getMessage(), log);
            return;
        }

        // make sure the parent exists
        VfsFolder parentFolder = repo.getMutableFolder(repoPath.getParent());
        if (parentFolder == null) {
            response.sendError(HttpStatus.SC_CONFLICT,
                    "Directory cannot be created: parent doesn't exist: " + repoPath.getParent(), log);
            return;
        }

        repo.createOrGetFolder(repoPath);
        response.setStatus(HttpStatus.SC_CREATED);
    }

    @Override
    public void handleDelete(ArtifactoryRequest request, ArtifactoryResponse response) throws IOException {
        RepoPath repoPath = request.getRepoPath();
        String repoKey = repoPath.getRepoKey();
        LocalRepo localRepository = repoService.localOrCachedRepositoryByKey(repoKey);
        if (localRepository == null) {
            response.setStatus(HttpStatus.SC_NOT_FOUND);
            return;
        }

        if (!NamingUtils.isProperties(repoPath.getPath())) {
            deleteItem(response, repoPath);
        } else {
            deleteProperties(response, repoPath);
        }
    }

    private void deleteItem(ArtifactoryResponse response, RepoPath repoPath) throws IOException {
        StatusHolder statusHolder = repoService.undeploy(repoPath);
        if (statusHolder.isError()) {
            response.sendError(statusHolder);
        } else {
            response.setStatus(HttpStatus.SC_NO_CONTENT);
        }
    }

    private void deleteProperties(ArtifactoryResponse response, RepoPath repoPath) throws IOException {
        RepoPathImpl itemRepoPath = new RepoPathImpl(repoPath.getRepoKey(),
                NamingUtils.stripMetadataFromPath(repoPath.getPath()));
        boolean removed = repoService.removeProperties(itemRepoPath);
        if (removed) {
            response.setStatus(HttpStatus.SC_NO_CONTENT);
        } else {
            response.sendError(HttpStatus.SC_NOT_FOUND, "Failed to remove properties from " + itemRepoPath, log);
        }
    }

    @Override
    public void handleOptions(ArtifactoryResponse response) throws IOException {
        response.setHeader("DAV", "1,2");
        response.setHeader("Allow", WEBDAV_METHODS_LIST);
        response.setHeader("MS-Author-Via", "DAV");
        response.sendSuccess();
    }

    @Override
    public void handlePost(ArtifactoryRequest request, ArtifactoryResponse response) {
        RepoPath repoPath = request.getRepoPath();
        String repoKey = repoPath.getRepoKey();
        VirtualRepoDescriptor virtualRepo = repoService.virtualRepoDescriptorByKey(repoKey);

        StringBuilder allowHeaderBuilder = new StringBuilder();
        allowHeaderBuilder.append("GET");

        if (virtualRepo == null) {
            allowHeaderBuilder.append(",PUT,DELETE");
        }
        response.setHeader("Allow", allowHeaderBuilder.toString());
        response.setStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
    }

    @Override
    public void handleMove(ArtifactoryRequest request, ArtifactoryResponse response) throws IOException {
        RepoPath repoPath = request.getRepoPath();
        if (StringUtils.isEmpty(repoPath.getPath())) {
            response.sendError(HttpStatus.SC_BAD_REQUEST,
                    "Cannot perform MOVE action on a repository. " + "Please specify a valid path", log);
            return;
        }

        String destination = URLDecoder.decode(request.getHeader("Destination"), "UTF-8");
        if (StringUtils.isEmpty(destination)) {
            response.sendError(HttpStatus.SC_BAD_REQUEST, "Header 'Destination' is required.", log);
            return;
        }

        String targetPathWithoutContextUrl = StringUtils.remove(destination, request.getServletContextUrl());
        String targetPathParent = PathUtils.getParent(targetPathWithoutContextUrl);
        RepoPath targetPath = InternalRepoPathFactory.create(targetPathParent);
        if (!authService.canDelete(repoPath) || !authService.canDeploy(targetPath)) {
            response.sendError(HttpStatus.SC_FORBIDDEN, "Insufficient permissions.", log);
            return;
        }

        MoveMultiStatusHolder status = repoService.move(repoPath, targetPath, false, true, true);
        if (!status.hasWarnings() && !status.hasErrors()) {
            response.sendSuccess();
        } else {
            response.sendError(status);
        }
    }

    /**
     * resourcetype Return JAXP document builder instance.
     *
     * @return
     * @throws javax.servlet.ServletException
     */
    private DocumentBuilder getDocumentBuilder() throws IOException {
        DocumentBuilder documentBuilder;
        DocumentBuilderFactory documentBuilderFactory;
        try {
            documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new IOException("JAXP document builder creation failed");
        }
        return documentBuilder;
    }

    private List<String> getPropertiesFromXml(Node propNode) {
        List<String> properties;
        properties = new ArrayList<String>();
        NodeList childList = propNode.getChildNodes();
        for (int i = 0; i < childList.getLength(); i++) {
            Node currentNode = childList.item(i);
            switch (currentNode.getNodeType()) {
            case Node.TEXT_NODE:
                break;
            case Node.ELEMENT_NODE:
                String nodeName = currentNode.getNodeName();
                String propertyName;
                if (nodeName.indexOf(':') != -1) {
                    propertyName = nodeName.substring(nodeName.indexOf(':') + 1);
                } else {
                    propertyName = nodeName;
                }
                // href is a live property which is handled differently
                properties.add(propertyName);
                break;
            }
        }
        return properties;
    }

    @SuppressWarnings({ "OverlyComplexMethod" })
    private void parseProperties(ArtifactoryRequest request, XmlWriter xmlResponse, BaseBrowsableItem item,
            int type, List<String> propertiesList) throws IOException {
        RepoPath repoPath = item.getRepoPath();
        String creationDate = getIsoDate(item.getCreated());
        boolean isFolder = item.isFolder();
        String lastModified = getIsoDate(item.getLastModified());
        String resourceLength = item.getSize() + "";

        xmlResponse.writeElement(null, "response", XmlWriter.OPENING);
        String status = "HTTP/1.1 " + WebdavStatus.SC_OK + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK);

        //Generating href element
        xmlResponse.writeElement(null, "href", XmlWriter.OPENING);
        String origPath = request.getPath();
        String uri = request.getUri();
        String hrefBase = uri;
        origPath = HttpUtils.encodeQuery(origPath);
        if (origPath.length() > 0) {
            int idx = uri.lastIndexOf(origPath);
            if (idx > 0) {
                //When called recursively avoid concatenating the original path on top of itself
                hrefBase = uri.substring(0, idx);
            }
        }
        String path = repoPath.getPath();
        if (StringUtils.isNotBlank(path) && !hrefBase.endsWith("/")) {
            hrefBase += "/";
        }

        // Encode only the path since the base is already encoded
        String href = hrefBase + HttpUtils.encodeQuery(path);

        xmlResponse.writeText(href);
        xmlResponse.writeElement(null, "href", XmlWriter.CLOSING);

        String resourceName = path;
        int lastSlash = path.lastIndexOf('/');
        if (lastSlash != -1) {
            resourceName = resourceName.substring(lastSlash + 1);
        }

        switch (type) {
        case FIND_ALL_PROP:
            xmlResponse.writeElement(null, "propstat", XmlWriter.OPENING);
            xmlResponse.writeElement(null, "prop", XmlWriter.OPENING);

            xmlResponse.writeProperty(null, "creationdate", creationDate);
            xmlResponse.writeElement(null, "displayname", XmlWriter.OPENING);
            xmlResponse.writeData(resourceName);
            xmlResponse.writeElement(null, "displayname", XmlWriter.CLOSING);
            if (!isFolder) {
                xmlResponse.writeProperty(null, "getlastmodified", lastModified);
                xmlResponse.writeProperty(null, "getcontentlength", resourceLength);

                MimeType ct = NamingUtils.getMimeType(path);
                if (ct != null) {
                    xmlResponse.writeProperty(null, "getcontenttype", ct.getType());
                }
                xmlResponse.writeProperty(null, "getetag", getEtag(resourceLength, lastModified));
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.NO_CONTENT);
            } else {
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.OPENING);
                xmlResponse.writeElement(null, "collection", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.CLOSING);
            }
            xmlResponse.writeProperty(null, "source", "");
            xmlResponse.writeElement(null, "prop", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "status", XmlWriter.OPENING);
            xmlResponse.writeText(status);
            xmlResponse.writeElement(null, "status", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "propstat", XmlWriter.CLOSING);
            break;
        case FIND_PROPERTY_NAMES:
            xmlResponse.writeElement(null, "propstat", XmlWriter.OPENING);
            xmlResponse.writeElement(null, "prop", XmlWriter.OPENING);
            xmlResponse.writeElement(null, "creationdate", XmlWriter.NO_CONTENT);
            xmlResponse.writeElement(null, "displayname", XmlWriter.NO_CONTENT);
            if (!isFolder) {
                xmlResponse.writeElement(null, "getcontentlanguage", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "getcontentlength", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "getcontenttype", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "getetag", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "getlastmodified", XmlWriter.NO_CONTENT);
            }
            xmlResponse.writeElement(null, "resourcetype", XmlWriter.NO_CONTENT);
            xmlResponse.writeElement(null, "source", XmlWriter.NO_CONTENT);
            xmlResponse.writeElement(null, "lockdiscovery", XmlWriter.NO_CONTENT);
            xmlResponse.writeElement(null, "prop", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "status", XmlWriter.OPENING);
            xmlResponse.writeText(status);
            xmlResponse.writeElement(null, "status", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "propstat", XmlWriter.CLOSING);
            break;
        case FIND_BY_PROPERTY:
            //noinspection MismatchedQueryAndUpdateOfCollection
            List<String> propertiesNotFound = new ArrayList<String>();
            // Parse the list of properties
            xmlResponse.writeElement(null, "propstat", XmlWriter.OPENING);
            xmlResponse.writeElement(null, "prop", XmlWriter.OPENING);
            for (String property : propertiesList) {
                if ("creationdate".equals(property)) {
                    xmlResponse.writeProperty(null, "creationdate", creationDate);
                } else if ("displayname".equals(property)) {
                    xmlResponse.writeElement(null, "displayname", XmlWriter.OPENING);
                    xmlResponse.writeData(resourceName);
                    xmlResponse.writeElement(null, "displayname", XmlWriter.CLOSING);
                } else if ("getcontentlanguage".equals(property)) {
                    if (isFolder) {
                        propertiesNotFound.add(property);
                    } else {
                        xmlResponse.writeElement(null, "getcontentlanguage", XmlWriter.NO_CONTENT);
                    }
                } else if ("getcontentlength".equals(property)) {
                    if (isFolder) {
                        propertiesNotFound.add(property);
                    } else {
                        xmlResponse.writeProperty(null, "getcontentlength", resourceLength);
                    }
                } else if ("getcontenttype".equals(property)) {
                    if (isFolder) {
                        propertiesNotFound.add(property);
                    } else {
                        xmlResponse.writeProperty(null, "getcontenttype",
                                NamingUtils.getMimeTypeByPathAsString(path));
                    }
                } else if ("getetag".equals(property)) {
                    if (isFolder) {
                        propertiesNotFound.add(property);
                    } else {
                        xmlResponse.writeProperty(null, "getetag", getEtag(resourceLength, lastModified));
                    }
                } else if ("getlastmodified".equals(property)) {
                    if (isFolder) {
                        propertiesNotFound.add(property);
                    } else {
                        xmlResponse.writeProperty(null, "getlastmodified", lastModified);
                    }
                } else if ("source".equals(property)) {
                    xmlResponse.writeProperty(null, "source", "");
                } else {
                    propertiesNotFound.add(property);
                }
            }
            //Always include resource type
            if (isFolder) {
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.OPENING);
                xmlResponse.writeElement(null, "collection", XmlWriter.NO_CONTENT);
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.CLOSING);
            } else {
                xmlResponse.writeElement(null, "resourcetype", XmlWriter.NO_CONTENT);
            }

            xmlResponse.writeElement(null, "prop", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "status", XmlWriter.OPENING);
            xmlResponse.writeText(status);
            xmlResponse.writeElement(null, "status", XmlWriter.CLOSING);
            xmlResponse.writeElement(null, "propstat", XmlWriter.CLOSING);

            // TODO: [by fsi] Find out what this is for?
            /*
            if (propertiesNotFound.size() > 0) {
                status = "HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " " +
                        WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND);
                generatedXml.writeElement(null, "propstat", XmlWriter.OPENING);
                generatedXml.writeElement(null, "prop", XmlWriter.OPENING);
                for (String propertyNotFound : propertiesNotFound) {
                    generatedXml.writeElement(null, propertyNotFound, XmlWriter.NO_CONTENT);
                }
                generatedXml.writeElement(null, "prop", XmlWriter.CLOSING);
                generatedXml.writeElement(null, "status", XmlWriter.OPENING);
                generatedXml.writeText(status);
                generatedXml.writeElement(null, "status", XmlWriter.CLOSING);
                generatedXml.writeElement(null, "propstat", XmlWriter.CLOSING);
                
            }
            */
            break;

        }
        xmlResponse.writeElement(null, "response", XmlWriter.CLOSING);
    }

    /**
     * goes recursive through all folders. used by propfind
     */
    private void recursiveParseProperties(ArtifactoryRequest request, ArtifactoryResponse response,
            XmlWriter generatedXml, BaseBrowsableItem currentItem, int propertyFindType, List<String> properties,
            int depth) throws IOException {

        parseProperties(request, generatedXml, currentItem, propertyFindType, properties);

        if (depth <= 0) {
            return;
        }

        if (currentItem.isFolder()) {
            BrowsableItemCriteria criteria = new BrowsableItemCriteria.Builder(currentItem.getRepoPath()).build();
            List<BaseBrowsableItem> browsableChildren = repoBrowsing.getLocalRepoBrowsableChildren(criteria);
            for (BaseBrowsableItem child : browsableChildren) {
                recursiveParseProperties(request, response, generatedXml, child, propertyFindType, properties,
                        depth - 1);
            }
        }
    }

    /**
     * Get the ETag associated with a file.
     */
    private static String getEtag(String resourceLength, String lastModified) throws IOException {
        return "W/\"" + resourceLength + "-" + lastModified + "\"";
    }

    /**
     * Get creation date in ISO format.
     */
    private static synchronized String getIsoDate(long creationDate) {
        return creationDateFormat.format(new Date(creationDate));
    }

    private void logWebdavRequest(Document document) throws TransformerException {
        if (log.isDebugEnabled()) {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            StringWriter writer = new StringWriter();
            transformer.transform(new DOMSource(document), new StreamResult(writer));
            log.debug("Webdav request body:\n" + writer.toString());
        }
    }
}