/*
 * Copyright 2012 AMG.lab, a Bull Group Company
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xlcloud.openstack.client;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.xml.bind.MarshalException;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import org.xlcloud.openstack.api.IdentityManagementClient;
import org.xlcloud.openstack.model.exceptions.OpenStackAuthenticationException;
import com.sun.jersey.api.client.ClientHandlerException;

/**
 * This class is a handler set on {@link OpenStackKeystoneClient} when obtained by
 * {@link OpenStackKeystoneClient#getInstance(String, String, String, String)} method. Every time one of
 * methods of {@link IdentityManagementClient} is invoked, this invocation is
 * handled by {@link #invoke(Object, Method, Object[])} method.
 * 
 * @author Andrzej Stasiak, AMG.net
 */
public class RefreshAuthenticationHandler implements InvocationHandler {

    static private final Logger LOG = Logger.getLogger(RefreshAuthenticationHandler.class);

    public OpenStackKeystoneClient client;

    private String password;

    private String username;

    private String tenantName;

    /**
     * @param client
     * @param username
     * @param password
     * @param tenantName
     */
    RefreshAuthenticationHandler( OpenStackKeystoneClient client, String username, String password, String tenantName ) {
        this.client = client;
        this.username = username;
        this.password = password;
        this.tenantName = tenantName;
    }

    /**
     * This method is invoked every time when any
     * {@link IdentityManagementClient} method is invoked. It checks if
     * {@link OpenStackKeystoneClient} has any token and if not - tries to authenticate
     * and then invoke actual method. Otherwise, it invokes actual method and if
     * OpenStack responds with 401, it tries to authenticate afterwards (to
     * handle the situation when the token has expired).
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (StringUtils.isBlank(client.getToken())) {
                LOG.info("Token is empty. Authenticating by Keystone.");
                client.reauthenticate(username, password, tenantName);
                return this.doInvoke(client, method, args);
            } else {
                try {
                    return this.doInvoke(client, method, args);
                } catch (OpenStackAuthenticationException e) {
                    LOG.info("Got 401 response. Authenticating by Keystone.");
                    client.reauthenticate(username, password, tenantName);
                    return this.doInvoke(client, method, args);
                }
            }
        } catch (ClientHandlerException e) {
            throw unwrapNoRouteToHostException(e);
        }
    }

    /**
     * This method checks if the given exception contains MarshalException and if so,
     * returns exception linked to it. Otherwise, return original exception.
     * @param e
     *            caught exception
     * @return input exception or unwrapped NoRouteToHostException
     */
    private Throwable unwrapNoRouteToHostException(ClientHandlerException e) {
        /*
         * FIXME ugly NoRouteToHostException recognition. Try to find a better
         * way...
         */
        if (e.getCause() != null && e.getCause().getCause() instanceof MarshalException) {
            Throwable linkedException = ((MarshalException) e.getCause().getCause()).getLinkedException();
            LOG.warn("Got marshal exception linked with " + linkedException.getClass() + " that will be rethrown.");
            return linkedException;
        } else {
            return e;
        }
    }

    /**
     * Invokes method (by reflection) and unwraps potential
     * {@link InvocationTargetException}
     * 
     * @param target
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    private Object doInvoke(Object target, Method method, Object[] args) throws Throwable {
        try {
            return method.invoke(target, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }
}
