/*
 * Copyright (c) OSGi Alliance (2004, 2017). All Rights Reserved.
 *
 * Licensed 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 org.osgi.service.dmt.security;

import java.security.Permission;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;

/**
 * Indicates the callers authority to send alerts to management servers,
 * identified by their principal names.
 * <p>
 * {@code AlertPermission} has a target string which controls the principal
 * names where alerts can be sent. A wildcard is allowed at the end of the
 * target string, to allow sending alerts to any principal with a name matching
 * the given prefix. The &quot;*&quot; target means that alerts can be sent to
 * any destination.
 * 
 * @author $Id: cfbfdadc581fb700f0a67bff7fdc67772d18a313 $
 */
public class AlertPermission extends Permission {
	private static final long	serialVersionUID	= -3206463101788245739L;

	// specifies whether the target string had a wildcard at the end
	private final boolean		isPrefix;

	// the target string without the wildcard (if there was one)
	private final String		serverId;

	/**
	 * Creates a new {@code AlertPermission} object with its name set to the
	 * target string. Name must be non-null and non-empty.
	 * 
	 * @param target the name of a principal, can end with {@code *} to match
	 *        any principal identifier with the given prefix
	 * @throws NullPointerException if {@code name} is {@code null}
	 * @throws IllegalArgumentException if {@code name} is empty
	 */
	public AlertPermission(String target) {
		super(target);

		if (target == null)
			throw new NullPointerException("'target' parameter must not be null.");

		if (target.equals(""))
			throw new IllegalArgumentException("'target' parameter must not be empty.");

		isPrefix = target.endsWith("*");
		if (isPrefix)
			serverId = target.substring(0, target.length() - 1);
		else
			serverId = target;
	}

	/**
	 * Creates a new {@code AlertPermission} object using the 'canonical' two
	 * argument constructor. In this version this class does not define any
	 * actions, the second argument of this constructor must be "*" so that this
	 * class can later be extended in a backward compatible way.
	 * 
	 * @param target the name of the server, can end with {@code *} to match any
	 *        server identifier with the given prefix
	 * @param actions no actions defined, must be "*" for forward compatibility
	 * @throws NullPointerException if {@code name} or {@code actions} is
	 *         {@code null}
	 * @throws IllegalArgumentException if {@code name} is empty or
	 *         {@code actions} is not "*"
	 */
	public AlertPermission(String target, String actions) {
		this(target);

		if (actions == null)
			throw new NullPointerException("'actions' parameter must not be null.");

		if (!actions.equals("*"))
			throw new IllegalArgumentException("'actions' parameter must be \"*\".");
	}

	/**
	 * Checks whether the given object is equal to this AlertPermission
	 * instance. Two AlertPermission instances are equal if they have the same
	 * target string.
	 * 
	 * @param obj the object to compare to this AlertPermission instance
	 * @return {@code true} if the parameter represents the same permissions as
	 *         this instance
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == this)
			return true;

		if (!(obj instanceof AlertPermission))
			return false;

		AlertPermission other = (AlertPermission) obj;

		return isPrefix == other.isPrefix && serverId.equals(other.serverId);
	}

	/**
	 * Returns the action list (always {@code *} in the current version).
	 * 
	 * @return the action string &quot;*&quot;
	 */
	@Override
	public String getActions() {
		return "*";
	}

	/**
	 * Returns the hash code for this permission object. If two AlertPermission
	 * objects are equal according to the {@link #equals(Object)} method, then
	 * calling this method on each of the two AlertPermission objects must
	 * produce the same integer result.
	 * 
	 * @return hash code for this permission object
	 */
	@Override
	public int hashCode() {
		return Boolean.valueOf(isPrefix).hashCode() ^ serverId.hashCode();
	}

	/**
	 * Checks if this AlertPermission object implies the specified permission.
	 * Another AlertPermission instance is implied by this permission either if
	 * the target strings are identical, or if this target can be made identical
	 * to the other target by replacing a trailing &quot;*&quot; with any
	 * string.
	 * 
	 * @param p the permission to check for implication
	 * @return true if this AlertPermission instance implies the specified
	 *         permission
	 */
	@Override
	public boolean implies(Permission p) {
		if (!(p instanceof AlertPermission))
			return false;

		AlertPermission other = (AlertPermission) p;

		return impliesServer(other);
	}

	/**
	 * Returns a new PermissionCollection object for storing AlertPermission
	 * objects.
	 * 
	 * @return the new PermissionCollection
	 */
	@Override
	public PermissionCollection newPermissionCollection() {
		return new DmtAlertPermissionCollection();
	}

	/*
	 * Returns true if the server name parameter of the given AlertPermission is
	 * implied by the server name of this permission, i.e. this server name is a
	 * prefix of the other one but ends with a *, or the two server names are
	 * equal.
	 */
	boolean impliesServer(AlertPermission p) {
		return isPrefix ? p.serverId.startsWith(serverId) : !p.isPrefix && p.serverId.equals(serverId);
	}
}

/**
 * Represents a homogeneous collection of AlertPermission objects.
 */
final class DmtAlertPermissionCollection extends PermissionCollection {
	private static final long	serialVersionUID	= 2288462124510043429L;

	private ArrayList<Permission>	perms;

	/**
	 * Create an empty DmtAlertPermissionCollection object.
	 */
	public DmtAlertPermissionCollection() {
		perms = new ArrayList<>();
	}

	/**
	 * Adds a permission to the DmtAlertPermissionCollection.
	 * 
	 * @param permission the Permission object to add
	 * @exception IllegalArgumentException if the permission is not a
	 *            AlertPermission
	 * @exception SecurityException if this DmtAlertPermissionCollection object
	 *            has been marked readonly
	 */
	@Override
	public void add(Permission permission) {
		if (!(permission instanceof AlertPermission))
			throw new IllegalArgumentException("Cannot add permission, invalid permission type: " + permission);
		if (isReadOnly())
			throw new SecurityException("Cannot add permission, collection is marked read-only.");

		// only add new permission if it is not already implied by the
		// permissions in the collection
		if (!implies(permission)) {
			// remove all permissions that are implied by the new one
			Iterator<Permission> i = perms.iterator();
			while (i.hasNext())
				if (permission.implies(i.next()))
					i.remove();

			// no need to synchronize because all adds are done sequentially
			// before any implies() calls
			perms.add(permission);

		}
	}

	/**
	 * Check whether this set of permissions implies the permission specified in
	 * the parameter.
	 * 
	 * @param permission the Permission object to compare
	 * @return true if the parameter permission is a proper subset of the
	 *         permissions in the collection, false otherwise
	 */
	@Override
	public boolean implies(Permission permission) {
		if (!(permission instanceof AlertPermission))
			return false;

		AlertPermission other = (AlertPermission) permission;

		Iterator<Permission> i = perms.iterator();
		while (i.hasNext())
			if (((AlertPermission) i.next()).impliesServer(other))
				return true;

		return false;
	}

	/**
	 * Returns an enumeration of all the AlertPermission objects in the
	 * container. The returned value cannot be {@code null}.
	 * 
	 * @return an enumeration of all the AlertPermission objects
	 */
	@Override
	public Enumeration<Permission> elements() {
		// Convert Iterator into Enumeration
		return Collections.enumeration(perms);
	}
}
