/////////////////////////////////////////////////////////////////////////
//
// © University of Southampton IT Innovation Centre, 2004
//
// Copyright in this library belongs to the IT Innovation Centre of
// 2 Venture Road, Chilworth Science Park, Southampton, SO16 7NP, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
//      Created by:             Darren Marvin
//      Created date:           2004/04/30
//      Created for project:    GEMSS
//
/////////////////////////////////////////////////////////////////////////
//
//      Dependencies: None
//
/////////////////////////////////////////////////////////////////////////
//
//      Last commit info:       $Author: $
//                              $Date: $
//                              $Revision: $
//
/////////////////////////////////////////////////////////////////////////

package uk.ac.soton.itinnovation.gemss.security.context.token;

import java.util.Set;
import java.util.Iterator;
import java.util.Map;
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.logging.Logger;

import eu.gemss.components.security.InvalidCredentialException;
import eu.gemss.components.security.InvalidPasswordException;
import eu.gemss.components.security.token.trust.*;
import eu.gemss.components.security.token.provision.*;
import eu.gemss.components.security.token.*;
import eu.gemss.components.security.token.types.UnsupportedTokenException;
import eu.gemss.components.*;
import eu.gemss.signals.SignalHandler;

import uk.ac.soton.itinnovation.gemss.security.context.token.*;
import uk.ac.soton.itinnovation.gemss.security.context.token.provision.*;
import uk.ac.soton.itinnovation.gemss.signal.SignalProcessor;
import uk.ac.soton.itinnovation.gemss.security.context.configuration.*;
import uk.ac.soton.itinnovation.gemss.security.context.token.trust.*;
import uk.ac.soton.itinnovation.gemss.security.context.token.store.*;
import uk.ac.soton.itinnovation.gemss.security.context.*;
import eu.gemss.components.security.token.store.*;
import eu.gemss.components.security.*;

public class SecurityTokenManager {

    private static Logger mLogger = Logger.getLogger("uk.ac.soton.itinnovation.gemss.security.context.token.SecurityTokenManager");
    private Map mTokenProviderMappings = null;
    private Map mTrustProviderMappings = null;
    private SignalHandler mSignalHandler = null;
    private SecurityContextConfiguration mConfiguration = null;
    private TokenProviderRegistry mTokenProviderRegistry = null;
    private TrustProviderRegistry mTrustProviderRegistry = null;
    private StoreProviderRegistry mStoreProviderRegistry = null;

    /**
     * Default constructor requires a processor for posting signals to
     * @param processor processor to send to
     */
    public SecurityTokenManager(SecurityContextConfiguration config,SignalHandler handler) throws SecurityTokenException {
        mConfiguration = config;
        mSignalHandler = handler;
        if(mTokenProviderRegistry==null || mTrustProviderRegistry==null || mStoreProviderRegistry==null)
            init();
    }

    /**
     * Generate a security token of the required type
     * @param tokenDesc security token descriptor
     * @return security token
     */
    public SecurityToken generateSecurityToken(TokenDescriptor tokenDesc) throws SecurityTokenException, UnsupportedTokenException, InvalidCredentialException  {
        SecurityTokenProvider tokenProvider = null;
        SecurityToken token = null;
        try{

            //first of all check to see if a cached token descriptor matches the
            //required one
            Set set = mTokenProviderMappings.keySet();
            Iterator iterator = set.iterator();
            while(iterator.hasNext()) {
                TokenDescriptor tD = (TokenDescriptor) iterator.next();
                if(tD.equals(tokenDesc)) {
                    //have a match and so use the associated provider
                    tokenProvider = (SecurityTokenProvider) mTokenProviderMappings.get(tD);
                }
            }
            if(tokenProvider==null) {
                //ask the ProviderRegistry for any providers that supply the required
                //token type
                tokenProvider = mTokenProviderRegistry.getProvider(tokenDesc);
                tokenProvider.setSignalHandler(mSignalHandler);
                tokenProvider.setTokenProviderConfiguration(mConfiguration.getSecurityProviderConfiguration());
                //add the token provider to the mappings, registry won't hold reference
                //to providers that don't match, neither does this thing so unused providers
                //should by unloaded by garbage collector.
                mTokenProviderMappings.put(tokenDesc,tokenProvider);
            }
            //get the security token that we desire
            token = tokenProvider.generateSecurityToken(tokenDesc);
        }
        catch(TokenProviderException ex) {
            mLogger.log(Level.WARNING,"Unable to retrieve token",ex);
            throw new SecurityTokenException(ex.getMessage());
        }
        catch(UnsupportedTokenException ex) {
            throw (UnsupportedTokenException) ex;
        }
        catch(Exception ex) {
            mLogger.log(Level.WARNING,"Unexpected exception, please send a bug report including all log files.",ex);
            throw new SecurityTokenException(ex.getMessage());
        }
        return token;

    }


    /**
     * Add a security token instance to the prevailing security context
     * @param secToken security token to add
     * @throws SecurityContextException
     */
    public void addSecurityTokenToContext(SecurityToken secToken) throws SecurityTokenException, eu.gemss.components.security.token.SecurityTokenException {
        SecurityTokenStoreProvider[] providers = null;
        try{
            providers = mStoreProviderRegistry.getProvider(secToken.getTokenDescriptor());
            //current policy is to add token to the first provider that accepts it

            boolean success = false;
            for(int i=0;i<providers.length;i++) {
                try{
                    providers[i].addSecurityToken(secToken);
                    success = true;
                    break;
                }
                catch(StoreProviderException ex) {
                    //try the next one
                    mLogger.log(Level.WARNING,"Failed to add to store provider described as '" + secToken.getTokenDescriptor().getDescription() + "'",ex);

                }
            }
            if(!success) {
                mLogger.log(Level.WARNING,"Could not find a single store provider for token described as '" + secToken.getTokenDescriptor().getDescription() + "'");
                throw new StoreProviderException("Could not establish a security context properly, logs will hold more information","Could not find a single store provider for token described as '" + secToken.getTokenDescriptor().getDescription() + "'");
            }

        }
        catch(StoreProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
    }

    /**
     * Remove from the prevailing security context the security token identified by the supplied token descriptor
     * @param secToken token to remove
     * @throws SecurityContextException
     */
    public void removeSecurityTokenFromContext(SecurityToken secToken) throws SecurityContextException, eu.gemss.components.security.token.SecurityTokenException {
        SecurityTokenStoreProvider[] providers = null;
        try{
            providers = mStoreProviderRegistry.getProvider(secToken.getTokenDescriptor());
            //current policy is to remove token from all providers that say they have the token
            boolean success = false;
            for(int i=0;i<providers.length;i++) {
                try{
                    if(providers[i].contains(secToken.getTokenDescriptor())) {
                        providers[i].removeSecurityToken(secToken);
                        success = true;
                    }
                }
                catch(StoreProviderException ex) {
                    //try the next one

                }
            }
            if(!success) {
                mLogger.log(Level.WARNING,"Could not find a single store provider for token described as '" + secToken.getTokenDescriptor().getDescription() + "'");
                throw new StoreProviderException("Could not establish a security context properly, logs will hold more information","Could not find a single store provider for token described as '" + secToken.getTokenDescriptor().getDescription() + "'");
            }
        }
        catch(StoreProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
    }


    /**
     * Retrieve from the prevailing security context a previously stored security token matching the passed token descriptor
     * @param tokenDesc token identifier
     * @return security token required
     * @throws SecurityContextException
     */
    public SecurityToken getSecurityTokenFromContext(TokenDescriptor tokenDesc) throws SecurityContextException, eu.gemss.components.security.token.SecurityTokenException, eu.gemss.components.security.InvalidCredentialException {
        SecurityTokenStoreProvider[] providers = null;
        SecurityToken secToken = null;
        try{
            providers = mStoreProviderRegistry.getProvider(tokenDesc);
            //current policy is to get token from the first provider that returns it successfully

            boolean success = false;
            for(int i=0;i<providers.length;i++) {
                try{
                    if(providers[i].contains(tokenDesc)) {
                        secToken = providers[i].getSecurityToken(tokenDesc);
                    }
                    success = true;
                    break;
                }
                catch(StoreProviderException ex) {
                    //try the next one

                }
            }
            if(!success) {
                mLogger.log(Level.WARNING,"Could not find a single store provider for token described as '" + tokenDesc.getDescription() + "'");
                throw new StoreProviderException("Could not establish a security context properly, logs will hold more information","Could not find a single store provider for token described as '" + tokenDesc.getDescription() + "'");
            }
            return secToken;
        }
        catch(StoreProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
    }
    /**
     * Verify that the supplied security token is trusted
     * @param token security token to check
     * @return true if trusted, false otherwise
     * @throws SecurityTokenException
     * @throws eu.gemss.components.security.token.SecurityTokenException
     */
    public boolean isTrusted(SecurityToken token) throws SecurityTokenException, eu.gemss.components.security.token.SecurityTokenException {
        SecurityTrustProvider trustProvider = null;
        TokenDescriptor tokenType = null;
        try{
            mLogger.log(Level.INFO,"**** Checking trust using SecurityTokenManager ****");
            //first of all check to see if a cached trust provider supports the
            //required token type
            tokenType = token.getTokenDescriptor();
            Set set = mTrustProviderMappings.keySet();
            Iterator iterator = set.iterator();
            while(iterator.hasNext()) {
                TokenDescriptor tD = (TokenDescriptor) iterator.next();
                if(tD.equals(tokenType)) {
                    //have a match and so use the associated provider
                    trustProvider = (SecurityTrustProvider) mTrustProviderMappings.get(tD);
                }
            }
            if(trustProvider==null) {
                //ask the ProviderRegistry for any providers that supply the required
                //token type
                trustProvider = mTrustProviderRegistry.getProvider(tokenType);
                trustProvider.setSignalHandler(mSignalHandler);
                trustProvider.setTrustProviderConfiguration(mConfiguration.getSecurityProviderConfiguration());
                //add the token provider to the mappings, registry won't hold reference
                //to providers that don't match, neither does this thing so unused providers
                //should by unloaded by garbage collector.
                mTokenProviderMappings.put(tokenType,trustProvider);
            }
            //get the security token that we desire
            mLogger.log(Level.INFO,"**** Checking trust using Provider ****");
            return trustProvider.isTrusted(token);
        }
        catch(TrustProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
    }

    private void init() throws SecurityTokenException {
        try{
            //create the token provider mappings from the ProviderRegistry and
            //won't load providers until a request for a particular token type arrives.
            mTokenProviderMappings = new Hashtable();
            mTrustProviderMappings = new Hashtable();
            mTokenProviderRegistry = TokenProviderRegistry.getInstance(mConfiguration);
            mTrustProviderRegistry = TrustProviderRegistry.getInstance(mConfiguration);
            mStoreProviderRegistry = StoreProviderRegistry.getInstance(mConfiguration);
        }
        catch(TokenProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
        catch(TrustProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
        catch(StoreProviderException ex) {
            throw new SecurityTokenException(ex.getMessage());
        }
        catch(Exception ex) {
            mLogger.log(Level.SEVERE,"Unexpected error occured, please send a bug report including all log files",ex);
            throw new SecurityTokenException("Unexpected error occured, please send a bug report including all log files");
        }
    }

}