/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/wvcm/src/org/apache/wvcm/store/webdav/ResourceWebdavAccessor.java,v 1.18 2005/03/04 18:14:31 pnever Exp $
 * $Revision: 1.18 $
 * $Date: 2005/03/04 18:14:31 $
 *
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. 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.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Slide", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 APACHE SOFTWARE FOUNDATION OR
 * ITS 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */

package org.apache.wvcm.store.webdav;
import org.apache.webdav.methods.*;
import org.apache.wvcm.store.webdav.request.*;
import org.apache.wvcm.store.webdav.response.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.wvcm.LockToken;
import javax.wvcm.PropertyNameList;
import javax.wvcm.PropertyNameList.AttributeName;
import javax.wvcm.PropertyNameList.NestedPropertyName;
import javax.wvcm.PropertyNameList.PropertyName;
import javax.wvcm.Resource;
import javax.wvcm.SearchToken;
import javax.wvcm.WvcmException;
import javax.wvcm.WvcmException.ReasonCode;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.wvcm.LocationImpl;
import org.apache.wvcm.LockTokenImpl;
import org.apache.wvcm.ProviderImpl;
import org.apache.wvcm.ResourceImpl;
import org.apache.wvcm.store.ResourceAccessor;
import org.apache.wvcm.store.webdav.request.ProppatchRequest.PropertyUpdate;
import org.apache.wvcm.store.webdav.request.ReportRequest.ExpandPropertyReportRequest;
import org.apache.wvcm.store.webdav.response.NoProptatMultistatusResponse.ResourceStatus;
import org.apache.wvcm.store.webdav.response.ProppatchMultistatusResponse.Propstat;
import org.apache.wvcm.util.PropertyNameLists;


/**
 * WebDAV-based implementation of ResourceAccessor.
 *
 * @author <a href="mailto:peter.nevermann@softwareag.com">Peter Nevermann</a>
 * @version $Revision: 1.18 $
 */
public class ResourceWebdavAccessor extends WebdavAccessor implements ResourceAccessor {
    
    private Resource resource;
    
    /**
     * Constructor.
     */
    public ResourceWebdavAccessor( Resource resource ) {
        LocationImpl locationImpl = (LocationImpl)resource.location();
        this.resource = resource;
        this.providerImpl = (ProviderImpl)locationImpl.provider();
        initialize( locationImpl.hostname(), locationImpl.port() );
    }
    
    /**
     * Get the resource proxy this accessor works for.
     *
     * @return   a ResourceImpl
     */
    public Resource resource() {
        return resource;
    }
    
    /**
     * Get the HttpClient of this accessor.
     *
     * @return   a HttpClient
     */
    protected HttpClientWrapper client() {
        String hostname = ((LocationImpl)resource.location()).hostname();
        int port = ((LocationImpl)resource.location()).port();
        return getHttpClient( providerImpl, hostname, port );
    }
    
    protected RetryHandler retryHandler(InputStream content) {
        return new RetryHandler(content);
    }
    
    protected RetryHandler retryHandler(String content) {
        return new RetryHandler(content);
    }
    
    protected RetryHandler retryHandler() {
        return retryHandler((String)null);
    }
    
    /**
     * Return a new resource proxy at the location of the associated resource containing
     * the wanted properties. The exact resource type of the resulting resource proxy
     * is determined by this method.
     * The resource content is written to <code>content</code>
     * and <code>content</code> is closed.
     *
     * @param    wantedPropertyList the wanted properties
     * @param    content            an OutputStream
     * @return   a new resource proxy containing the wanted properties
     * @throws   WvcmException
     */
    public Resource doReadContent(PropertyNameList wantedPropertyList, OutputStream content) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        GetMethod method = new GetMethod(loc.escapedPath());
        method.addRequestHeader( "Content-Length", "0" );
        String contentCharset = "";
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            contentCharset = method.getResponseCharSet();
            switch( sc ) {
                case HttpStatus.SC_OK:
                    InputStream rb = method.getResponseBodyAsStream();
                    int x = rb.read();
                    while( x >= 0 ) {
                        content.write( x );
                        x= rb.read();
                    }
                    content.flush();
                    content.close();
                    break;
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
        
        Resource result;
        if (wantedPropertyList == PropertyNameList.EMPTY_DO_NOT_DETERMINE_RESOURCETYPE) {
            // PERFORMANCE:
            // This is to save the extra roundtrip (doReadProperties) to the server.
            // The exact resourcetype is not determined. Instead, a new resource proxy
            // of the same resourcetype as the request resource is returned
            return new ResourceProxyFactory(resource, providerImpl, null).create(resource.location(), resource.getClass(), null);
        }
        else {
            result = doReadProperties( wantedPropertyList );
            if( PropertyNameLists.contains(wantedPropertyList, PropertyName.CONTENT_CHARACTER_SET.getString()) )
                result.setContentCharacterSet( contentCharset );
        }
        return result;
    }
    
    /**
     * Return a new resource proxy at the location of the associated resource containing
     * the wanted properties. The exact resource type of the resulting resource proxy
     * is determined by this method.
     *
     * @param    wantedPropertyList the wanted properties
     * @return   a new resource proxy containing the wanted properties
     * @throws   WvcmException
     */
    public Resource doReadProperties( PropertyNameList wantedPropertyList ) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        EntityEnclosingMethod method = null;
        AbstractRequest req = null;
        if (containsNestedProperties(wantedPropertyList)) {
            method = new ReportMethod(loc.escapedPath());
            req = new ExpandPropertyReportRequest(wantedPropertyList);
        }
        else {
            method = new PropfindMethod(loc.escapedPath());
            req = new PropfindRequest(wantedPropertyList);
        }
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        method.addRequestHeader( "Depth", "0" );
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_MULTI_STATUS:
                    MultistatusResponse resp =
                        new MultistatusResponse( resource, method.getResponseBodyAsStream(), wantedPropertyList );
                    return (ResourceImpl)resp.createResourceProxy();
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    protected boolean containsNestedProperties(PropertyNameList pnl) {
        if (pnl == null) {
            return false;
        }
        PropertyName[] pna = pnl.getPropertyNames();
        for (int i = 0; i < pna.length; i++) {
            if (pna[i] instanceof NestedPropertyName) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Persists content changes to a resource.
     * If <code>contentIdentifier</code> matches the current
     * state identifier of the persistent resource,
     * the content of the resource is replaced with the
     * bytes read from <code>content</code>, and <code>content</code>
     * is then closed.
     * If reading from the stream throws a <code>java.io.IOException</code>,
     * then no further data will be read from the stream,
     * and after attempting to close the stream, a <code>WvcmException</code>
     * wrapping the <code> IOException</code> will be thrown,
     * possibly leading to incomplete data being stored on the resource.
     * @throws WvcmException if the resource identified by this {@link Resource}
     * does not exist.
     *
     * @param    content the content input stream
     * @param    contentIdentifier the content identifier to match the current identifier
     * @throws   WvcmException
     */
    public void doWriteContent( InputStream content, String contentIdentifier ) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        PutMethod method = new PutMethod(loc.escapedPath());
        if (contentIdentifier != null) {
            method.setRequestHeader( "If-Match", "\""+contentIdentifier+"\"" );
        }
        method.setRequestBody( content );
        String ctype = null;
        try {
            ctype = resource.getContentType();
        } catch (WvcmException x) {} // ignore silently
        
        if( ctype != null && ctype.length() > 0 ) {
            StringBuffer ctypeHeader = new StringBuffer( ctype );
            String charset = null;
            try {
                charset = resource.getContentCharacterSet();
            } catch (WvcmException y) {} // ignore silently
            
            if( charset != null && charset.length() > 0 ) {
                ctypeHeader.append( "; charset=\""+charset+"\"" );
            }
            method.setRequestHeader( "Content-Type", ctypeHeader.toString() );
        }
        
        addLockTokens(method);
        
        try {
            method = (PutMethod)retryHandler(content).executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_NO_CONTENT:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.WRITE_FAILED, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * Deletes the resource at the location of the associated resource.
     * If a folder is deleted, its members are deleted as well.
     */
    
    public void doWriteProperties() throws WvcmException {
        
        // TODO: prio=m, effort=1.0, descr=(allow CONTENT_TYPE and CONTENT_CHARACTER_SET to be set as any other prop at expense of extra PUT)
        
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        ProppatchMethod method = new ProppatchMethod(loc.escapedPath());
        ProppatchRequest req = new ProppatchRequest( propertyUpdates(resource) );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        addLockTokens(method);
        
        try {
            method = (ProppatchMethod)retryHandler(req.reqBodyAsString()).executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_MULTI_STATUS:
                    ProppatchMultistatusResponse resp =
                        new ProppatchMultistatusResponse( resource, method.getResponseBodyAsStream() );
                    List failures = resp.getPropstatList(true);
                    if (!failures.isEmpty()) {
                        Propstat failure = (Propstat)failures.get(0);
                        throw new WvcmException(
                            failure.getMessage(), resourcepath, ReasonCode.CANNOT_MODIFY_PROPERTIES, null);
                    }
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CANNOT_MODIFY_PROPERTIES, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    private Map propertyUpdates( Resource resource ) throws WvcmException {
        ResourceImpl r = (ResourceImpl)resource;
        Map result = new HashMap();
        Iterator i = r.listOfSetProperties().iterator();
        while( i.hasNext() ) {
            PropertyName pname = (PropertyName)i.next();
            Object value = null;
            try {
                value = r.getProperty(pname);
            } catch (WvcmException e) {
                e.printStackTrace();
            }
            result.put( pname, new PropertyUpdate(value, false) );
        }
        i = r.listOfRemovedAttributes().iterator();
        while( i.hasNext() ) {
            AttributeName pname = (AttributeName)i.next();
            result.put( pname, new PropertyUpdate(null, true) );
        }
        return result;
    }
    
    public void doDelete() throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        DeleteMethod method = new DeleteMethod(loc.escapedPath());
        addLockTokens(method);
        
        try {
            method = (DeleteMethod)retryHandler().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_NO_CONTENT:
                case HttpStatus.SC_NOT_FOUND:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                case HttpStatus.SC_MULTI_STATUS:
                    NoProptatMultistatusResponse resp =
                        new NoProptatMultistatusResponse(resource(), method.getResponseBodyAsStream());
                    List rsL = resp.getResourceStatusList(true);
                    if (rsL.size() > 0) {
                        throw new WvcmException(
                            st, resourcepath, ReasonCode.CANNOT_UNBIND_RESOURCE, getNestedExceptions(rsL));
                    }
                    else {
                        break;
                    }
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CANNOT_UNBIND_RESOURCE, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    protected Exception[] getNestedExceptions(List rsL) {
        Exception[] nestedExceptions = new Exception[rsL.size()];
        for (int i = 0; i < rsL.size(); i++) {
            ResourceStatus rs = (ResourceStatus)rsL.get(i);
            ReasonCode rc;
            switch (rs.getStatus()) {
                case HttpStatus.SC_UNAUTHORIZED:
                    rc = ReasonCode.UNAUTHORIZED;
                    break;
                case HttpStatus.SC_LOCKED:
                    rc = ReasonCode.LOCKED;
                    break;
                default:
                    rc = ReasonCode.CANNOT_UNBIND_RESOURCE;
                    break;
            }
            nestedExceptions[i] = new WvcmException(rs.getMessage(),
                                                    rs.getResourcePath(),
                                                    rc,
                                                    null);
        }
        return nestedExceptions;
    }
    
    /**
     * Moves the resource identified by associated proxy
     * to the location identified by the <code>destination</code>.
     * @param destination The new location of the resource.
     * @param overwrite If <code>false</code> the existence of a resource
     * at the destination will cause doMove to fail; otherwise,
     * doMove will replace the destination resource.
     */
    public void doMove(String destination, boolean overwrite) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        LocationImpl destloc = new LocationImpl(destination, providerImpl);
        String resourcepath = loc.path();
        MoveMethod method = new MoveMethod(loc.escapedPath());
        method.addRequestHeader( "Destination", destloc.escapedPath() );
        method.addRequestHeader( "Overwrite", (overwrite ? "T" : "F") );
        method.addRequestHeader( "Content-Length", "0" );
        addLockTokens(method);
        
        try {
            method = (MoveMethod)retryHandler().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_CREATED:
                case HttpStatus.SC_NO_CONTENT:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CONFLICT, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * Create a copy of the resource identified by the associated proxy
     * at the location identified by the <code>destination</code>.
     * @param destination The location of the new resource created by doCopy.
     * @param overwrite If <code>false</code> the existence of a resource
     * at the destination will cause the copy to fail; otherwise,
     * doCopy will replace the destination resource.
     */
    public void doCopy(String destination, boolean overwrite) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        LocationImpl destloc = new LocationImpl(destination, providerImpl);
        String resourcepath = loc.path();
        CopyMethod method = new CopyMethod(loc.escapedPath());
        method.addRequestHeader( "Destination", destloc.escapedPath() );
        method.addRequestHeader( "Overwrite", (overwrite ? "T" : "F") );
        method.addRequestHeader( "Content-Length", "0" );
        addLockTokens(method);
        
        try {
            method = (CopyMethod)retryHandler().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_CREATED:
                case HttpStatus.SC_NO_CONTENT:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                case HttpStatus.SC_MULTI_STATUS:
                    NoProptatMultistatusResponse resp =
                        new NoProptatMultistatusResponse(resource, method.getResponseBodyAsStream());
                    List rsL = resp.getResourceStatusList(true);
                    if (rsL.size() > 0) {
                        throw new WvcmException(
                            st, resourcepath, ReasonCode.CANNOT_UNBIND_RESOURCE, getNestedExceptions(rsL));
                    }
                    else {
                        break;
                    }
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CONFLICT, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    
    /**
     * Checks whether the associated resource exists at the storage where this accessor
     * is in charge of.
     *
     * @return   true, if the resource exists
     * @throws   WvcmException
     */
    public boolean doCheckExists() throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        HttpMethod method = new HeadMethod(loc.escapedPath());
        method.addRequestHeader( "Content-Length", "0" );
        boolean exists = false;
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_OK:
                    exists = true;
                    break;
                case HttpStatus.SC_NOT_FOUND:
                    exists = false;
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        "Unauthorized", resourcepath, ReasonCode.UNAUTHORIZED, null);
                default:
                    throw new WvcmException(
                        "Unable to complete operation", resourcepath, ReasonCode.FORBIDDEN, null);
            }
            return exists;
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * Return a list of {@link Resource} objects containing the wanted properties
     * according to the conditions of the specified search token from the scope
     * defined by the associated resource.
     * A requested property named XXX can be retrieved from
     * the resource with the <code>getXxx</code> method.
     *
     * @param    wantedPropertyList  the wanted properties
     * @param    searchToken         a  SearchToken
     * @return   the result list
     * @throws   WvcmException
     */
    public List doSearch(PropertyNameList wantedPropertyList, SearchToken searchToken) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        SearchMethod method = new SearchMethod(loc.escapedPath());
        SearchRequest req = new SearchRequest( resourcepath, wantedPropertyList, searchToken );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_MULTI_STATUS:
                    MultistatusResponse resp =
                        new MultistatusResponse( resource, method.getResponseBodyAsStream(), wantedPropertyList );
                    return resp.createResourceProxies();
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    public ResourceImpl doReadAllAttributes( PropertyNameList wantedPropertyList ) throws WvcmException {
        PropertyNameList allAttributes =
            PropertyNameLists.extractAllAttributes( wantedPropertyList );
        if( allAttributes == null )
            return null;
        
        LocationImpl loc = (LocationImpl)resource.location();
        String resourcepath = loc.path();
        PropfindMethod method = new PropfindMethod(loc.escapedPath());
        PropfindRequest req = new PropfindRequest( allAttributes );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        method.addRequestHeader( "Depth", "0" );
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_MULTI_STATUS:
                    MultistatusResponse resp =
                        new MultistatusResponse( resource, method.getResponseBodyAsStream(), allAttributes );
                    return (ResourceImpl)resp.createAllAttributesProxy();
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    protected void doLabel(String label, int operator) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource().location();
        String resourcepath = loc.path();
        LabelMethod method = new LabelMethod(loc.escapedPath());
        LabelRequest req = new LabelRequest( label, operator );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        addLockTokens(method);
        
        try {
            method = (LabelMethod)retryHandler(req.reqBodyAsString()).executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_OK:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CANNOT_CHECK_IN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Write failed", resourcepath, ReasonCode.WRITE_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * Return the list of {@link AccessControlElement} instances (ACL) defined
     * on the associated resource.
     * The ACL specifies the list of access control elements (ACEs), which define what principals
     * are to get what privileges for this resource.
     * Each ACE specifies the set of privileges to be either granted or denied to a single principal.
     * If the ACL is empty, no principal is granted any privilege.
     *
     * @param    includeInherited    if false, only ACEs defined for the resource are returned;
     *                               otherwise, the ACL includes all inherited ACEs
     * @return   a List
     * @throws   WvcmException
     */
    public List doReadAccessControlList(boolean includeInherited) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource().location();
        PropertyNameList acl = new PropertyNameList(
            new PropertyName[]{
                new AttributeName("DAV:", "acl")
            }
        );
        String resourcepath = loc.path();
        PropfindMethod method = new PropfindMethod(loc.escapedPath());
        PropfindRequest req = new PropfindRequest( acl, false );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        method.addRequestHeader( "Depth", "0" );
        
        try {
            client().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_MULTI_STATUS:
                    MultistatusResponse resp =
                        new MultistatusResponse( resource(), method.getResponseBodyAsStream() );
                    return (List)resp.createAccessControlList(includeInherited);
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * Modifies the access control list (ACL) of the associated resource. Specifically, this method only
     * permits modification to ACEs that are not inherited, and are not protected.
     *
     * @param    acl                 a list of {@link AccessControlElement} instances
     * @throws   WvcmException
     */
    public void doWriteAccessControlList(List acl) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource().location();
        String resourcepath = loc.path();
        AclMethod method = new AclMethod(loc.escapedPath());
        AclRequest req = new AclRequest( acl );
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        addLockTokens(method);
        
        try {
            method = (AclMethod)retryHandler(req.reqBodyAsString()).executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_OK:
                    break;
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_LOCKED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.LOCKED, null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.FORBIDDEN, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    protected void addLockTokens(HttpMethod method) throws WvcmException {
        StringBuffer b = new StringBuffer();
        Iterator activeLockTokens = ((ResourceImpl)resource()).getActiveLockTokens().iterator();
        if (activeLockTokens.hasNext()) {
            b.append("(");
            int i = 0;
            while (activeLockTokens.hasNext()) {
                LockTokenImpl lt = (LockTokenImpl)activeLockTokens.next();
                if (i > 0) {
                    b.append(",");
                }
                b.append("<").append(lt.getLockId()).append(">");
                i++;
            }
            b.append(")");
            method.addRequestHeader("If", b.toString());
        }
    }
    
    /**
     * NOT YET STANDARD
     * Locks this resource.
     *
     * @param    timeout             a  Timeout
     * @param    deep                a  boolean
     * @param    exclusive           a  boolean
     * @param    owner               a  String
     * @return   a LockToken
     * @throws   WvcmException
     */
    public LockToken doLock(LockToken.Timeout timeout, boolean deep, boolean exclusive, String owner) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource().location();
        String resourcepath = loc.path();
        LockMethod method = new LockMethod(loc.escapedPath());
        LockRequest req = new LockRequest(exclusive, owner);
        method.setRequestBody( req.reqBodyAsString() );
        method.addRequestHeader( "Content-Type", "text/xml; charset=\"utf-8\"" );
        
        if (timeout != null) {
            method.addRequestHeader("Timeout", timeout.toString());
        }
        
        if( deep )
            method.addRequestHeader( "Depth", "infinity" );
        else
            method.addRequestHeader( "Depth", "0" );
        
        try {
            method = (LockMethod)retryHandler(req.reqBodyAsString()).executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_OK:
                    LockResponse resp =
                        new LockResponse( resource(), method.getResponseBodyAsStream() );
                    return resp.createLockToken();
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CANNOT_LOCK, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    /**
     * NOT YET STANDARD
     * Releases the specified lock of this resource.
     *
     * @param    lockToken           a  LockToken
     * @throws   WvcmException
     */
    public void doUnlock(LockToken lockToken) throws WvcmException {
        LocationImpl loc = (LocationImpl)resource().location();
        String resourcepath = loc.path();
        UnlockMethod method = new UnlockMethod(loc.escapedPath());
        method.addRequestHeader( "Lock-Token", "<"+((LockTokenImpl)lockToken).getLockId()+">" );
        
        try {
            method = (UnlockMethod)retryHandler().executeMethod( method );
            int sc = method.getStatusCode();
            String st = method.getStatusText();
            switch( sc ) {
                case HttpStatus.SC_NO_CONTENT:
                    break;
                case HttpStatus.SC_NOT_FOUND:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.NOT_FOUND, null);
                case HttpStatus.SC_UNAUTHORIZED:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.UNAUTHORIZED, null);
                case HttpStatus.SC_FORBIDDEN:
                case HttpStatus.SC_CONFLICT:
                    ErrorResponse errorRsp = new ErrorResponse( method.getResponseBodyAsStream() );
                    throw new WvcmException(
                        st, resourcepath, errorRsp.getWvcmReasonCode(), null);
                default:
                    throw new WvcmException(
                        st, resourcepath, ReasonCode.CANNOT_UNLOCK, null);
            }
        }
        catch (java.io.IOException e) {
            throw new WvcmException(
                "Read failed", resourcepath, ReasonCode.READ_FAILED, new Exception[]{e});
        }
        finally {
            method.releaseConnection();
        }
    }
    
    protected class RetryHandler {
        private int retryMax = 5;
        private long retryTimeout = 5000;
        private long retrySleep = 500;
        private InputStream content;
        private String contentAsStr;
        
        private int tries = 0;
        private int rc = -1;
        private long start = System.currentTimeMillis();
        
        public RetryHandler(InputStream content) {
            this.content = content;
            init();
        }
        
        public RetryHandler(String content) {
            this.contentAsStr = content;
            init();
        }

        private void init() {
            try { this.retryMax = Integer.parseInt( (String)providerImpl.initParameter("retryMax") ); }
            catch (NumberFormatException e) {}
            try { this.retryTimeout = Integer.parseInt( (String)providerImpl.initParameter("retryTimeout") ); }
            catch (NumberFormatException e) {}
            try { this.retrySleep = Integer.parseInt( (String)providerImpl.initParameter("retrySleep") ); }
            catch (NumberFormatException e) {}
        }
        
        public boolean canRetry() {
            return (System.currentTimeMillis() - start) < retryTimeout &&
                tries < retryMax &&
                (rc == -1 || rc == HttpStatus.SC_CONFLICT);
        }
        
        public HttpMethod executeMethod(HttpMethod method) throws IOException, HttpException {
            while (canRetry()) {
                if (rc > 0) {
                    try {
                        Thread.currentThread().sleep(retrySleep);
                    } catch (InterruptedException e) {}
                    
                    // recycle method
                    Header[] headers = method.getRequestHeaders();
                    String resourcePath = method.getPath();
                    Class mclass = method.getClass();
                    Constructor mc = null;
                    HttpMethod newMethod = null;
                    try {
                        mc = mclass.getConstructor(new Class[]{String.class});
                        newMethod = (HttpMethod)mc.newInstance(new Object[]{resourcePath});
                        for (int i = 0; i < headers.length; i++) {
                            newMethod.setRequestHeader(headers[i]);
                        }
                        if (newMethod instanceof EntityEnclosingMethod) {
                            if (content != null) {
                                content.reset();
                                ((EntityEnclosingMethod)newMethod).setRequestBody(content);
                            }
                            else if (contentAsStr != null) {
                                ((EntityEnclosingMethod)newMethod).setRequestBody(contentAsStr);
                            }
                        }
                    }
                    catch (Exception e) {
                        e.printStackTrace();
                        //System.out.println("["+Thread.currentThread().getName()+"] RETRY FAILED tries="+tries+", max tries="+maxtries+", elapsed="+(System.currentTimeMillis() - start)+", timeout="+timeout);
                        return method;
                    }
                    method.releaseConnection();
                    method = newMethod;
                }
                rc = client().executeMethod(method);
                //System.out.println("["+Thread.currentThread().getName()+"] rc="+rc);
                tries++;
            }
            if (tries > 1) {
                if (rc != HttpStatus.SC_CONFLICT) {
                    //System.out.println("["+Thread.currentThread().getName()+"] RETRY SUCCESSFUL tries="+tries+", max tries="+retryMax+", elapsed="+(System.currentTimeMillis() - start)+", timeout="+retryTimeout);
                }
                else {
                    System.out.println("["+Thread.currentThread().getName()+"] RETRY FAILED tries="+tries+", max tries="+retryMax+", elapsed="+(System.currentTimeMillis() - start)+", timeout="+retryTimeout);
                }
            }
            else {
                if (rc != HttpStatus.SC_CONFLICT) {
                    //System.out.println("["+Thread.currentThread().getName()+"] FIRST TRY OK");
                }
                else {
                    System.out.println("["+Thread.currentThread().getName()+"] FIRST TRY FAILED tries="+tries+", max tries="+retryMax+", elapsed="+(System.currentTimeMillis() - start)+", timeout="+retryTimeout);
                }
            }
            return method;
        }
    }
}


