/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.jgss;

import org.ietf.jgss.*;
import sun.security.jgss.spi.*;
import java.util.*;
import com.sun.security.jgss.*;
import sun.security.jgss.spnego.SpNegoCredElement;

public class GSSCredentialImpl implements ExtendedGSSCredential {

    private GSSManagerImpl gssManager = null;
    private boolean destroyed = false;

    /*
     * We store all elements in a hashtable, using <oid, usage> as the
     * key. This makes it easy to locate the specific kind of credential we
     * need. The implementation needs to be optimized for the case where
     * there is just one element (tempCred).
     */
    private Hashtable<SearchKey, GSSCredentialSpi> hashtable = null;

    // XXX Optimization for single mech usage
    private GSSCredentialSpi tempCred = null;

    GSSCredentialImpl(GSSManagerImpl gssManager, int usage)
        throws GSSException {
        this(gssManager, null, GSSCredential.DEFAULT_LIFETIME,
             (Oid[]) null, usage);
    }

    GSSCredentialImpl(GSSManagerImpl gssManager, GSSName name,
                             int lifetime, Oid mech, int usage)
        throws GSSException {
        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        init(gssManager);
        add(name, lifetime, lifetime, mech, usage);
    }

    GSSCredentialImpl(GSSManagerImpl gssManager, GSSName name,
                      int lifetime, Oid mechs[], int usage)
        throws GSSException {
        init(gssManager);
        boolean defaultList = false;
        if (mechs == null) {
            mechs = gssManager.getMechs();
            defaultList = true;
        }

        for (int i = 0; i < mechs.length; i++) {
            try {
                add(name, lifetime, lifetime, mechs[i], usage);
            } catch (GSSException e) {
                if (defaultList) {
                    // Try the next mechanism
                    GSSUtil.debug("Ignore " + e + " while acquring cred for "
                        + mechs[i]);
                    //e.printStackTrace();
                } else throw e; // else try the next mechanism
            }
        }
        if ((hashtable.size() == 0) || (usage != getUsage()))
            throw new GSSException(GSSException.NO_CRED);
    }

    // Wrap a mech cred into a GSS cred
    public GSSCredentialImpl(GSSManagerImpl gssManager,
                      GSSCredentialSpi mechElement) throws GSSException {

        init(gssManager);
        int usage = GSSCredential.ACCEPT_ONLY;
        if (mechElement.isInitiatorCredential()) {
            if (mechElement.isAcceptorCredential()) {
                usage = GSSCredential.INITIATE_AND_ACCEPT;
            } else {
                usage = GSSCredential.INITIATE_ONLY;
            }
        }
        SearchKey key = new SearchKey(mechElement.getMechanism(),
                                        usage);
        tempCred = mechElement;
        hashtable.put(key, tempCred);
        // More mechs that can use this cred, say, SPNEGO
        if (!GSSUtil.isSpNegoMech(mechElement.getMechanism())) {
            key = new SearchKey(GSSUtil.GSS_SPNEGO_MECH_OID, usage);
            hashtable.put(key, new SpNegoCredElement(mechElement));
        }
    }

    void init(GSSManagerImpl gssManager) {
        this.gssManager = gssManager;
        hashtable = new Hashtable<SearchKey, GSSCredentialSpi>(
                                                gssManager.getMechs().length);
    }

    public void dispose() throws GSSException {
        if (!destroyed) {
            GSSCredentialSpi element;
            Enumeration<GSSCredentialSpi> values = hashtable.elements();
            while (values.hasMoreElements()) {
                element = values.nextElement();
                element.dispose();
            }
            destroyed = true;
        }
    }

    public GSSCredential impersonate(GSSName name) throws GSSException {
        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }
        Oid mech = tempCred.getMechanism();
        GSSNameSpi nameElement = (name == null ? null :
                                  ((GSSNameImpl)name).getElement(mech));
        GSSCredentialSpi cred = tempCred.impersonate(nameElement);
        return (cred == null ?
            null : new GSSCredentialImpl(gssManager, cred));
    }

    public GSSName getName() throws GSSException {
        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }
        return GSSNameImpl.wrapElement(gssManager, tempCred.getName());
    }

    public GSSName getName(Oid mech) throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        SearchKey key = null;
        GSSCredentialSpi element = null;

        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        key = new SearchKey(mech, GSSCredential.INITIATE_ONLY);
        element = hashtable.get(key);

        if (element == null) {
            key = new SearchKey(mech, GSSCredential.ACCEPT_ONLY);
            element = hashtable.get(key);
        }

        if (element == null) {
            key = new SearchKey(mech, GSSCredential.INITIATE_AND_ACCEPT);
            element = hashtable.get(key);
        }

        if (element == null) {
            throw new GSSExceptionImpl(GSSException.BAD_MECH, mech);
        }

        return GSSNameImpl.wrapElement(gssManager, element.getName());

    }

    /**
     * Returns the remaining lifetime of this credential. The remaining
     * lifetime is defined as the minimum lifetime, either for initiate or
     * for accept, across all elements contained in it. Not terribly
     * useful, but required by GSS-API.
     */
    public int getRemainingLifetime() throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        SearchKey tempKey;
        GSSCredentialSpi tempCred;
        int tempLife = 0, tempInitLife = 0, tempAcceptLife = 0;
        int min = INDEFINITE_LIFETIME;

        for (Enumeration<SearchKey> e = hashtable.keys();
                                        e.hasMoreElements(); ) {
            tempKey = e.nextElement();
            tempCred = hashtable.get(tempKey);
            if (tempKey.getUsage() == INITIATE_ONLY)
                tempLife = tempCred.getInitLifetime();
            else if (tempKey.getUsage() == ACCEPT_ONLY)
                tempLife = tempCred.getAcceptLifetime();
            else {
                tempInitLife = tempCred.getInitLifetime();
                tempAcceptLife = tempCred.getAcceptLifetime();
                tempLife = (tempInitLife < tempAcceptLife ?
                            tempInitLife:
                            tempAcceptLife);
            }
            if (min > tempLife)
                min = tempLife;
        }

        return min;
    }

    public int getRemainingInitLifetime(Oid mech) throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        GSSCredentialSpi element = null;
        SearchKey key = null;
        boolean found = false;
        int max = 0;

        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        key = new SearchKey(mech, GSSCredential.INITIATE_ONLY);
        element = hashtable.get(key);

        if (element != null) {
            found = true;
            if (max < element.getInitLifetime())
                max = element.getInitLifetime();
        }

        key = new SearchKey(mech, GSSCredential.INITIATE_AND_ACCEPT);
        element = hashtable.get(key);

        if (element != null) {
            found = true;
            if (max < element.getInitLifetime())
                max = element.getInitLifetime();
        }

        if (!found) {
            throw new GSSExceptionImpl(GSSException.BAD_MECH, mech);
        }

        return max;

    }

    public int getRemainingAcceptLifetime(Oid mech) throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        GSSCredentialSpi element = null;
        SearchKey key = null;
        boolean found = false;
        int max = 0;

        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        key = new SearchKey(mech, GSSCredential.ACCEPT_ONLY);
        element = hashtable.get(key);

        if (element != null) {
            found = true;
            if (max < element.getAcceptLifetime())
                max = element.getAcceptLifetime();
        }

        key = new SearchKey(mech, GSSCredential.INITIATE_AND_ACCEPT);
        element = hashtable.get(key);

        if (element != null) {
            found = true;
            if (max < element.getAcceptLifetime())
                max = element.getAcceptLifetime();
        }

        if (!found) {
            throw new GSSExceptionImpl(GSSException.BAD_MECH, mech);
        }

        return max;

    }

    /**
     * Returns the usage mode for this credential. Returns
     * INITIATE_AND_ACCEPT if any one element contained in it supports
     * INITIATE_AND_ACCEPT or if two different elements exist where one
     * support INITIATE_ONLY and the other supports ACCEPT_ONLY.
     */
    public int getUsage() throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        SearchKey tempKey;
        boolean initiate = false;
        boolean accept = false;

        for (Enumeration<SearchKey> e = hashtable.keys();
                                        e.hasMoreElements(); ) {
            tempKey = e.nextElement();
            if (tempKey.getUsage() == INITIATE_ONLY)
                initiate = true;
            else if (tempKey.getUsage() == ACCEPT_ONLY)
                accept = true;
            else
                return INITIATE_AND_ACCEPT;
        }
        if (initiate) {
            if (accept)
                return INITIATE_AND_ACCEPT;
            else
                return INITIATE_ONLY;
        } else
            return ACCEPT_ONLY;
    }

    public int getUsage(Oid mech) throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        GSSCredentialSpi element = null;
        SearchKey key = null;
        boolean initiate = false;
        boolean accept = false;

        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        key = new SearchKey(mech, GSSCredential.INITIATE_ONLY);
        element = hashtable.get(key);

        if (element != null) {
            initiate = true;
        }

        key = new SearchKey(mech, GSSCredential.ACCEPT_ONLY);
        element = hashtable.get(key);

        if (element != null) {
            accept = true;
        }

        key = new SearchKey(mech, GSSCredential.INITIATE_AND_ACCEPT);
        element = hashtable.get(key);

        if (element != null) {
            initiate = true;
            accept = true;
        }

        if (initiate && accept)
            return GSSCredential.INITIATE_AND_ACCEPT;
        else if (initiate)
            return GSSCredential.INITIATE_ONLY;
        else if (accept)
            return GSSCredential.ACCEPT_ONLY;
        else {
            throw new GSSExceptionImpl(GSSException.BAD_MECH, mech);
        }
    }

    public Oid[] getMechs() throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }
        Vector<Oid> result = new Vector<Oid>(hashtable.size());

        for (Enumeration<SearchKey> e = hashtable.keys();
                                                e.hasMoreElements(); ) {
            SearchKey tempKey = e.nextElement();
            result.addElement(tempKey.getMech());
        }
        return result.toArray(new Oid[0]);
    }

    public void add(GSSName name, int initLifetime, int acceptLifetime,
                    Oid mech, int usage) throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }
        if (mech == null) mech = ProviderList.DEFAULT_MECH_OID;

        SearchKey key = new SearchKey(mech, usage);
        if (hashtable.containsKey(key)) {
            throw new GSSExceptionImpl(GSSException.DUPLICATE_ELEMENT,
                                       "Duplicate element found: " +
                                       getElementStr(mech, usage));
        }

        // XXX If not instance of GSSNameImpl then throw exception
        // Application mixing GSS implementations
        GSSNameSpi nameElement = (name == null ? null :
                                  ((GSSNameImpl)name).getElement(mech));

        tempCred = gssManager.getCredentialElement(nameElement,
                                                   initLifetime,
                                                   acceptLifetime,
                                                   mech,
                                                   usage);
        /*
         * Not all mechanisms support the concept of one credential element
         * that can be used for both initiating and accepting a context. In
         * the event that an application requests usage INITIATE_AND_ACCEPT
         * for a credential from such a mechanism, the GSS framework will
         * need to obtain two different credential elements from the
         * mechanism, one that will have usage INITIATE_ONLY and another
         * that will have usage ACCEPT_ONLY. The mechanism will help the
         * GSS-API realize this by returning a credential element with
         * usage INITIATE_ONLY or ACCEPT_ONLY prompting it to make another
         * call to getCredentialElement, this time with the other usage
         * mode.
         */

        if (tempCred != null) {
            if (usage == GSSCredential.INITIATE_AND_ACCEPT &&
                (!tempCred.isAcceptorCredential() ||
                !tempCred.isInitiatorCredential())) {

                int currentUsage;
                int desiredUsage;

                if (!tempCred.isInitiatorCredential()) {
                    currentUsage = GSSCredential.ACCEPT_ONLY;
                    desiredUsage = GSSCredential.INITIATE_ONLY;
                } else {
                    currentUsage = GSSCredential.INITIATE_ONLY;
                    desiredUsage = GSSCredential.ACCEPT_ONLY;
                }

                key = new SearchKey(mech, currentUsage);
                hashtable.put(key, tempCred);

                tempCred = gssManager.getCredentialElement(nameElement,
                                                        initLifetime,
                                                        acceptLifetime,
                                                        mech,
                                                        desiredUsage);

                key = new SearchKey(mech, desiredUsage);
                hashtable.put(key, tempCred);
            } else {
                hashtable.put(key, tempCred);
            }
        }
    }

    public boolean equals(Object another) {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        if (this == another) {
            return true;
        }

        if (!(another instanceof GSSCredentialImpl)) {
            return false;
        }

        // NOTE: The specification does not define the criteria to compare
        // credentials.
        /*
         * XXX
         * The RFC says: "Tests if this GSSCredential refers to the same
         * entity as the supplied object.  The two credentials must be
         * acquired over the same mechanisms and must refer to the same
         * principal.  Returns "true" if the two GSSCredentials refer to
         * the same entity; "false" otherwise."
         *
         * Well, when do two credentials refer to the same principal? Do
         * they need to have one GSSName in common for the different
         * GSSName's that the credential elements return? Or do all
         * GSSName's have to be in common when the names are exported with
         * their respective mechanisms for the credential elements?
         */
        return false;

    }

    /**
     * Returns a hashcode value for this GSSCredential.
     *
     * @return a hashCode value
     */
    public int hashCode() {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        // NOTE: The specification does not define the criteria to compare
        // credentials.
        /*
         * XXX
         * Decide on a criteria for equals first then do this.
         */
        return 1;
    }

    /**
     * Returns the specified mechanism's credential-element.
     *
     * @param mechOid the oid for mechanism to retrieve
     * @param initiate boolean indicating if the function is
     *    to throw exception or return null when element is not
     *    found.
     * @return mechanism credential object
     * @exception GSSException of invalid mechanism
     */
    public GSSCredentialSpi getElement(Oid mechOid, boolean initiate)
        throws GSSException {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        SearchKey key;
        GSSCredentialSpi element;

        if (mechOid == null) {
            /*
             * First see if the default mechanism satisfies the
             * desired usage.
             */
            mechOid = ProviderList.DEFAULT_MECH_OID;
            key = new SearchKey(mechOid,
                                 initiate? INITIATE_ONLY : ACCEPT_ONLY);
            element = hashtable.get(key);
            if (element == null) {
                key = new SearchKey(mechOid, INITIATE_AND_ACCEPT);
                element = hashtable.get(key);
                if (element == null) {
                    /*
                     * Now just return any element that satisfies the
                     * desired usage.
                     */
                    Object[] elements = hashtable.entrySet().toArray();
                    for (int i = 0; i < elements.length; i++) {
                        element = (GSSCredentialSpi)
                            ((Map.Entry)elements[i]).getValue();
                        if (element.isInitiatorCredential() == initiate)
                            break;
                    } // for loop
                }
            }
        } else {

            if (initiate)
                key = new SearchKey(mechOid, INITIATE_ONLY);
            else
                key = new SearchKey(mechOid, ACCEPT_ONLY);

            element = hashtable.get(key);

            if (element == null) {
                key = new SearchKey(mechOid, INITIATE_AND_ACCEPT);
                element = hashtable.get(key);
            }
        }

        if (element == null)
            throw new GSSExceptionImpl(GSSException.NO_CRED,
                                       "No credential found for: " +
                                       getElementStr(mechOid,
                                       initiate? INITIATE_ONLY : ACCEPT_ONLY));
        return element;
    }

    Set<GSSCredentialSpi> getElements() {
        HashSet<GSSCredentialSpi> retVal =
                        new HashSet<GSSCredentialSpi>(hashtable.size());
        Enumeration<GSSCredentialSpi> values = hashtable.elements();
        while (values.hasMoreElements()) {
            GSSCredentialSpi o = values.nextElement();
            retVal.add(o);
        }
        return retVal;
    }

    private static String getElementStr(Oid mechOid, int usage) {
        String displayString = mechOid.toString();
        if (usage == GSSCredential.INITIATE_ONLY) {
            displayString =
                displayString.concat(" usage: Initiate");
        } else if (usage == GSSCredential.ACCEPT_ONLY) {
            displayString =
                displayString.concat(" usage: Accept");
        } else {
            displayString =
                displayString.concat(" usage: Initiate and Accept");
        }
        return displayString;
    }

    public String toString() {

        if (destroyed) {
            throw new IllegalStateException("This credential is " +
                                        "no longer valid");
        }

        GSSCredentialSpi element = null;
        StringBuffer buffer = new StringBuffer("[GSSCredential: ");
        Object[] elements = hashtable.entrySet().toArray();
        for (int i = 0; i < elements.length; i++) {
            try {
                buffer.append('\n');
                element = (GSSCredentialSpi)
                    ((Map.Entry)elements[i]).getValue();
                buffer.append(element.getName());
                buffer.append(' ');
                buffer.append(element.getMechanism());
                buffer.append(element.isInitiatorCredential() ?
                              " Initiate" : "");
                buffer.append(element.isAcceptorCredential() ?
                              " Accept" : "");
                buffer.append(" [");
                buffer.append(element.getClass());
                buffer.append(']');
            } catch (GSSException e) {
                // skip to next element
            }
        }
        buffer.append(']');
        return buffer.toString();
    }

    static class SearchKey {
        private Oid mechOid = null;
        private int usage = GSSCredential.INITIATE_AND_ACCEPT;
        public SearchKey(Oid mechOid, int usage) {

            this.mechOid = mechOid;
            this.usage = usage;
        }
        public Oid getMech() {
            return mechOid;
        }
        public int getUsage() {
            return usage;
        }
        public boolean equals(Object other) {
            if (! (other instanceof SearchKey))
                return false;
            SearchKey that = (SearchKey) other;
            return ((this.mechOid.equals(that.mechOid)) &&
                    (this.usage == that.usage));
        }
        public int hashCode() {
            return mechOid.hashCode();
        }
    }

}
