/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.security;

import java.util.*;
import org.apache.log4j.*;
import org.w3c.dom.*;
import com.silverwrist.util.DOMElementHelper;
import com.silverwrist.venice.except.AccessError;
import com.silverwrist.venice.except.ConfigException;
import com.silverwrist.venice.svc.SecurityMonitorEnvironment;
import com.silverwrist.venice.util.XMLLoader;

/**
 * A <CODE>SecurityMonitor</CODE> which is configured by means of XML data, supplied by means of a Venice
 * configuration file.  The configuration file defines roles, role lists, defaults, and permissions within
 * the security monitor's scope.
 *
 * @author Eric J. Bowersox &lt;erbo@silcom.com&gt;
 * @version X
 */
public class StaticSecurityMonitor implements SecurityMonitor
{
  /*--------------------------------------------------------------------------------
   * Internal class for evaluating static permissions
   *--------------------------------------------------------------------------------
   */

  final class StaticPermission
  {
    private Role role;           // the role for the test
    private String message;      // the failure message

    StaticPermission(Role role, String message)
    {
      this.role = role;
      this.message = message;

    } // end constructor

    final void test(int level, String errormessage) throws AccessError
    {
      if (!(role.isSatisfiedBy(level)))
      { // the static permission test failed!
	logger.warn("Static permission test (level " + level + " vs. role " + role + ") failed");
	if (errormessage==null)
	  errormessage = message;
	if (errormessage==null)
	  errormessage = "Operation not permitted.";
	throw new AccessError(errormessage);

      } // end if

    } // end test

    final boolean test(int level)
    {
      return role.isSatisfiedBy(level);

    } // end test

  } // end class StaticPermission

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(StaticSecurityMonitor.class);

  private static volatile SecurityMonitor root_monitor = null;

  private static final int DEFAULT_SCOPE_OFFSET = 3;

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private String id;               // the identity of this security monitor
  private ScopeInfo scope;         // the scope of this security monitor
  private SecurityMonitor parent;  // the parent of this security monitor
  private Map sym_to_role;         // mapping of role symbols to roles
  private Map level_to_role;       // mapping of role levels to roles
  private Map lists;               // mapping of list symbols to lists
  private Map default_roles;       // mapping of symbols to default values
  private Map static_permissions;  // mapping of symbols to static permissions
  private Set dynamic_permissions; // set of defined dynamic permission names

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  /**
   * Creates a new <CODE>StaticSecurityMonitor</CODE> from information stored as XML in a configuration file.
   *
   * @param cfg The root element of the security monitor configuration, which must be a
   *            <CODE>&lt;security-definition/&gt;</CODE> element.
   * @param env A mapping of all previously-known security monitors.  This one will be added to
   *            it on success.
   * @exception com.silverwrist.venice.except.ConfigException The XML configuration data was incorrect in
   *            some fashion.
   */
  public StaticSecurityMonitor(Element cfg, SecurityMonitorEnvironment env) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();
    boolean set_root_monitor = false;

    loader.configVerifyTagName(cfg,"security-definition");  // verify the tag name

    // Get the new security monitor's ID.
    id = loader.configGetAttribute(cfg,"id");
    if (logger.isDebugEnabled())
      logger.debug("defining new StaticSecurityMonitor with id=" + id);

    // Make sure this security monitor isn't already defined.
    if (env.isMonitorDefined(id))
    { // the monitor with this ID has already been defined!
      logger.fatal("security monitor with id=" + id + " is already defined!");
      throw new ConfigException("security monitor id=" + id + " is already defined!");

    } // end if

    // See if the security monitor has a parent attribute.
    DOMElementHelper root_h = new DOMElementHelper(cfg);
    if (root_h.hasAttribute("parent"))
    { // get the parent and determine if it exists or not
      String parent_id = cfg.getAttribute("parent");
      parent = env.getMonitor(parent_id);
      if (parent==null)
      { // no parent!  that's bogus!
	logger.fatal("parent security monitor with id=" + parent_id + " does not exist!");
	throw new ConfigException("parent security monitor with id=" + parent_id + " does not exist!");

      } // end if

      // Determine the new scope for this security monitor.
      int my_scope = parent.getScopeInfo().getScope();
      int my_offset = DEFAULT_SCOPE_OFFSET;

      if (root_h.hasAttribute("offset"))
      { // load the offset attribute and make sure it's in range
	my_offset = loader.configGetAttributeInt(cfg,"offset");
	if (my_offset<1)
	{ // the offset must be greater than or equal to 1!
	  logger.fatal("offset= value (" + my_offset + ") was out of range");
	  throw new ConfigException("offset= attribute of <security-definition/> must be >= 1");

	} // end if

      } // end if

      // Determine the final scope and check its validity.
      my_scope += my_offset;
      if (!(ScopeInfo.isValidScope(my_scope)))
      { // resulting scope is out of range!
	logger.fatal("scope for id=" + id + " comes out to " + my_scope + ", and that's not in range");
	throw new ConfigException("scope for security monitor id=" + id + " is out of range!");

      } // end if

      // allocate a scope info object with the new scope
      scope = new ScopeInfo(my_scope);

    } // end if (security monitor has parent)
    else
    { // this must be the root security monitor!
      if (env.isRootMonitorDefined())
      { // but we already have a root - can't be two roots!
	logger.fatal("trying to define root security monitor but we already have one");
	throw new ConfigException("root security monitor is already defined!");

      } // end if

      // we are the root security monitor...we live at scope 0, our parent is the primordial monitor
      set_root_monitor = true;
      scope = new ScopeInfo(0);
      parent = PrimordialSecurityMonitor.get();

    } // end else (security monitor is root)

    // get the defined roles
    Element sect = root_h.getSubElement("defined-roles");
    NodeList nl;
    int i;
    if (sect!=null)
    { // we need to define some roles here...
      HashMap tmp_sym_to_role = new HashMap();
      HashMap tmp_level_to_role = new HashMap();
      nl = sect.getChildNodes();
      for (i=0; i<nl.getLength(); i++)
      { // get each child node, see if it's a <role/>
	Node n = nl.item(i);
	if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("role")))
	{ // create the role and add it to the temporary 
	  Role r = createRole((Element)n);
	  tmp_sym_to_role.put(r.getSymbol(),r);
	  tmp_level_to_role.put(new Integer(r.getLevel()),r);

	} // end if

      } // end for

      if (tmp_sym_to_role.size()>0)
      { // save these off as unmodifiable maps
	sym_to_role = Collections.unmodifiableMap(tmp_sym_to_role);
	level_to_role = Collections.unmodifiableMap(tmp_level_to_role);

      } // end if
      else
      { // nothing defined here!
	sym_to_role = Collections.EMPTY_MAP;
	level_to_role = Collections.EMPTY_MAP;

      } // end else

    } // end if
    else
    { // I guess we don't define any roles!
      sym_to_role = Collections.EMPTY_MAP;
      level_to_role = Collections.EMPTY_MAP;

    } // end else

    // since lists may indirectly define default roles and permissions, create storage space for them
    HashMap tmp_default_roles = new HashMap();
    HashMap tmp_static_permissions = new HashMap();
    HashSet tmp_dynamic_permissions = new HashSet();

    // get the defined role lists
    sect = root_h.getSubElement("defined-lists");
    if (sect!=null)
    { // we need to define some role lists here!
      HashMap tmp_lists = new HashMap();
      nl = sect.getChildNodes();
      for (i=0; i<nl.getLength(); i++)
      { // get each child node, see if it's a <list/>
	Node n = nl.item(i);
	if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("list")))
	{ // create the role list and add it to the temporary map
	  // but first, get the ID
	  String list_id = id + "." + loader.configGetAttribute((Element)n,"id");

	  // now actually build the list and insert it
	  List rlist = buildList((Element)n,list_id,tmp_default_roles,tmp_static_permissions,
				 tmp_dynamic_permissions);
	  tmp_lists.put(list_id,rlist);

	} // end if

      } // end for

      if (tmp_lists.size()>0)
	lists = Collections.unmodifiableMap(tmp_lists);
      else
	lists = Collections.EMPTY_MAP;

    } // end if
    else // no lists defined here!
      lists = Collections.EMPTY_MAP;

    // Get the additional defined default roles.
    sect = root_h.getSubElement("defaults");
    if (sect!=null)
    { // get the nodes in the defaults section
      nl = sect.getChildNodes();
      for (i=0; i<nl.getLength(); i++)
      { // pick out each one and process it if it's a <default/>
	Node n = nl.item(i);
	if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("default")))
	  processDefault((Element)n,tmp_default_roles);

      } // end for

    } // end if
    // else no more defined defaults

    // Since that's it for the defaults, freeze the defaults list.
    if (tmp_default_roles.size()>0)
      default_roles = Collections.unmodifiableMap(tmp_default_roles);
    else
      default_roles = Collections.EMPTY_MAP;

    // Get the defined permissions.
    sect = root_h.getSubElement("permissions");
    if (sect!=null)
    { // get the nodes in the permissions section
      nl = sect.getChildNodes();
      for (i=0; i<nl.getLength(); i++)
      { // pick out each one and process it if it's a <permission/>
	Node n = nl.item(i);
	if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("permission")))
	  processPermission((Element)n,tmp_static_permissions,tmp_dynamic_permissions);

      } // end for

    } // end if
    // else no more defined permissions

    // That's now it for the permissions, so freeze those elements.
    if (tmp_static_permissions.size()>0)
      static_permissions = Collections.unmodifiableMap(tmp_static_permissions);
    else
      static_permissions = Collections.EMPTY_MAP;
    if (tmp_dynamic_permissions.size()>0)
      dynamic_permissions = Collections.unmodifiableSet(tmp_dynamic_permissions);
    else
      dynamic_permissions = Collections.EMPTY_SET;

    // Finish up by adding ourselves to the known monitors list.
    env.storeMonitor(this,set_root_monitor);

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private Role createRole(Element e) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();

    // Get the role symbol and automagically scope it.
    String symbol = id + "." + loader.configGetAttribute(e,"id");

    // Look for the value.
    String value_str = loader.configGetAttribute(e,"value").trim().toUpperCase();
    int level;
    if (value_str.equals("LMIN"))
      level = scope.getLowBandLow();
    else if (value_str.equals("LMAX"))
      level = scope.getLowBandHigh();
    else if (value_str.equals("HMIN"))
      level = scope.getHighBandLow();
    else if (value_str.equals("HMAX"))
      level = scope.getHighBandHigh();
    else if (   value_str.startsWith("L+") || value_str.startsWith("L-") || value_str.startsWith("H+")
	     || value_str.startsWith("H-"))
    { // take the characters following the 2-character prefix and convert them to an integer
      int offset;
      try
      { // convert the value and make sure it's not less than 0
	offset = Integer.parseInt(value_str.substring(2));
	if (offset<0)
	{ // don't want it less than zero here!
	  logger.fatal("offset value " + offset + " was out of range");
	  throw new ConfigException("offset value= attribute for <role/> was out of range",e);
	  
	} // end if

      } // end try
      catch (NumberFormatException nfe)
      { // not a numeric offset value
	logger.fatal("offset value \"" + value_str + "\" was not numeric");
	throw new ConfigException("offset value= attribute for <role/> was not properly numeric",e);

      } // end catch

      if (value_str.charAt(1)=='-')
	offset = -offset;  // compute as negative offset
      try
      { // now use the scope to compute the level!
	level = scope.getLevel((value_str.charAt(0)=='H'),offset);

      } // end try
      catch (IllegalArgumentException iae)
      { // we landed with a value outside the scope!
	logger.fatal("offset value \"" + value_str + "\" was not in the scope");
	throw new ConfigException("offset value= attribute for <role/> was not within the scope",e);

      } // end catch

    } // end else if
    else
    { // just a straight numeric level
      try
      { // parse it out and give it a scope check
	level = Integer.parseInt(value_str);
	if (!(scope.isInScope(level)))
	{ // not in the right scope - can't help you, pal!
	  logger.fatal("level value \"" + level + "\" was not in the scope");
	  throw new ConfigException("level value= attribute for <role/> was not within the scope",e);

	} // end if

      } // end try
      catch (NumberFormatException nfe)
      { // the level was not numeric
	logger.fatal("level value \"" + value_str + "\" was not numeric");
	throw new ConfigException("level value= attribute for <role/> was not properly numeric",e);

      } // end catch

    } // end else

    // Get the text; default to the symbol name if it doesn't exist.
    DOMElementHelper h = new DOMElementHelper(e);
    String text = h.getElementText();
    if (text==null)
      text = symbol;

    // create the resulting role!
    return Role.create(level,text,symbol);

  } // end createRole

  private List buildList(Element elem, String listid, Map defaultrole, Map static_perm, Set dynamic_perm)
      throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();

    // If there's a permission tag under this list, take it.
    DOMElementHelper h = new DOMElementHelper(elem);
    Element perm = h.getSubElement("permission");
    if (perm!=null)
    { // Find out what the permission is.
      DOMElementHelper ph = new DOMElementHelper(perm);
      if (ph.hasAttribute("role"))
      { // look up the role and make sure it corresponds to one we know
	Role role = this.getRole(perm.getAttribute("role"));
	if (role==null)
	{ // role not present!
	  logger.fatal("list <permission/> role (" + perm.getAttribute("role") + ") not defined");
	  throw new ConfigException("<permission/> inside of <list/> did not use defined role!",perm);

	} // end if

	// create a new StaticPermission and add it to the mapping
	StaticPermission sp = new StaticPermission(role,ph.getElementText());
	static_perm.put(listid,sp);

      } // end if
      else  // this is a dynamic permission, add it to the set
	dynamic_perm.add(listid);

    } // end if
    // else just skip this check

    // Begin loading the list elements.
    NodeList nl = elem.getChildNodes();
    ArrayList rc = new ArrayList(nl.getLength());
    boolean have_default = false;
    for (int i=0; i<nl.getLength(); i++)
    { // look for list elements
      Node n = nl.item(i);
      if ((n.getNodeType()==Node.ELEMENT_NODE) && (n.getNodeName().equals("element")))
      { // look at the attributes of this element node
	String rname = loader.configGetAttribute((Element)n,"role");
	Role r = this.getRole(rname);
	if (r==null)
	{ // the role is not defined!
	  logger.fatal("list <element/> role (" + rname + ") not defined");
	  throw new ConfigException("<element/> inside of <list/> did not use defined role!",(Element)n);

	} // end if

	rc.add(r);  // add element to defining list

	// Check and see if this item is a default.
	DOMElementHelper nh = new DOMElementHelper((Element)n);
	if (nh.hasAttribute("default"))
	{ // this is a default item...
	  if (have_default)
	  { // but there can't be two defaults!
	    logger.fatal("duplicate default= attributes in list <element/> nodes!");
	    throw new ConfigException("duplicate default= attribute in list <element/>",nh.getElement());

	  } // end if
	  else
	  { // we have a default for the list now!
	    defaultrole.put(listid,r);
	    have_default = true;

	  } // end else

	} // end if

      } // end if

    } // end for

    // Final prep on the list prior to returning it.
    Collections.sort(rc);
    rc.trimToSize();
    return Collections.unmodifiableList(rc);

  } // end buildlist

  private void processDefault(Element elem, Map defaultrole) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();

    // Start by getting the default ID.
    String def_id = id + "." + loader.configGetAttribute(elem,"id");

    // Now get the associated default role.
    Role r = this.getRole(loader.configGetAttribute(elem,"role"));
    if (r==null)
    { // no role found - this is an error!
      logger.fatal("<default/> role (" + elem.getAttribute("role") + ") not defined");
      throw new ConfigException("<default/> did not use defined role!",elem);

    } // end if

    // and save the default
    defaultrole.put(def_id,r);

  } // end processDefault

  private void processPermission(Element elem, Map static_perm, Set dynamic_perm) throws ConfigException
  {
    XMLLoader loader = XMLLoader.get();

    // Start by getting the permission ID.
    String perm_id = id + "." + loader.configGetAttribute(elem,"id");

    // Now get the associated role, if any.
    DOMElementHelper h = new DOMElementHelper(elem);
    if (h.hasAttribute("role"))
    { // this is a static permission; try and get the associated role
      Role r = this.getRole(elem.getAttribute("role"));
      if (r==null)
      { // no role found - this is an error!
	logger.fatal("<permission/> role (" + elem.getAttribute("role") + ") not defined");
	throw new ConfigException("<permission/> did not use defined role!",elem);

      } // end if

      // create static permission and add it
      StaticPermission sp = new StaticPermission(r,h.getElementText());
      static_perm.put(perm_id,sp);

    } // end if
    else // this is a dynamic permission; just add to our set
      dynamic_perm.add(perm_id);

  } // end processPermission

  /*--------------------------------------------------------------------------------
   * Implementations from interface SecurityMonitor
   *--------------------------------------------------------------------------------
   */

  /**
   * Tests a specified permission within this security monitor.  If the supplied security level is less
   * than the defined security level for this permission, an exception is thrown.
   *
   * @param symbol The programmatic symbol of the permission to test.
   * @param level The security level to supply for the test.
   * @param errormsg The error message to be thrown if the test fails.  May be <CODE>null</CODE>, in
   *                 which case the security monitor will throw an appropriate default, if applicable.
   * @return <CODE>true</CODE> if the specified permission was defined and satisfied; <CODE>false</CODE>
   *         if the specified permission was not defined within this security monitor.
   * @exception com.silverwrist.venice.except.AccessError If the specified permission was defined but
   *            not satisfied.
   * @see #testPermission(java.lang.String,int)
   */
  public boolean testPermission(String symbol, int level, String errormsg) throws AccessError
  {
    if (symbol==null)
      throw new NullPointerException("testPermission() got null symbol");
    StaticPermission sp = (StaticPermission)(static_permissions.get(symbol));
    if (sp==null)
    { // permission not found here - NOTE! Do not call to parent unless we are at the root level, as
      // permission tests always follow the DYNAMIC chain, not the static one!
      if (scope.getScope()==0)
	return parent.testPermission(symbol,level,errormsg);
      else
	return false;

    } // end if

    sp.test(level,errormsg); // will throw AccessError on failure
    return true;

  } // end testPermission

  /**
   * Tests a specified permission within this security monitor.  If the supplied security level is greater
   * than or equal to the defined security level for this permission, <CODE>true</CODE> is returned.
   *
   * @param symbol The programmatic symbol of the permission to test.
   * @param level The security level to supply for the test.
   * @return <CODE>true</CODE> if the specified permission was defined and satisfied; <CODE>false</CODE>
   *         if the specified permission was not defined within this security monitor, or was defined but
   *         not satisfied.
   * @see #testPermission(java.lang.String,int,java.lang.String)
   */
  public boolean testPermission(String symbol, int level)
  {
    if (symbol==null)
      throw new NullPointerException("testPermission() got null symbol");
    StaticPermission sp = (StaticPermission)(static_permissions.get(symbol));
    if (sp==null)
    { // permission not found here - NOTE! Do not call to parent unless we are at the root level, as
      // permission tests always follow the DYNAMIC chain, not the static one!
      if (scope.getScope()==0)
	return parent.testPermission(symbol,level);
      else
	return false;

    } // end if

    return sp.test(level);

  } // end testPermission

  /**
   * Determines whether this <CODE>SecurityMonitor</CODE> defines the specified permission symbol.
   *
   * @param symbol The programmatic symbol of the permission to look up.
   * @param no_follow If this is <CODE>true</CODE>, we look only in this <CODE>SecurityMonitor</CODE>
   *                  for the specified permission, and not in any "parent" <CODE>SecurityMonitor</CODE>
   *                  objects.
   * @return <CODE>true</CODE> if this is a defined permission symbol, <CODE>false</CODE> if not.
   */
  public boolean permissionDefined(String symbol, boolean no_follow)
  {
    if (symbol==null)
      throw new NullPointerException("permissionDefined() got null symbol");
    if (static_permissions.containsKey(symbol) || dynamic_permissions.contains(symbol))
      return true;
    if (no_follow)
      return false;
    return parent.permissionDefined(symbol,false);

  } // end permissionDefined

  /**
   * Returns a defined list of roles for this <CODE>SecurityMonitor</CODE>.
   *
   * @param symbol The programmatic symbol of the role list to look up.
   * @return The associated role list, or <CODE>null</CODE> if the role list name was not defined.
   * @see Role
   */
  public List getRoleList(String symbol)
  {
    if (symbol==null)
      throw new NullPointerException("getRoleList() got null symbol");
    List rc = (List)(lists.get(symbol));
    if (rc==null)
      rc = parent.getRoleList(symbol);
    return rc;

  } // end getRoleList

  /**
   * Returns a defined role for this <CODE>SecurityMonitor</CODE>.
   *
   * @param symbol The programmatic symbol of the role to look up.
   * @return The associated role, or <CODE>null</CODE> if the role name was not defined.
   * @see Role
   */
  public Role getRole(String symbol)
  {
    if (symbol==null)
      throw new NullPointerException("getRole() got null symbol");
    Role rc = (Role)(sym_to_role.get(symbol));
    if (rc==null)
      rc = parent.getRole(symbol);
    return rc;

  } // end getRole

  /**
   * Returns the defined role that has the given level.
   *
   * @param level The level value to look up.
   * @return The associated role, or <CODE>null</CODE> if no such role could be found.
   * @see Role
   */
  public Role getRoleForLevel(int level)
  {
    Role rc = (Role)(level_to_role.get(new Integer(level)));
    if (rc==null)
      rc = parent.getRoleForLevel(level);
    return rc;

  } // end getRoleForLevel

  /**
   * Returns a defined default role for this <CODE>SecurityMonitor</CODE>.
   *
   * @param symbol The programmatic symbol of the default role to look up.
   * @return The associated role, or <CODE>null</CODE> if the default role name was not defined.
   * @see Role
   */
  public Role getDefaultRole(String symbol)
  {
    if (symbol==null)
      throw new NullPointerException("getRole() got null symbol");
    Role rc = (Role)(default_roles.get(symbol));
    if (rc==null)
      rc = parent.getDefaultRole(symbol);
    return rc;

  } // end getDefaultRole

  /**
   * Returns the <CODE>ScopeInfo</CODE> associated with this <CODE>SecurityMonitor</CODE>.
   *
   * @return The <CODE>ScopeInfo</CODE> associated with this <CODE>SecurityMonitor</CODE>.
   * @see ScopeInfo
   */
  public ScopeInfo getScopeInfo()
  {
    return scope;

  } // end getScopeInfo

  /**
   * Returns the identifier for this <CODE>SecurityMonitor</CODE>.
   *
   * @return The identifier for this <CODE>SecurityMonitor</CODE>.
   */
  public String getID()
  {
    return id;

  } // end getID

} // end class StaticSecurityMonitor
