/*
 *  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 java.security;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import org.apache.harmony.security.fortress.PolicyUtils;

/**
 * An {@code UnresolvedPermission} represents a {@code Permission} whose type
 * should be resolved lazy and not during initialization time of the {@code
 * Policy}. {@code UnresolvedPermission}s contain all information to be replaced
 * by a concrete typed {@code Permission} right before the access checks are
 * performed.
 */
public final class UnresolvedPermission extends Permission
    implements Serializable {

    private static final long serialVersionUID = -4821973115467008846L;

    private String type;

    private String name;

    private String actions;

    // The signer certificates
    private transient Certificate[] targetCerts;

    // Cached hash value
    private transient int hash;

    /**
     * Constructs a new instance of {@code UnresolvedPermission}. The supplied
     * parameters are used when this instance is resolved to the concrete
     * {@code Permission}.
     *
     * @param type
     *            the fully qualified class name of the permission this class is
     *            resolved to.
     * @param name
     *            the name of the permission this class is resolved to, maybe
     *            {@code null}.
     * @param actions
     *            the actions of the permission this class is resolved to, maybe
     *            {@code null}.
     * @param certs
     *            the certificates of the permission this class is resolved to,
     *            maybe {@code null}.
     * @throws NullPointerException
     *             if type is {@code null}.
     */
    public UnresolvedPermission(String type, String name, String actions,
                                Certificate[] certs) {
        super(type);
        checkType(type);
        this.type = type;
        this.name = name;
        this.actions = actions;
        if (certs != null) {
            this.targetCerts = new Certificate[certs.length];
            System.arraycopy(certs, 0, targetCerts, 0, certs.length);
        }
        hash = 0;
    }

    // Check type parameter
    private final void checkType(String type) {
        if (type == null) {
            throw new NullPointerException("type == null");
        }

        // type is the class name of the Permission class.
        // Empty string is inappropriate for class name.
        // But this check is commented out for compatibility with RI.
        // see JIRA issue HARMONY-733
        // if (type.length() == 0) {
        //     throw new IllegalArgumentException("type cannot be empty");
        // }
    }

    /**
     * Compares the specified object with this {@code UnresolvedPermission} for
     * equality and returns {@code true} if the specified object is equal,
     * {@code false} otherwise. To be equal, the specified object needs to be an
     * instance of {@code UnresolvedPermission}, the two {@code
     * UnresolvedPermission}s must refer to the same type and must have the same
     * name, the same actions and certificates.
     *
     * @param obj
     *            object to be compared for equality with this {@code
     *            UnresolvedPermission}.
     * @return {@code true} if the specified object is equal to this {@code
     *         UnresolvedPermission}, otherwise {@code false}.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof UnresolvedPermission) {
            UnresolvedPermission that = (UnresolvedPermission) obj;
            if (getName().equals(that.getName())
                    && (name == null ? that.name == null : name
                            .equals(that.name))
                    && (actions == null ? that.actions == null : actions
                            .equals(that.actions))
                    && equalsCertificates(this.targetCerts, that.targetCerts)) {
                return true;
            }
        }
        return false;
    }

    /*
     * check whether given array of certificates are equivalent
     */
    private boolean equalsCertificates(Certificate[] certs1,
            Certificate[] certs2) {
        if (certs1 == null || certs2 == null) {
            return certs1 == certs2;
        }

        int length = certs1.length;
        if (length != certs2.length) {
            return false;
        }

        if (length > 0) {
            boolean found;
            for (int i = 0; i < length; i++) {
                // Skip the checking for null
                if(certs1[i] == null){
                    continue;
                }
                found = false;
                for (int j = 0; j < length; j++) {
                    if (certs1[i].equals(certs2[j])) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    return false;
                }
            }

            for (int i = 0; i < length; i++) {
                if(certs2[i] == null){
                    continue;
                }
                found = false;
                for (int j = 0; j < length; j++) {
                    if (certs2[i].equals(certs1[j])) {
                        found = true;
                        break;
                    }
                }

                if (!found) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Returns the hash code value for this {@code UnresolvedPermission}.
     * Returns the same hash code for {@code UnresolvedPermission}s that are
     * equal to each other as required by the general contract of
     * {@link Object#hashCode}.
     *
     * @return the hash code value for this {@code UnresolvedPermission}.
     * @see Object#equals(Object)
     * @see UnresolvedPermission#equals(Object)
     */
    @Override
    public int hashCode() {
        if (hash == 0) {
            hash = getName().hashCode();
            if (name != null) {
                hash ^= name.hashCode();
            }
            if (actions != null) {
                hash ^= actions.hashCode();
            }
        }
        return hash;
    }

    /**
     * Returns an empty string since there are no actions allowed for {@code
     * UnresolvedPermission}. The actions, specified in the constructor, are
     * used when the concrete permission is resolved and created.
     *
     * @return an empty string, indicating that there are no actions.
     */
    @Override
    public String getActions() {
        return "";
    }

    /**
     * Returns the name of the permission this {@code UnresolvedPermission} is
     * resolved to.
     *
     * @return the name of the permission this {@code UnresolvedPermission} is
     *         resolved to.
     */
    public String getUnresolvedName() {
        return name;
    }

    /**
     * Returns the actions of the permission this {@code UnresolvedPermission}
     * is resolved to.
     *
     * @return the actions of the permission this {@code UnresolvedPermission}
     *         is resolved to.
     */
    public String getUnresolvedActions() {
        return actions;
    }

    /**
     * Returns the fully qualified class name of the permission this {@code
     * UnresolvedPermission} is resolved to.
     *
     * @return the fully qualified class name of the permission this {@code
     *         UnresolvedPermission} is resolved to.
     */
    public String getUnresolvedType() {
        return super.getName();
    }

    /**
     * Returns the certificates of the permission this {@code
     * UnresolvedPermission} is resolved to.
     *
     * @return the certificates of the permission this {@code
     *         UnresolvedPermission} is resolved to.
     */
    public Certificate[] getUnresolvedCerts() {
        if (targetCerts != null) {
            Certificate[] certs = new Certificate[targetCerts.length];
            System.arraycopy(targetCerts, 0, certs, 0, certs.length);
            return certs;
        }
        return null;
    }

    /**
     * Indicates whether the specified permission is implied by this {@code
     * UnresolvedPermission}. {@code UnresolvedPermission} objects imply nothing
     * since nothing is known about them yet.
     * <p>
     * Before actual implication checking, this method tries to resolve
     * UnresolvedPermissions (if any) against the passed instance. Successfully
     * resolved permissions (if any) are taken into account during further
     * processing.
     *
     * @param permission
     *            the permission to check.
     * @return always {@code false}
     */
    @Override
    public boolean implies(Permission permission) {
        return false;
    }

    /**
     * Returns a string containing a concise, human-readable description of this
     * {@code UnresolvedPermission} including its target name and its target
     * actions.
     *
     * @return a printable representation for this {@code UnresolvedPermission}.
     */
    @Override
    public String toString() {
        return "(unresolved " + type + " " + name + " "
            + actions + ")";
    }

    /**
     * Returns a new {@code PermissionCollection} for holding {@code
     * UnresolvedPermission} objects.
     *
     * @return a new PermissionCollection for holding {@code
     *         UnresolvedPermission} objects.
     */
    @Override
    public PermissionCollection newPermissionCollection() {
        return new UnresolvedPermissionCollection();
    }

    /**
     * Tries to resolve this permission into the specified class.
     * <p>
     * It is assumed that the class has a proper name (as returned by {@code
     * getName()} of this unresolved permission), so no check is performed to
     * verify this. However, the class must have all required certificates (as
     * per {@code getUnresolvedCerts()}) among the passed collection of signers.
     * If it does, a zero, one, and/or two-argument constructor is tried to
     * instantiate a new permission, which is then returned.
     * <p>
     * If an appropriate constructor is not available or the class is improperly
     * signed, {@code null} is returned.
     *
     * @param targetType
     *            - a target class instance, must not be {@code null}
     * @return resolved permission or null
     */
    Permission resolve(Class targetType) {
        // check signers at first
        if (PolicyUtils.matchSubset(targetCerts, targetType.getSigners())) {
            try {
                return PolicyUtils.instantiatePermission(targetType,
                                                         name,
                                                         actions);
            } catch (Exception ignore) {
                //TODO log warning?
            }
        }
        return null;
    }

    /**
     * Outputs {@code type},{@code name},{@code actions}
     * fields via default mechanism; next manually writes certificates in the
     * following format: <br>
     *
     * <ol>
     * <li> int : number of certs or zero </li>
     * <li> each cert in the following format
     *     <ol>
     *     <li> String : certificate type </li>
     *     <li> int : length in bytes of certificate </li>
     *     <li> byte[] : certificate encoding </li>
     *     </ol>
     * </li>
     * </ol>
     *
     *  @see  <a href="http://java.sun.com/j2se/1.5.0/docs/api/serialized-form.html#java.security.UnresolvedPermission">Java Spec</a>
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        if (targetCerts == null) {
            out.writeInt(0);
        } else {
            out.writeInt(targetCerts.length);
            for (int i = 0; i < targetCerts.length; i++) {
                try {
                    byte[] enc = targetCerts[i].getEncoded();
                    out.writeUTF(targetCerts[i].getType());
                    out.writeInt(enc.length);
                    out.write(enc);
                } catch (CertificateEncodingException cee) {
                    throw (IOException) new NotSerializableException("Cannot encode certificate: " + targetCerts[i]).initCause(cee);
                }
            }
        }
    }

    /**
     * Reads the object from stream and checks target type for validity.
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        in.defaultReadObject();
        checkType(getUnresolvedType());
        int certNumber = in.readInt();
        if (certNumber != 0) {
            targetCerts = new Certificate[certNumber];
            for (int i = 0; i < certNumber; i++) {
                try {
                    String type = in.readUTF();
                    int length = in.readInt();
                    byte[] enc = new byte[length];
                    in.readFully(enc, 0, length);
                    targetCerts[i] = CertificateFactory.getInstance(type)
                        .generateCertificate(new ByteArrayInputStream(enc));
                } catch (CertificateException cee) {
                    throw (IOException) new IOException("Error decoding certificate").initCause(cee);
                }
            }
        }
    }
}
