/* * 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.Auth; import com.bradmcevoy.http.CollectionResource; 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 javax.xml.transform.TransformerException; import org.apache.commons.io.IOUtils; import org.exist.EXistException; import org.exist.storage.BrokerPool; import org.exist.security.PermissionDeniedException; import org.exist.security.User; import org.exist.util.VirtualTempFile; import org.exist.util.serializer.XMLWriter; 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 user 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 user 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 user An Exist operation is performed with User. Can be NULL. * @param pool Handle to Exist database. */ public MiltonDocument(String host, XmldbURI uri, BrokerPool pool, User user) { super(); if(userAgentHelper==null){ userAgentHelper=new DefaultUserAgentHelper(); } if(LOG.isTraceEnabled()) LOG.trace("DOCUMENT:" + uri.toString()); resourceXmldbUri = uri; brokerPool = pool; this.host = host; existDocument = new ExistDocument(uri, brokerPool); // store simpler type existResource = existDocument; if (user != null) { existDocument.setUser(user); 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 LOG.debug("Serializing XML to /dev/null to determine size" + " (" + resourceXmldbUri + ") MacFinder="+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 = 0L + 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 { LOG.debug("Serializing XML to virtual file" + " (" + 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 = 0L + 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 = 0L + existDocument.getContentLength(); } LOG.debug("Size=" + 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("Lock: " + 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("Refresh: " + resourceXmldbUri + " token=" + 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 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); } catch (EXistException ex) { LOG.debug(ex.getMessage()); lr = LockResult.failed(LockResult.FailureReason.PRECONDITION_FAILED); } return lr; } //@Override public void unlock(String tokenId) throws NotAuthorizedException, PreConditionFailedException { if(LOG.isDebugEnabled()) LOG.debug("Unlock: " + resourceXmldbUri); try { existDocument.unlock(); } catch (PermissionDeniedException ex) { LOG.debug(ex.getMessage()); throw new NotAuthorizedException(this); } catch (DocumentNotLockedException ex) { LOG.debug(ex.getMessage()); throw new PreConditionFailedException(this); } catch (EXistException ex) { LOG.debug(ex.getMessage()); throw new PreConditionFailedException(this); } } //@Override public LockToken getCurrentLock() { if(LOG.isDebugEnabled()) LOG.debug("getLock: " + 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("moveTo: " + resourceXmldbUri + " newName=" + 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("copyTo: " + resourceXmldbUri + " newName=" + 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 * ================ */ public void writeXML(XMLWriter xw) throws TransformerException { xw.startElement("document"); xw.attribute("name", resourceXmldbUri.lastSegment().toString()); xw.attribute("created", getXmlDateTime(existDocument.getCreationTime())); xw.attribute("last-modified", getXmlDateTime(existDocument.getLastModified())); xw.attribute("owner", existDocument.getOwnerUser()); xw.attribute("group", existDocument.getOwnerGroup()); xw.attribute("permissions", "" + existDocument.getPermissions().toString()); xw.attribute("size", "" + existDocument.getContentLength()); xw.endElement("document"); } }