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

import java.security.Permission;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;

/**
 * Represents permission to call a method. An instance of this class
 * contains a name (also referred to as a "target name") but no actions list;
 * you either have the named permission or you don't. The target name can be
 * any of the following forms:
 * <pre>
 * *
 * <i>Identifier</i>
 * *<i>Suffix</i>
 * <i>Identifier</i>*
 * <i>QualifiedIdentifier</i>.*
 * <i>QualifiedIdentifier</i>.<i>Identifier</i>
 * <i>QualifiedIdentifier</i>.*<i>Suffix</i>
 * <i>QualifiedIdentifier</i>.<i>Identifier</i>*
 * </pre>
 * where <i>QualifiedIdentifier</i> and <i>Identifier</i> are as defined in
 * <i>The Java(TM) Language Specification</i> except that whitespace is not
 * permitted, and <i>Suffix</i> is defined to be one or more characters
 * that may be part of an <i>Identifier</i>. These forms are defined to
 * match fully qualified names of the form
 * <i>QualifiedIdentifier</i>.<i>Identifier</i> as follows:
 * <table border=1 cellpadding=5
 *        summary="Describes target name forms and matching semantics">
 * <tr>
 * <th>Target Name</th>
 * <th><i>QualifiedIdentifier</i> Match</th>
 * <th><i>Identifier</i> Match</th>
 * </tr>
 * <tr>
 * <td>*</td>
 * <td>any</td>
 * <td>any</td>
 * </tr>
 * <tr>
 * <td><b>method</b></td>
 * <td>any</td>
 * <td><b>method</b></td>
 * </tr>
 * <tr>
 * <td>*<b>suffix</b></td>
 * <td>any</td>
 * <td>any ending with <b>suffix</b></td>
 * </tr>
 * <tr>
 * <td><b>prefix</b>*</td>
 * <td>any</td>
 * <td>any starting with <b>prefix</b></td>
 * </tr>
 * <tr>
 * <td><b>type</b>.*</td>
 * <td><b>type</b></td>
 * <td>any</td>
 * </tr>
 * <tr>
 * <td><b>type</b>.<b>method</b></td>
 * <td><b>type</b></td>
 * <td><b>method</b></td>
 * </tr>
 * <tr>
 * <td><b>type</b>.*<b>suffix</b></td>
 * <td><b>type</b></td>
 * <td>any ending with <b>suffix</b></td>
 * </tr>
 * <tr>
 * <td><b>type</b>.<b>prefix</b>*</td>
 * <td><b>type</b></td>
 * <td>any starting with <b>prefix</b></td>
 * </tr>
 * </table>
 * <p>
 * This class, and simple subclasses of it, can be used (for example) with
 * {@link net.jini.jeri.BasicInvocationDispatcher}. It is
 * recommended that a simple subclass of this class be defined for each
 * remote object implementation class that can be exported using an
 * {@link net.jini.export.Exporter}, to allow separation of
 * grants in policy files.
 *
 * 
 * @since 2.0
 */
public class AccessPermission extends Permission {
    private static final long serialVersionUID = 7269818741475881138L;

    /**
     * The interface name, or null if wildcarded.
     */
    private transient String iface;
    /**
     * The name of the method, with prefix or suffix '*' permitted,
     * or null if wildcarded.
     */
    private transient String method;

    /**
     * Creates an instance with the specified target name.
     *
     * @param name the target name
     * @throws NullPointerException if the target name is <code>null</code>
     * @throws IllegalArgumentException if the target name does not match
     * the syntax specified in the comments at the beginning of this class
     */
    public AccessPermission(String name) {
	super(name);
	init(name);
    }

    /**
     * Parses the target name and initializes the transient fields.
     */
    private void init(String name) {
	if (name == null) {
	    throw new NullPointerException("name cannot be null");
	} else if (name.length() == 0) {
	    throw new IllegalArgumentException("name cannot be empty");
	}
	int i = name.lastIndexOf('.');
	if (i >= 0) {
	    iface = name.substring(0, i);
	    name = name.substring(i + 1);
	}
	if (!name.equals("*")) {
	    method = name;
	}
	if (iface != null && !validClass(iface)) {
	    throw new IllegalArgumentException("invalid interface name");
	} else if (method != null && !validMethod(method)) {
	    throw new IllegalArgumentException("invalid method name");
	}
    }

    /**
     * Returns true if the name is a syntactically valid fully qualified
     * class name (no whitespace permitted), and returns false otherwise.
     */
    private static boolean validClass(String name) {
	int len = name.length();
    outer:
	for (int i = 0;
	     i < len && Character.isJavaIdentifierStart(name.charAt(i));
	     i++)
	{
	    while (++i < len) {
		char c = name.charAt(i);
		if (c == '.') {
		    continue outer;
		}
		if (!Character.isJavaIdentifierPart(c)) {
		    return false;
		}
	    }
	    return true;
	}
	return false;
    }

    /**
     * Returns true if the name is a syntactically valid method name, or
     * if the name is a syntactically valid method name with a '*' appended
     * or could be constructed from some syntactically valid method name
     * containing more than two characters by replacing the first character
     * of that name with '*', and returns false otherwise.
     */
    private static boolean validMethod(String name) {
	int len = name.length();
	if (len == 0) {
	    return false;
	}
	char c = name.charAt(0);
	if (!Character.isJavaIdentifierStart(c) && !(c == '*' && len > 1)) {
	    return false;
	}
	if (c != '*' && name.charAt(len - 1) == '*') {
	    len--;
	}
	while (--len >= 1) {
	    if (!Character.isJavaIdentifierPart(name.charAt(len))) {
		return false;
	    }
	}
	return true;
    }

    /**
     * Returns <code>true</code> if every fully qualified name that
     * matches the specified permission's name also matches this
     * permission's name; returns <code>false</code> otherwise.
     *
     * @param perm the permission to check
     * @return <code>true</code> if every fully qualified name that
     * matches the specified permission's name also matches this
     * permission's name; <code>false</code> otherwise
     */
    public boolean implies(Permission perm) {
	if (perm == null || perm.getClass() != getClass()) {
	    return false;
	}
	AccessPermission ap = (AccessPermission) perm;
	if (iface != null && !iface.equals(ap.iface)) {
	    return false;
	} else if (method == null) {
	    return true;
	} else if (ap.method == null) {
	    return false;
	}
	int len = method.length() - 1;
	if (method.charAt(0) == '*') {
	    return ap.method.regionMatches(ap.method.length() - len,
					   method, 1, len);
	} else if (method.charAt(len) == '*') {
	    return ap.method.regionMatches(0, method, 0, len);
	}
	return method.equals(ap.method);
    }

    /**
     * Returns <code>true</code> if the specified object is an instance
     * of the same class as this permission and has the same target name
     * as this permission; returns <code>false</code> otherwise.
     */
    public boolean equals(Object obj) {
	if (obj == this) {
	    return true;
	} else if (obj == null || obj.getClass() != getClass()) {
	    return false;
	}
	return getName().equals(((Permission) obj).getName());
    }

    /**
     * Returns a hash code value for this object.
     */
    public int hashCode() {
	return getName().hashCode();
    }

    /**
     * Returns the empty string.
     */
    public String getActions() {
	return "";
    }

    /**
     * Verifies the syntax of the target name and recreates any transient
     * state.
     *
     * @throws InvalidObjectException if the target name is <code>null</code>,
     * or if the target name does not match the syntax specified in the
     * comments at the beginning of this class
     */
    private void readObject(ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
	s.defaultReadObject();
	Exception cause;
	try {
	    init(getName());
	    return;
	} catch (NullPointerException e) {
	    cause = e;
	} catch (IllegalArgumentException e) {
	    cause = e;
	}
	InvalidObjectException e =
	    new InvalidObjectException(cause.getMessage());
	e.initCause(cause);
	throw e;
    }
}
