/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 javax.security.auth;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Principal;
import java.util.Set;

/**
 * Protects private credential objects belonging to a {@code Subject}. It has
 * only one action which is "read". The target name of this permission has a
 * special syntax:
 *
 * <pre>
 * targetName = CredentialClass {PrincipalClass &quot;PrincipalName&quot;}*
 * </pre>
 *
 * First it states a credential class and is followed then by a list of one or
 * more principals identifying the subject.
 * <p>
 * The principals on their part are specified as the name of the {@code
 * Principal} class followed by the principal name in quotes. For example, the
 * following file may define permission to read the private credentials of a
 * principal named "Bob": "com.sun.PrivateCredential com.sun.Principal \"Bob\""
 * <p>
 * The syntax also allows the use of the wildcard "*" in place of {@code
 * CredentialClass} or {@code PrincipalClass} and/or {@code PrincipalName}.
 *
 * @see Principal
 */
public final class PrivateCredentialPermission extends Permission {

    private static final long serialVersionUID = 5284372143517237068L;

    // allowed action
    private static final String READ = "read";

    private String credentialClass;

    // current offset
    private transient int offset;

    // owners set
    private transient CredOwner[] set;

    /**
     * Creates a new permission for private credentials specified by the target
     * name {@code name} and an {@code action}. The action is always
     * {@code "read"}.
     *
     * @param name
     *            the target name of the permission.
     * @param action
     *            the action {@code "read"}.
     */
    public PrivateCredentialPermission(String name, String action) {
        super(name);
        if (READ.equalsIgnoreCase(action)) {
            initTargetName(name);
        } else {
            throw new IllegalArgumentException("Action must be \"read\"");
        }
    }

    /**
     * Creates a {@code PrivateCredentialPermission} from the {@code Credential}
     * class and set of principals.
     *
     * @param credentialClass
     *            the credential class name.
     * @param principals
     *            the set of principals.
     */
    PrivateCredentialPermission(String credentialClass, Set<Principal> principals) {
        super(credentialClass);
        this.credentialClass = credentialClass;

        set = new CredOwner[principals.size()];
        for (Principal p : principals) {
            CredOwner element = new CredOwner(p.getClass().getName(), p.getName());
            // check for duplicate elements
            boolean found = false;
            for (int ii = 0; ii < offset; ii++) {
                if (set[ii].equals(element)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                set[offset++] = element;
            }
        }
    }

    /**
     * Initialize a PrivateCredentialPermission object and checks that a target
     * name has a correct format: CredentialClass 1*(PrincipalClass
     * "PrincipalName")
     */
    private void initTargetName(String name) {

        if (name == null) {
            throw new NullPointerException("name == null");
        }

        // check empty string
        name = name.trim();
        if (name.isEmpty()) {
            throw new IllegalArgumentException("name is empty");
        }

        // get CredentialClass
        int beg = name.indexOf(' ');
        if (beg == -1) {
            throw badSyntax();
        }
        credentialClass = name.substring(0, beg);

        // get a number of pairs: PrincipalClass "PrincipalName"
        beg++;
        int count = 0;
        int nameLength = name.length();
        for (int i, j = 0; beg < nameLength; beg = j + 2, count++) {
            i = name.indexOf(' ', beg);
            j = name.indexOf('"', i + 2);

            if (i == -1 || j == -1 || name.charAt(i + 1) != '"') {
                throw badSyntax();
            }
        }

        // name MUST have one pair at least
        if (count < 1) {
            throw badSyntax();
        }

        beg = name.indexOf(' ');
        beg++;

        // populate principal set with instances of CredOwner class
        String principalClass;
        String principalName;

        set = new CredOwner[count];
        for (int index = 0, i, j; index < count; beg = j + 2, index++) {
            i = name.indexOf(' ', beg);
            j = name.indexOf('"', i + 2);

            principalClass = name.substring(beg, i);
            principalName = name.substring(i + 2, j);

            CredOwner element = new CredOwner(principalClass, principalName);
            // check for duplicate elements
            boolean found = false;
            for (int ii = 0; ii < offset; ii++) {
                if (set[ii].equals(element)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                set[offset++] = element;
            }
        }
    }

    private IllegalArgumentException badSyntax() {
        throw new IllegalArgumentException("Target name MUST have the following syntax: " +
                "CredentialClass 1*(PrincipalClass \"PrincipalName\")");
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        initTargetName(getName());
    }

    /**
     * Returns the principal's classes and names associated with this {@code
     * PrivateCredentialPermission} as a two dimensional array. The first
     * dimension of the array corresponds to the number of principals. The
     * second dimension defines either the name of the {@code PrincipalClass}
     * [x][0] or the value of {@code PrincipalName} [x][1].
     * <p>
     * This corresponds to the the target name's syntax:
     *
     * <pre>
     * targetName = CredentialClass {PrincipalClass &quot;PrincipalName&quot;}*
     * </pre>
     *
     * @return the principal classes and names associated with this {@code
     *         PrivateCredentialPermission}.
     */
    public String[][] getPrincipals() {

        String[][] s = new String[offset][2];

        for (int i = 0; i < s.length; i++) {
            s[i][0] = set[i].principalClass;
            s[i][1] = set[i].principalName;
        }
        return s;
    }

    @Override
    public String getActions() {
        return READ;
    }

    /**
     * Returns the class name of the credential associated with this permission.
     *
     * @return the class name of the credential associated with this permission.
     */
    public String getCredentialClass() {
        return credentialClass;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        for (int i = 0; i < offset; i++) {
            hash = hash + set[i].hashCode();
        }
        return getCredentialClass().hashCode() + hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }

        if (obj == null || this.getClass() != obj.getClass()) {
            return false;
        }

        PrivateCredentialPermission that = (PrivateCredentialPermission) obj;

        return credentialClass.equals(that.credentialClass) && (offset == that.offset)
                && sameMembers(set, that.set, offset);
    }

    @Override
    public boolean implies(Permission permission) {

        if (permission == null || this.getClass() != permission.getClass()) {
            return false;
        }

        PrivateCredentialPermission that = (PrivateCredentialPermission) permission;

        if (!("*".equals(credentialClass) || credentialClass
                .equals(that.getCredentialClass()))) {
            return false;
        }

        if (that.offset == 0) {
            return true;
        }

        CredOwner[] thisCo = set;
        CredOwner[] thatCo = that.set;
        int thisPrincipalsSize = offset;
        int thatPrincipalsSize = that.offset;
        for (int i = 0, j; i < thisPrincipalsSize; i++) {
            for (j = 0; j < thatPrincipalsSize; j++) {
                if (thisCo[i].implies(thatCo[j])) {
                    break;
                }
            }
            if (j == thatCo.length) {
                return false;
            }
        }
        return true;
    }

    @Override
    public PermissionCollection newPermissionCollection() {
        return null;
    }

    /**
     * Returns true if the two arrays have the same length, and every member of
     * one array is contained in another array
     */
    private boolean sameMembers(Object[] ar1, Object[] ar2, int length) {
        if (ar1 == null && ar2 == null) {
            return true;
        }
        if (ar1 == null || ar2 == null) {
            return false;
        }
        boolean found;
        for (int i = 0; i < length; i++) {
            found = false;
            for (int j = 0; j < length; j++) {
                if (ar1[i].equals(ar2[j])) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }

    private static final class CredOwner implements Serializable {

        private static final long serialVersionUID = -5607449830436408266L;

        String principalClass;

        String principalName;

        // whether class name contains wildcards
        private transient boolean isClassWildcard;

        // whether pname contains wildcards
        private transient boolean isPNameWildcard;

        // Creates a new CredOwner with the specified Principal Class and Principal Name
        CredOwner(String principalClass, String principalName) {
            super();
            if ("*".equals(principalClass)) {
                isClassWildcard = true;
            }

            if ("*".equals(principalName)) {
                isPNameWildcard = true;
            }

            if (isClassWildcard && !isPNameWildcard) {
                throw badPrincipal();
            }

            this.principalClass = principalClass;
            this.principalName = principalName;
        }

        private IllegalArgumentException badPrincipal() {
            throw new IllegalArgumentException("invalid syntax: Principal Class can not be a " +
                    "wildcard (*) value if Principal Name is not a wildcard (*) value");
        }

        // Checks if this CredOwner implies the specified Object.
        boolean implies(Object obj) {
            if (obj == this) {
                return true;
            }

            CredOwner co = (CredOwner) obj;

            if (isClassWildcard || principalClass.equals(co.principalClass)) {
                if (isPNameWildcard || principalName.equals(co.principalName)) {
                    return true;
                }
            }
            return false;
        }

        // Checks two CredOwner objects for equality.
        @Override
        public boolean equals(Object obj) {
            return principalClass.equals(((CredOwner) obj).principalClass)
                    && principalName.equals(((CredOwner) obj).principalName);
        }

        // Returns the hash code value for this object.
        @Override
        public int hashCode() {
            return principalClass.hashCode() + principalName.hashCode();
        }
    }
}
