/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/wvcm/src/org/apache/wvcm/store/webdav/WebdavAccessor.java,v 1.15 2005/03/04 18:14:31 pnever Exp $
 * $Revision: 1.15 $
 * $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 java.util.*;
import org.apache.commons.httpclient.*;

import java.io.IOException;
import javax.wvcm.PropertyNameList;
import javax.wvcm.PropertyNameList.PropertyName;
import javax.wvcm.Provider;
import javax.wvcm.ProviderFactory.Callback;
import javax.wvcm.ProviderFactory.Callback.Authentication;
import javax.wvcm.WvcmException;
import javax.wvcm.WvcmException.ReasonCode;
import org.apache.webdav.methods.OptionsMethod;
import org.apache.wvcm.ProviderImpl;
import org.apache.wvcm.store.Accessor;
import org.apache.wvcm.store.webdav.request.OptionsRequest;
import org.apache.wvcm.store.webdav.response.OptionsResponse;


/**
 * WebDAV-based implementation of Accessor.
 *
 * @author <a href="mailto:peter.nevermann@softwareag.com">Peter Nevermann</a>
 * @version $Revision: 1.15 $
 */
public class WebdavAccessor implements Accessor {
    
    // provider -> HttpClientWrapper
    private static Map httpclients = Collections.synchronizedMap( new HashMap() );
    
    protected ProviderImpl providerImpl;
    
    private String hostname = null;
    private int port = 0;
    private String defaultContext = null;
    private Hashtable headers = null;
    
    // caches
    private String defaultContextPath = null;
    
    static {
        // do preemptive authentication
        System.setProperty( "httpclient.authentication.preemptive", "true" );
    }
    
    /**
     * Dummy constructor
     */
    public WebdavAccessor() {
    }
    
    /**
     * Constructor.
     */
    public WebdavAccessor( Provider provider ) {
        this.providerImpl = (ProviderImpl)provider;
        this.hostname = (String)providerImpl.initParameter( "host" );
        if( hostname == null || hostname.equals("") )
            throw new IllegalStateException( "Cannot determine host" );
        try {
            this.port = Integer.parseInt( (String)providerImpl.initParameter("port") );
        }
        catch (NumberFormatException e) {
            throw new IllegalStateException( "Cannot determine port" );
        }
        initialize( hostname, port );
    }
    
    /**
     * Return a list of {@link Folder} objects that identify folders
     * on the server that contain workspaces for the Provider of this accessor.
     * If the Provider does not allow the client to create new
     * workspaces on the server, an empty List is returned.
     * @param wantedPropertyList The properties available in the returned proxies.
     */
    public List serverWorkspaceFolderList(PropertyNameList wantedPropertyList) throws WvcmException {
        PropertyName[] wsfollist = {PropertyName.WORKSPACE_FOLDER_LIST};
        OptionsRequest req = new OptionsRequest( new PropertyNameList(wsfollist) );
        
        // TODO: prio=h, effort=0.5, descr=(switch to live property workspace-collection-set when implemeneted in server)
        
        String resourcepath = defaultContextPath();
        OptionsMethod method = new OptionsMethod( resourcepath );
        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_OK:
                    OptionsResponse resp =
                        new OptionsResponse( providerImpl, method.getResponseBodyAsStream() );
                    return resp.getFolderList( PropertyName.WORKSPACE_FOLDER_LIST );
                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});
        }
        
    }
    
    // -------------------------------------------------------------------------
    // Non-interface methods
    // -------------------------------------------------------------------------
    
    protected void initialize( String hostname, int port ) throws IllegalStateException {
        this.hostname = hostname;
        this.port = port;
        this.defaultContext = (String)providerImpl.initParameter( "context" );
        this.headers = (Hashtable)providerImpl.initParameter( "headers" );
    }
    
    /**
     * Get the HttpClient of this accessor.
     *
     * @return   a HttpClient
     */
    protected HttpClientWrapper client() {
        return getHttpClient( providerImpl, hostname, port );
    }
    
    /**
     * HttpClient factory.
     */
    protected HttpClientWrapper getHttpClient( Provider provider, String hostname, int port ) {
        Host host = new Host( hostname, port );
        HttpClientWrapper result = (HttpClientWrapper)httpclients.get( host );
        HttpClient httpclient = null;
        
        if( result == null ) {
            httpclient = new HttpClient();
            httpclient.getHostConfiguration().setHost( hostname, port, "http" );
            result = new HttpClientWrapper(provider, httpclient);
            httpclients.put( host, result );
        }
        else {
            httpclient = result.getClient();
        }
        
        getAuthentication( hostname, result );
        return result;
    }
    
    private void getAuthentication(String hostname, HttpClientWrapper httpclientWrapper) {
        String username = null;
        String password = null;
        Callback cb = providerImpl.callback();
        String realm = (String)providerImpl.initParameter( "realm" );
        if( cb != null ) {
            // TODO: prio=l, effort=1.0, descr=(handle retryCount of cb.getAuthentication)
            Authentication authentication = cb.getAuthentication(realm, new Integer(0));
            username = authentication.loginName();
            password = authentication.password();
            
            if( authenticationChanged(hostname, realm, username, password, httpclientWrapper.getClient()) ) {
                HttpState state = new HttpState();
                state.setCredentials(
                    realm, hostname, new UsernamePasswordCredentials(username, password) );
                httpclientWrapper.getClient().setState( state );
            }
        }
    }
    
    private boolean authenticationChanged( String hostname, String realm, String username, String password, HttpClient httpclient ) {
        boolean sameUsername = false;
        boolean samePassword = false;
        HttpState currentState = httpclient.getState();
        UsernamePasswordCredentials currentCredentials = (currentState != null ? (UsernamePasswordCredentials)currentState.getCredentials(realm, hostname) : null);
        String currentUsername = (currentCredentials != null ? currentCredentials.getUserName() : null);
        String currentPassword = (currentCredentials != null ? currentCredentials.getPassword() : null);
        
        if( username == null )
            sameUsername = (currentUsername == null);
        else
            sameUsername = username.equals( currentUsername );
        
        if( password == null )
            samePassword = (currentPassword == null);
        else
            samePassword = password.equals( currentPassword );
        
        return !(sameUsername && samePassword);
    }
    
    /**
     * Get context path
     *
     * @return   a String
     */
    protected synchronized String defaultContextPath() {
        if( defaultContextPath == null ) {
            if( defaultContext != null && defaultContext.length() > 0 )
                defaultContextPath = "/"+defaultContext;
            else
                defaultContextPath = "/";
        }
        return defaultContextPath;
    }
    
    /**
     * Host class
     */
    private static class Host {
        String host;
        int port;
        String thread;
        
        Host( String host, int port ) {
            this.host = host;
            this.port = port;
            this.thread = Thread.currentThread().getName();
        }
        
        public int hashCode() {
            return port;
        }
        
        public boolean equals( Object o ) {
            if( !(o instanceof Host) )
                return false;
            
            Host a = (Host)o;
            if( host != null && !host.equals(a.host) || host == null && a.host != null )
                return false;
            if( port != a.port )
                return false;
            if( thread != null && !thread.equals(a.thread) || thread == null && a.thread != null )
                return false;
            return true;
        }
    }
    
    /**
     * Wrapper for HttpClient
     */
    protected static class HttpClientWrapper {
        
        private HttpClient client;
        private Provider provider;
        
        /**
         * Constructor
         *
         * @param    client              a  HttpClient
         */
        public HttpClientWrapper(Provider provider, HttpClient client) {
            this.client = client;
            this.provider = provider;
        }
        
        public int executeMethod(HttpMethod method) throws IOException, HttpException {
            ProviderImpl p = (ProviderImpl)provider;
            method.addRequestHeader("User-Agent", "wvcm");
            Hashtable headers = (Hashtable)p.initParameter("headers");
            if( headers != null ) {
                Enumeration i = headers.keys();
                while( i.hasMoreElements() ) {
                    String headerName = (String)i.nextElement();
                    String headerValue = (String)headers.get(headerName);
                    method.addRequestHeader(headerName, headerValue);
                }
            }
            return client.executeMethod(method);
        }
        
        public HttpClient getClient() {
            return client;
        }
    }
}

