org.exist.webdav.MiltonDocument.java Source code

Java tutorial

Introduction

Here is the source code for org.exist.webdav.MiltonDocument.java

Source

/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2010 The eXist Project
 *  http://exist-db.org
 *
 *  This program 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 2
 *  of the License, or (at your option) any later version.
 *
 *  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
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  $Id$
 */
package org.exist.webdav;

import com.bradmcevoy.http.CollectionResource;
import com.bradmcevoy.http.Auth;
import com.bradmcevoy.http.CopyableResource;
import com.bradmcevoy.http.DeletableResource;
import com.bradmcevoy.http.GetableResource;
import com.bradmcevoy.http.HttpManager;
import com.bradmcevoy.http.LockInfo;
import com.bradmcevoy.http.LockResult;
import com.bradmcevoy.http.LockTimeout;
import com.bradmcevoy.http.LockToken;
import com.bradmcevoy.http.LockableResource;
import com.bradmcevoy.http.MoveableResource;
import com.bradmcevoy.http.PropFindableResource;
import com.bradmcevoy.http.Range;
import com.bradmcevoy.http.exceptions.BadRequestException;
import com.bradmcevoy.http.exceptions.ConflictException;
import com.bradmcevoy.http.exceptions.LockedException;
import com.bradmcevoy.http.exceptions.NotAuthorizedException;
import com.bradmcevoy.http.exceptions.PreConditionFailedException;
import com.bradmcevoy.http.webdav.DefaultUserAgentHelper;
import com.bradmcevoy.http.webdav.UserAgentHelper;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Map;
import java.util.Properties;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.commons.io.IOUtils;

import org.exist.EXistException;
import org.exist.storage.BrokerPool;
import org.exist.security.PermissionDeniedException;
import org.exist.security.Subject;
import org.exist.util.VirtualTempFile;
import org.exist.webdav.ExistResource.Mode;
import org.exist.webdav.exceptions.DocumentAlreadyLockedException;
import org.exist.webdav.exceptions.DocumentNotLockedException;
import org.exist.xmldb.XmldbURI;

/**
 * Class for representing an eXist-db document as a Milton WebDAV document.
 * See <a href="http://milton.ettrema.com">Milton</a>.
 *
 * @author Dannes Wessels (dizzzz_at_exist-db.org)
 */
public class MiltonDocument extends MiltonResource implements GetableResource, PropFindableResource,
        DeletableResource, LockableResource, MoveableResource, CopyableResource {

    public static final String PROPFIND_METHOD_XML_SIZE = "org.exist.webdav.PROPFIND_METHOD_XML_SIZE";
    public static final String GET_METHOD_XML_SIZE = "org.exist.webdav.GET_METHOD_XML_SIZE";

    private ExistDocument existDocument;

    private VirtualTempFile vtf = null;;

    // Only for PROPFIND the estimate size for an XML document must be shown
    private boolean isPropFind = false;

    private enum SIZE_METHOD {
        NULL, EXACT, APPROXIMATE
    };

    private static SIZE_METHOD propfindSizeMethod = null;
    private static SIZE_METHOD getSizeMethod = null;

    private static UserAgentHelper userAgentHelper = null;

    /**
     * Set to TRUE if getContentLength is used for PROPFIND.
     */
    public void setIsPropFind(boolean isPropFind) {
        this.isPropFind = isPropFind;
    }

    /**
     *  Constructor of representation of a Document in the Milton framework, without subject information.
     * To be called by the resource factory.
     *
     * @param host  FQ host name including port number.
     * @param uri   Path on server indicating path of resource
     * @param brokerPool Handle to Exist database.
     */
    public MiltonDocument(String host, XmldbURI uri, BrokerPool brokerPool) {
        this(host, uri, brokerPool, null);
    }

    /**
     *  Constructor of representation of a Document in the Milton framework, with subject information.
     * To be called by the resource factory.
     *
     * @param host  FQ host name including port number.
     * @param uri   Path on server indicating path of resource.
     * @param subject  An Exist operation is performed with  User. Can be NULL.
     * @param pool Handle to Exist database.
     */
    public MiltonDocument(String host, XmldbURI uri, BrokerPool pool, Subject subject) {

        super();

        if (userAgentHelper == null) {
            userAgentHelper = new DefaultUserAgentHelper();
        }

        if (LOG.isTraceEnabled()) {
            LOG.trace(String.format("DOCUMENT:%s", uri.toString()));
        }

        resourceXmldbUri = uri;
        brokerPool = pool;
        this.host = host;

        existDocument = new ExistDocument(uri, brokerPool);

        // store simpler type
        existResource = existDocument;

        if (subject != null) {
            existDocument.setUser(subject);
            existDocument.initMetadata();
        }

        // PROPFIND method 
        if (propfindSizeMethod == null) {
            // get user supplied preferred size determination approach
            String systemProp = System.getProperty(PROPFIND_METHOD_XML_SIZE);

            if (systemProp == null) {
                // Default method is approximate
                propfindSizeMethod = SIZE_METHOD.APPROXIMATE;

            } else {
                // Try to parse from environment property
                try {
                    propfindSizeMethod = SIZE_METHOD.valueOf(systemProp.toUpperCase());

                } catch (IllegalArgumentException ex) {
                    LOG.debug(ex.getMessage());
                    // Set preffered default
                    propfindSizeMethod = SIZE_METHOD.APPROXIMATE;
                }
            }
        }

        // GET method
        if (getSizeMethod == null) {
            // get user supplied preferred size determination approach
            String systemProp = System.getProperty(GET_METHOD_XML_SIZE);

            if (systemProp == null) {
                // Default method is NULL
                getSizeMethod = SIZE_METHOD.NULL;

            } else {
                // Try to parse from environment property
                try {
                    getSizeMethod = SIZE_METHOD.valueOf(systemProp.toUpperCase());

                } catch (IllegalArgumentException ex) {
                    LOG.debug(ex.getMessage());
                    // Set preffered default
                    getSizeMethod = SIZE_METHOD.APPROXIMATE;
                }
            }
        }

    }

    /* ================
     * GettableResource
     * ================ */

    @Override
    public void sendContent(OutputStream out, Range range, Map<String, String> params, String contentType)
            throws IOException, NotAuthorizedException, BadRequestException {
        try {
            if (vtf == null) {
                LOG.debug("Serializing from database");
                existDocument.stream(out);

            } else {
                // Experimental. Does not work right, the virtual file
                // Often does not contain the right amount of bytes.

                LOG.debug("Serializing from buffer");
                InputStream is = vtf.getByteStream();
                IOUtils.copy(is, out);
                out.flush();
                IOUtils.closeQuietly(is);
                vtf.delete();
                vtf = null;
            }

        } catch (PermissionDeniedException e) {
            LOG.debug(e.getMessage());
            throw new NotAuthorizedException(this);

        } finally {
            IOUtils.closeQuietly(out);
        }
    }

    @Override
    public Long getMaxAgeSeconds(Auth auth) {
        return null;
    }

    @Override
    public String getContentType(String accepts) {
        return existDocument.getMimeType();
    }

    @Override
    public Long getContentLength() {

        // Note
        // Whilst for non-XML documents the exact size of the documents can
        // be determined by checking the administration, this is not possible
        // for XML documents.
        //
        // For XML documents by default the 'approximate' size is available
        // which can be sufficient (pagesize * nr of pages). Exact size
        // is dependant on many factors, the serialization parameters.
        //
        // The approximate size is a good indication of the size of document
        // but some WebDAV client, mainly the MacOsX Finder version, can
        // not deal with this guesstimate, resulting in incomplete or overcomplete
        // documents.
        //
        // Special for this, two system variables can be set to change the
        // way the size is calculated. Supported values are
        // NULL, EXACT, APPROXIMATE
        //
        // PROPFIND: Unfortunately both NULL and APPROXIMATE do not work for 
        // MacOsX Finder. The default behaviour for the Finder 'user-agent' is 
        // exact, for the others it is approximate. 
        // This behaviour is swiched by the system properties.
        //
        // GET: the NULL value seems to be working well for macosx too.

        Long size = null;

        // MacOsX has a bad reputation
        boolean isMacFinder = userAgentHelper.isMacFinder(HttpManager.request().getUserAgentHeader());

        if (existDocument.isXmlDocument()) {
            // XML document, exact size is not (directly) known)

            if (isPropFind) {

                // PROPFIND
                // In this scensario the XML document is not actually
                // downloaded, only the size needs to be known.
                // This is the most expensive scenario

                if (isMacFinder || SIZE_METHOD.EXACT == propfindSizeMethod) {

                    // Returns the exact size, default behaviour for Finder, 
                    // or when set by a system property

                    if (LOG.isDebugEnabled()) {
                        LOG.debug(String.format("Serializing XML to /dev/null to determine size (%s) MacFinder=%s",
                                resourceXmldbUri, isMacFinder));
                    }

                    // Stream document to '/dev/null' and count bytes
                    ByteCountOutputStream counter = new ByteCountOutputStream();
                    try {
                        existDocument.stream(counter);

                    } catch (Exception ex) {
                        LOG.error(ex);
                    }

                    size = counter.getByteCount();

                } else if (SIZE_METHOD.NULL == propfindSizeMethod) {

                    // Returns size unknown. This is not supported
                    // by MacOsX finder

                    size = null;

                } else {
                    // Returns the estimated document size. This is the
                    // default value, but not suitable for MacOsX Finder.
                    size = existDocument.getContentLength();
                }

            } else {

                // GET
                // In this scenario, the document will actually be downloaded
                // in the next step.

                if (SIZE_METHOD.EXACT == getSizeMethod) {

                    // Return the exact size by pre-serializing the document
                    // to a buffer first. isMacFinder is not needed

                    try {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(String.format("Serializing XML to virtual file (%s)", resourceXmldbUri));
                        }

                        vtf = new VirtualTempFile();
                        existDocument.stream(vtf);
                        vtf.close();

                    } catch (Exception ex) {
                        LOG.error(ex);
                    }

                    size = vtf.length();

                } else if (SIZE_METHOD.APPROXIMATE == getSizeMethod) {

                    // Return approximate size, be warned to use this

                    size = existDocument.getContentLength();
                    vtf = null; // force live serialization

                } else {

                    // Return no size, the whole file will be downloaded
                    // Works well for macosx finder

                    size = null;
                    vtf = null; // force live serialization
                }
            }

        } else {
            // Non XML document, actual size is known
            size = existDocument.getContentLength();
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Size=%s (%s)", size, resourceXmldbUri));
        }
        return size;

    }

    /* ====================
     * PropFindableResource
     * ==================== */

    @Override
    public Date getCreateDate() {
        Date createDate = null;

        Long time = existDocument.getCreationTime();
        if (time != null) {
            createDate = new Date(time);
        }

        return createDate;
    }

    /* =================
     * DeletableResource
     * ================= */

    @Override
    public void delete() throws NotAuthorizedException, ConflictException, BadRequestException {
        existDocument.delete();
    }

    /* ================
     * LockableResource
     * ================ */

    @Override
    public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
            throws NotAuthorizedException, PreConditionFailedException, LockedException {

        org.exist.dom.LockToken inputToken = convertToken(timeout, lockInfo);

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Lock: %s", resourceXmldbUri));
        }

        LockResult lr = null;
        try {
            org.exist.dom.LockToken existLT = existDocument.lock(inputToken);

            // Process result
            LockToken mltonLT = convertToken(existLT);
            lr = LockResult.success(mltonLT);

        } catch (PermissionDeniedException ex) {
            LOG.debug(ex.getMessage());
            throw new NotAuthorizedException(this);

        } catch (DocumentAlreadyLockedException ex) {
            // set result iso throw new LockedException(this);
            LOG.debug(ex.getMessage());
            lr = LockResult.failed(LockResult.FailureReason.ALREADY_LOCKED);

        } catch (EXistException ex) {
            LOG.debug(ex.getMessage());
            lr = LockResult.failed(LockResult.FailureReason.PRECONDITION_FAILED);

        }
        return lr;
    }

    @Override
    public LockResult refreshLock(String token) throws NotAuthorizedException, PreConditionFailedException {

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Refresh: %s token=%s", resourceXmldbUri, token));
        }

        LockResult lr = null;
        try {
            org.exist.dom.LockToken existLT = existDocument.refreshLock(token);

            // Process result
            LockToken mltonLT = convertToken(existLT);
            lr = LockResult.success(mltonLT);

        } catch (PermissionDeniedException ex) {
            LOG.debug(ex.getMessage());
            throw new NotAuthorizedException(this);

        } catch (DocumentNotLockedException | EXistException ex) {
            LOG.debug(ex.getMessage());
            lr = LockResult.failed(LockResult.FailureReason.PRECONDITION_FAILED);

        } catch (DocumentAlreadyLockedException ex) {
            //throw new LockedException(this);
            LOG.debug(ex.getMessage());
            lr = LockResult.failed(LockResult.FailureReason.ALREADY_LOCKED);

        }
        return lr;
    }

    @Override
    public void unlock(String tokenId) throws NotAuthorizedException, PreConditionFailedException {

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Unlock: %s", resourceXmldbUri));
        }

        try {
            existDocument.unlock();
        } catch (PermissionDeniedException ex) {
            LOG.debug(ex.getMessage());
            throw new NotAuthorizedException(this);

        } catch (DocumentNotLockedException | EXistException ex) {
            LOG.debug(ex.getMessage());
            throw new PreConditionFailedException(this);

        }
    }

    @Override
    public LockToken getCurrentLock() {

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("getCurrentLock: %s", resourceXmldbUri));
        }

        org.exist.dom.LockToken existLT = existDocument.getCurrentLock();

        if (existLT == null) {
            LOG.debug("No database lock token.");
            return null;
        }

        // Construct Lock Info
        LockToken miltonLT = convertToken(existLT);

        // Return values in Milton object
        return miltonLT;
    }

    /* ================
     * MoveableResource
     * ================ */

    @Override
    public void moveTo(CollectionResource rDest, String newName) throws ConflictException {

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("moveTo: %s newName=%s", resourceXmldbUri, newName));
        }

        XmldbURI destCollection = ((MiltonCollection) rDest).getXmldbUri();
        try {
            existDocument.resourceCopyMove(destCollection, newName, Mode.MOVE);

        } catch (EXistException ex) {
            throw new ConflictException(this);
        }
    }

    /* ================
     * CopyableResource
     * ================ */

    @Override
    public void copyTo(CollectionResource rDest, String newName) {

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("copyTo: %s newName=%s", resourceXmldbUri, newName));
        }

        XmldbURI destCollection = ((MiltonCollection) rDest).getXmldbUri();
        try {
            existDocument.resourceCopyMove(destCollection, newName, Mode.COPY);

        } catch (EXistException ex) {
            // unable to throw new ConflictException(this);
            LOG.error(ex.getMessage());
        }
    }

    /* ================
     * StAX serializer
     * ================ */

    /**
     *  Serialize document properties
     * 
     * @param writer STAX writer
     * @throws XMLStreamException Thrown when writing data failed
     */
    public void writeXML(XMLStreamWriter writer) throws XMLStreamException {
        writer.writeStartElement("exist", "document", "http://exist.sourceforge.net/NS/exist");
        writer.writeAttribute("name", resourceXmldbUri.lastSegment().toString());
        writer.writeAttribute("created", getXmlDateTime(existDocument.getCreationTime()));
        writer.writeAttribute("last-modified", getXmlDateTime(existDocument.getLastModified()));
        writer.writeAttribute("owner", existDocument.getOwnerUser());
        writer.writeAttribute("group", existDocument.getOwnerGroup());
        writer.writeAttribute("permissions", "" + existDocument.getPermissions().toString());
        writer.writeAttribute("size", "" + existDocument.getContentLength());
        writer.writeEndElement();
    }

    /**
     * Set specific WebDAV serialization options
     * 
     * @param config XML serialization options
     */
    public void setConfiguration(Properties config) {
        existDocument.setConfiguration(config);
    }
}