/* Copyright (c) Citrix Systems, Inc.
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS 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
 * COPYRIGHT HOLDER OR 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.
 */

package com.xensource.xenapi;

import java.net.URL;
import java.util.Map;
import java.util.TimeZone;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfig;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.apache.xmlrpc.client.XmlRpcHttpClientConfig;
import org.apache.xmlrpc.client.XmlRpcSun15HttpTransportFactory;

import com.xensource.xenapi.Types.BadServerResponse;
import com.xensource.xenapi.Types.SessionAuthenticationFailed;
import com.xensource.xenapi.Types.XenAPIException;

/**
 * Represents a connection to a XenServer. Creating a new instance of this class initialises a new XmlRpcClient that is
 * then used by all method calls: each method call in xenapi takes a Connection as a parameter, composes an XMLRPC
 * method call, and dispatches it on the Connection's client via the dispatch method.
 */
public class Connection
{
    /**
     * The version of the bindings that this class belongs to.
     */
    public static final String BINDINGS_VERSION = "6.1.0-1";

    /**
     * true if the connection is to the Rio edition of XenServer. Certain function calls are not allowed.
     *
     * @deprecated Use getAPIVersion() instead.
     */
    @Deprecated
    public Boolean rioConnection = false;

    private APIVersion apiVersion;

    protected int _wait = 600;

    /**
     * Updated when Session.login_with_password() is called.
     */
    public APIVersion getAPIVersion()
    {
        return apiVersion;
    }

    /**
     * The opaque reference to the session used by this connection
     */
    private String sessionReference;

    /**
     * As seen by the xmlrpc library. From our point of view it's a server.
     */
    private final XmlRpcClient client;

    private final boolean deprecatedConstructorUsed;

    /**
     * Creates a connection to a particular server using a given username and password. This object can then be passed
     * in to any other API calls.
     *
     * This constructor calls Session.loginWithPassword, passing itself as the first parameter.
     *
     * When this constructor is used, a call to dispose() (also called in the Connection's finalizer) will attempt a
     * Session.logout on this connection.
     *
     * @deprecated Use a constructor that takes a URL as the first parameter instead.
     */
    @Deprecated
    public Connection(String client, String username, String password) throws java.net.MalformedURLException,
            XmlRpcException, BadServerResponse, SessionAuthenticationFailed, XenAPIException
    {
        deprecatedConstructorUsed = true;

        // To login normally we call login_with_password(username, password, "1.X").  On rio this call fails and we
        // should use login_with_password(username,password) instead, and note that we are talking to a rio host so that we
        // can refuse to make certain miami-specific calls
        final String ApiVersion = APIVersion.latest().toString();
        this.client = getClientFromURL(new URL(client));
        try
        {
            //first try to login the modern way
            this.sessionReference = loginWithPassword(this.client, username, password, ApiVersion);
        } catch (BadServerResponse e)
        {
            //oops, something went wrong
            String[] errDesc = e.errorDescription;
            //was the problem that the host was running rio? If so it will have complained that it got three parameters
            //instead of two. Let us carefully verify the details of this complaint
            if (0 == errDesc[0].compareTo("MESSAGE_PARAMETER_COUNT_MISMATCH")
                && 0 == errDesc[1].compareTo("session.login_with_password")
                && 0 == errDesc[2].compareTo("2")
                && 0 == errDesc[3].compareTo("3"))
            {
                //and if so, we can have another go, using the older login method, and see how that goes.
                this.sessionReference = loginWithPassword(this.client, username, password);
                //success!. Note that we are talking to an old host on this connection
                this.rioConnection = true;
            } else
            {
                //Hmm... Can't solve this here. Let upstairs know about the problem.
                throw e;
            }
        }

        try
        {
            setAPIVersion(new Session(sessionReference));
        } catch (XenAPIException exn)
        {
            dispose();
            throw exn;
        } catch (XmlRpcException exn)
        {
            dispose();
            throw exn;
        }
    }

    /**
     * Creates a connection to a particular server using a given username and password. This object can then be passed
     * in to any other API calls.
     *
     * Note this constructor does NOT call Session.loginWithPassword; the programmer is responsible for calling it,
     * passing the Connection as a parameter. No attempt to connect to the server is made until login is called.
     *
     * When this constructor is used, a call to dispose() will do nothing. The programmer is responsible for manually
     * logging out the Session.
     */
    public Connection(URL url, int wait)
    {
        deprecatedConstructorUsed = false;
        _wait = wait;
        this.client = getClientFromURL(url);
    }

    /**
     * Creates a connection to a particular server using a given username and password. This object can then be passed
     * in to any other API calls.
     *
     * The additional sessionReference parameter must be a reference to a logged-in Session. Any method calls on this
     * Connection will use it. This constructor does not call Session.loginWithPassword, and dispose() on the resulting
     * Connection object does not call Session.logout. The programmer is responsible for ensuring the Session is logged
     * in and out correctly.
     */
    public Connection(URL url, String sessionReference)
    {
        deprecatedConstructorUsed = false;

        this.client = getClientFromURL(url);
        this.sessionReference = sessionReference;
    }

    protected void finalize() throws Throwable
    {
        dispose();
        super.finalize();
    }

    /**
     * Nothrow guarantee.
     */
    public void dispose()
    {
        if (!deprecatedConstructorUsed)
        {
            // We only need to do the Session.logout if they used the old deprecated constructor.
            return;
        }

        try
        {
            if (sessionReference != null)
            {
                String method_call = "session.logout";
                Object[] method_params = {Marshalling.toXMLRPC(this.sessionReference)};
                client.execute(method_call, method_params);
                sessionReference = null;
            }
        } catch (XmlRpcException exn)
        {
        }
    }

    /**
     * @deprecated The programmer is now responsible for calling login/logout themselves.
     */
    @Deprecated
    private static String loginWithPassword(XmlRpcClient client, String username, String password)
        throws BadServerResponse, XmlRpcException, SessionAuthenticationFailed
    {
        String method_call = "session.login_with_password";
        Object[] method_params = {Marshalling.toXMLRPC(username), Marshalling.toXMLRPC(password)};
        Map response = (Map)client.execute(method_call, method_params);
        if (response.get("Status").equals("Success"))
        {
            return (String)response.get("Value");
        } else if (response.get("Status").equals("Failure"))
        {
            Object[] error = (Object[])response.get("ErrorDescription");
            if (error[0].equals("SESSION_AUTHENTICATION_FAILED"))
            {
                throw new SessionAuthenticationFailed();
            }
        }
        throw new BadServerResponse(response);
    }

    /**
     * @deprecated The programmer is now responsible for calling login/logout themselves.
     */
    @Deprecated
    private static String loginWithPassword(XmlRpcClient client, String username, String password, String ApiVersion)
        throws BadServerResponse, XmlRpcException, SessionAuthenticationFailed
    {
        String method_call = "session.login_with_password";
        Object[] method_params = {Marshalling.toXMLRPC(username), Marshalling.toXMLRPC(password),
            Marshalling.toXMLRPC(ApiVersion)};
        Map response = (Map)client.execute(method_call, method_params);
        if (response.get("Status").equals("Success"))
        {
            return (String)response.get("Value");
        } else if (response.get("Status").equals("Failure"))
        {
            Object[] error = (Object[])response.get("ErrorDescription");
            if (error[0].equals("SESSION_AUTHENTICATION_FAILED"))
            {
                throw new SessionAuthenticationFailed();
            }
        }
        throw new BadServerResponse(response);
    }

    private XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();

    public XmlRpcClientConfigImpl getConfig()
    {
        return config;
    }

    private XmlRpcClient getClientFromURL(URL url)
    {
        config.setTimeZone(TimeZone.getTimeZone("UTC"));
        config.setServerURL(url);
        config.setReplyTimeout(_wait * 1000);
        config.setConnectionTimeout(5000);
        XmlRpcClient client = new XmlRpcClient();
        client.setConfig(config);
        return client;
    }

    /*
     * Because the binding calls are constructing their own parameter lists, they need to be able to get to
     * the session reference directly. This is all rather ugly and needs redone
     * Changed to public to allow easier integration with HTTP-level streaming interface,
     * see CA-15447
     */
    public String getSessionReference()
    {
        return this.sessionReference;
    }

    /**
     * The (auto-generated parts of) the bindings dispatch XMLRPC calls on this Connection's client through this method.
     */
    protected Map dispatch(String method_call, Object[] method_params) throws XmlRpcException, XenAPIException
    {
        Map response = (Map)client.execute(method_call, method_params);

        if (!deprecatedConstructorUsed)
        {
            // We are using the new-style constructor which doesn't perform login.
            // Set this Connection's Session reference from the value returned on the wire.
            if (method_call.equals("session.login_with_password") &&
                response.get("Status").equals("Success"))
            {
                // Store the Session reference and ask the server what the
                // API version it's using is.
                Session session = Types.toSession(response.get("Value"));
                sessionReference = session.ref;
                setAPIVersion(session);
            }
            else if (method_call.equals("session.slave_local_login_with_password") &&
                response.get("Status").equals("Success"))
            {
                // Store the Session reference and assume the latest API version.
                sessionReference = Types.toSession(response.get("Value")).ref;
                apiVersion = APIVersion.latest();
            }
            else if (method_call.equals("session.logout"))
            {
                // Work around a bug in XenServer 5.0 and below.
                // session.login_with_password should have rejected us with
                // HOST_IS_SLAVE, but instead we don't find out until later.
                // We don't want to leak the session, so we need to log out
                // this session from the master instead.
                if (response.get("Status").equals("Failure"))
                {
                    Object[] error = (Object[])response.get("ErrorDescription");
                    if (error.length == 2 && error[0].equals("HOST_IS_SLAVE"))
                    {
                        try
                        {
                            URL client_url =
                                ((XmlRpcHttpClientConfig)client.getClientConfig()).getServerURL();
                            Connection tmp_conn =
                                new Connection(new URL(client_url.getProtocol(),
                                    (String)error[1],
                                    client_url.getPort(),
                                    client_url.getFile()), _wait);
                            tmp_conn.sessionReference = sessionReference;
                            try
                            {
                                Session.logout(tmp_conn);
                            } finally
                            {
                                tmp_conn.dispose();
                            }
                        } catch (Exception exn2)
                        {
                            // Ignore -- we're going to throw HostIsSlave anyway.
                        }
                    }
                }

                // Clear the stored Session reference.
                this.sessionReference = null;
            }
        }

        return Types.checkResponse(response);
    }

    private void setAPIVersion(Session session) throws XenAPIException, XmlRpcException
    {
        try
        {
            long major = session.getThisHost(this).getAPIVersionMajor(this);
            long minor = session.getThisHost(this).getAPIVersionMinor(this);
            apiVersion = APIVersion.fromMajorMinor(major, minor);
        } catch (BadServerResponse exn)
        {
            apiVersion = APIVersion.API_1_1;
        }
    }
}
