package examples.security.rdbmsrealm;


import java.lang.reflect.InvocationTargetException;
import java.security.Principal;
import java.security.acl.Acl;
import java.security.acl.Group;
import java.security.acl.Permission;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import weblogic.logging.LogOutputStream;
import weblogic.security.acl.AbstractManageableRealm;
import weblogic.security.acl.AclImpl;
import weblogic.security.acl.BasicRealm;
import weblogic.security.acl.DebuggableRealm;
import weblogic.security.acl.User;
import weblogic.security.audit.Audit;
import weblogic.utils.UnsupportedOperationException;
import weblogic.utils.reuse.Pool;


/**
 * RDBMS realm class.  For historical reasons, the database schema we
 * use does not allow empty groups or ACLs to be represented in the
 * database.  If you need to be able to represent empty groups or
 * ACLs, you must rearrange the schema, which may require some code
 * changes to this class and the RDBMSDelegate class. <p>
 *
 * This realm is mostly complete, but it does not implement some realm
 * methods (notably ACL creation and deletion).
 *
 * @author Copyright (c) 1998-2000 by BEA Systems, Inc. All Rights Reserved.
 */
public class RDBMSRealm
  extends AbstractManageableRealm
  implements DebuggableRealm
{
  /**
   * The name of the realm properties file we will try to load.
   */
  static final String RDBMS_PROPS = "rdbmsrealm.properties";
  
  /**
   * The name of the realm DDL file we will try to load.
   */
  static final String RDBMS_DDL = "rdbmsrealm.ddl";

  /**
   * The maximum number of pooled database connections to maintain at
   * any given time.
   */
  private static final int DEFAULT_POOL_SIZE = 6;
  
  /**
   * We maintain a pool of delegates, each with its own connection to
   * the database.  Access to the pool itself is synchronized, but
   * since only one thread will obtain a given delegate from the pool
   * at any time, we don't need to synchronize any other method calls.
   *
   * @see #getDelegate
   * @see #returnDelegate
   * @see RDBMSDelegate$DFactory
   */
  private Pool delegatePool;
  
  /**
   * The debugging log.  This may be null.
   */
  LogOutputStream log;
  

  /**
   * Create a new RDBMS realm object.
   */
  public RDBMSRealm()
  {
    super("RDBMS Realm");
    delegatePool = createPool(DEFAULT_POOL_SIZE);
  }


  /**
   * Create a pool of delegates.  The objects returned by the pool's
   * factory method <i>must</i> extend the RDBMSDelegate class, or the
   * server will not start.
   *
   * @param size the number of pool instances to maintain
   */
  protected Pool createPool(int size)
  {
    return new Pool(new RDBMSDelegate.DFactory(this), size);
  }

  
  /**
   * Obtain a delegate from the pool.  If the pool is empty, this may
   * cause a new delegate to be created and handed back to us.  If a
   * fatal error occurs when you are using a delegate, do not return
   * it to the pool.  You can do this by setting your reference to the
   * delegate to null and calling returnDelegate() as usual; it will
   * do the right thing.
   *
   * @see #returnDelegate
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  protected RDBMSDelegate getDelegate()
  {
    try
    {
      return (RDBMSDelegate) delegatePool.getInstance();
    }
    catch (InvocationTargetException e)
    {
      throw new RDBMSException("could not get delegate", e);
    }
  }


  /**
   * Return a delegate to the pool.  If the delegate was set to null
   * because of errors, nothing is done.
   *
   * @see #getDelegate
   */
  protected void returnDelegate(RDBMSDelegate delegate)
  {
    if (delegate != null)
    {
      delegatePool.returnInstance(delegate);
    }
  }

  
  /**
   * Return the user with the given name, or null if the user does not
   * exist in the database.
   *
   * @param name the name to obtain
   * @return the user, or null if none
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public User getUser(String name)
  {
    // Most of the other methods in this class are patterned after
    // this one.  The delegate does all of the actual work.  These
    // methods simply obtain a delegate from the pool, call through to
    // it, and handle any errors that arise.

    // Obtain a delegate to talk to.
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getUser(name);
    }
    catch (SQLException e)
    {
      // If the delegate throws an exception, the connection to the
      // database may be in an unsafe state.  Make sure we don't use
      // this delegate again.

      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      // This will do nothing if the reference to the delegate has
      // been set to null because of an error, so the delegate will
      // not be returned to the pool.
      
      returnDelegate(delegate);
    }
  }


  /**
   * Return the principal with the given name, or null if the
   * principal does not exist in the database.
   *
   * @param name the name to obtain
   * @return the principal, or null if none
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  protected Principal getPrincipal(String name)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getPrincipal(name);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }

  
  protected Hashtable getGroupMembersInternal(String name)
  {
    // It's easiest to just call getGroup and use RDBMSGroup's
    // getMembers method to return the membership of this group.
    
    RDBMSGroup grp = (RDBMSGroup) getGroup(name);

    if (grp == null)
    {
      return null;
    } else {
      return grp.getMembers();
    }
  }

  
  /**
   * Return the group with the given name, or null if the group does
   * not exist in the database.
   *
   * @param name the name to obtain
   * @return the group, or null if none
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Group getGroup(String name)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getGroup(name);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Return the ACL with the given name, or null if the ACL does
   * not exist in the database.
   *
   * @param name the name to obtain
   * @return the ACL, or null if none
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Acl getAcl(String name)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getAcl(name);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Return an enumeration of all users in the database.  Each element
   * of the Enumeration is a User object.
   *
   * @return all users
   * @see weblogic.security.acl.User
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Enumeration getUsers()
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getUsers();
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Add a member to a group.
   *
   * @param group the group to add to
   * @param principal the principal to add
   * @return true
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  boolean addGroupMember(RDBMSGroup group, Principal member)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.addGroupMember(group, member);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Remove a member from a group.
   *
   * @param group the group to remove from
   * @param principal the principal to remove
   * @return true
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  boolean removeGroupMember(RDBMSGroup group, Principal member)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.removeGroupMember(group, member);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Return an enumeration of all groups in the database.  Each
   * element of the Enumeration is a Group object.  Note that in this
   * realm, empty groups cannot currently exist.
   *
   * @return all groups
   * @see java.security.acl.Group
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Enumeration getGroups()
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getGroups();
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Return an enumeration of all ACLs in the database.  Each element
   * of the Enumeration is an Acl object.  Note that in this realm,
   * empty ACLs cannot currently exist.
   *
   * @return all ACLs
   * @see java.security.acl.Acl
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Enumeration getAcls()
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getAcls();
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Return an enumeration of all permissions in the database.  Each
   * element of the Enumeration is a Permission object.
   *
   * @return all permissions
   * @see java.security.acl.Permission
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Enumeration getPermissions()
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getPermissions();
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Authenticate the given user.  If the authentication step passes,
   * a User object is returned for that user, otherwise null is
   * returned.
   *
   * @return the authenticated user, or null
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  protected User authUserPassword(String name, String passwd)
  {
    RDBMSUser user = (RDBMSUser) getUser(name);

    if (user != null && user.authenticate(passwd) == false)
    {
      user = null;
    }
    
    return user;
  }
  

  /**
   * Get a Permission that matches the specified name.  If no such
   * permission exists in the database, null is returned.
   *
   * @param name the name of the permission
   * @return permission object, or nullp
   * @exception RDBMSException an error occurred in communicating with
   * the database
   */
  public Permission getPermission(String name)
  {
    RDBMSDelegate delegate = getDelegate();

    try
    {
      return delegate.getPermission(name);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Create a new User.
   *
   * @param name the name of the new user
   * @param credential the credential for the user (must be a plaintext password)
   * @param constraints null, for this realm
   * @return the new User
   * @exception SecurityException invalid credential or constraint
   */
  public User newUser(String name, Object credential, Object constraints)
    throws SecurityException
  {
    RDBMSDelegate delegate = getDelegate();

    if (constraints != null)
    {
      throw new SecurityException("newUser does not support constraints");
    }

    if (credential == null || !(credential instanceof String))
    {
      throw new SecurityException("invalid credential");
    }
    
    try
    {
      return delegate.newUser(name, (String) credential);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Delete a user.  This removes the user from the users table, and
   * also from the group membership and ACL entry tables. <p>
   *
   * <b>Warning</b>: due to the schema layout, if you delete a user
   * that is the only member of one or more groups or ACLs, those
   * groups or ACLs will also disappear!  This may cause unexpected
   * exceptions at runtime.
   *
   * @param user the user to delete
   * @exception SecurityException invalid user
   */
  public void deleteUser(User user)
    throws SecurityException
  {
    RDBMSDelegate delegate = getDelegate();

    if (!(user instanceof RDBMSUser))
    {
      throw new SecurityException("invalid user type: " + user.getClass().getName());
    }
    
    try
    {
      delegate.deleteUser(user);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Delete a group.  This deletes the group from both the group
   * membership table and the ACL entry table. <p>
   *
   * <b>Warning</b>: due to the schema layout, if you delete a group
   * that is the only member of one or more ACLs, those ACLs will also
   * disappear!  This may cause unexpected exceptions at runtime.
   *
   * @param group the group to delete
   * @exception SecurityException invalid group
   */
  public void deleteGroup(Group group)
    throws SecurityException
  {
    RDBMSDelegate delegate = getDelegate();

    if (!(group instanceof RDBMSGroup))
    {
      throw new SecurityException("invalid group type: " +
				  group.getClass().getName());
    }
    
    try
    {
      delegate.deleteGroup(group);
    }
    catch (SQLException e)
    {
      delegate.close();
      delegate = null;

      throw new RDBMSException("caught SQL exception", e);
    }
    finally
    {
      returnDelegate(delegate);
    }
  }


  /**
   * Enable or disable debug logging.
   */
  public void setDebug(boolean enable)
  {
    if (enable && log == null)
    {
      log = new LogOutputStream("RDBMSRealm");
    }
    if (!enable)
    {
      log = null;
    }
  }


  /**
   * Obtain the debug log, if it is enabled.
   */
  public LogOutputStream getDebugLog()
  {
    return log;
  }


  /**
   * Factory method for creating new user objects.
   */
  User createUser(String name, String passwd)
  {
    return new RDBMSUser(name, passwd, this);
  }


  /**
   * Factory method for creating new group objects.
   */
  RDBMSGroup createGroup(String name, Hashtable members)
  {
    return new RDBMSGroup(name, this, members);
  }
}
