package examples.security.rdbmsrealm;


import examples.security.util.RealmProperties;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.Principal;
import java.security.acl.Acl;
import java.security.acl.Group;
import java.security.acl.NotOwnerException;
import java.security.acl.Permission;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import weblogic.security.acl.AclEntryImpl;
import weblogic.security.acl.AclImpl;
import weblogic.security.acl.PermissionImpl;
import weblogic.security.acl.User;
import weblogic.security.acl.UserInfo;
import weblogic.utils.reuse.Factory;


/**
 * The RDBMS delegate class.  An instance of this class communicates
 * with a single database connection.  A pool of instacnes is then
 * maintained by RDBMSRealm to provide high performance.
 *
 * @author Copyright (c) 1998-2000 by BEA Systems, Inc. All Rights Reserved.
 */
public class RDBMSDelegate
{
  // The following are some names that we look for in the property file.

  static final String DRIVER            = "driver";
  static final String DB_URL            = "dbURL";
  static final String DB_USER           = "dbUser";
  static final String DB_PASSWORD       = "dbPassword";

  /**
   * The realm with which this delegate is associated.
   */
  protected RDBMSRealm realm;
  
  /**
   * The main connection to the database.
   */
  protected Connection conn;

  // We use multiple connections for JDBC drivers that can't handle
  // multiple open result sets on a single connection.

  protected Connection conn2;
  protected Connection conn3;
  protected Connection conn4;

  private RealmProperties properties;
  
  // We maintain a number of prepared SQL statements for performing
  // database lookups.
  
  private PreparedStatement getUserStmt;
  private PreparedStatement getGroupMembersStmt;
  private PreparedStatement getPermissionStmt;
  private PreparedStatement getAclEntriesStmt;
  private PreparedStatement getUsersStmt;
  private PreparedStatement getGroupsStmt;
  private PreparedStatement getAclsStmt;
  private PreparedStatement getPermissionsStmt;

  private PreparedStatement newUserStmt;
  private PreparedStatement addGroupMemberStmt;

  private PreparedStatement removeGroupMemberStmt;
  
  private PreparedStatement deleteUserStmt1;
  private PreparedStatement deleteUserStmt2;
  private PreparedStatement deleteUserStmt3;

  private PreparedStatement deleteGroupStmt1;
  private PreparedStatement deleteGroupStmt2;

  /**
   * Determine whether or not we need to create a new SQL statement
   * for recursive calls to getGroup.  Some servers need this, and
   * some don't.
   */
  private boolean getGroupNewStatement;
  
  /**
   * This is the bogus owner associated with all ACLs found in the
   * database.
   */
  protected Principal aclOwner = new User("unperson");


  /**
   * A shorthand convenience function for preparing an SQL statement.
   *
   * @param name the name of the statement to prepare
   */
  protected PreparedStatement prepare(String propKey)
    throws SQLException, RDBMSException
  {
    String sqlStr = properties.get(propKey);
    if (sqlStr == null)
    {
      throw new RDBMSException("realm initialization failed, could not find property '" +
                               propKey + "' in properties file " +
			       RDBMSRealm.RDBMS_PROPS);
    }
    return conn.prepareStatement(sqlStr);
  }


  /**
   * Create a new delegate, associated with the given realm.
   *
   * @exception RDBMSException an error occurred in fetching
   * properties or communicating with the database
   */
  protected RDBMSDelegate(RDBMSRealm realm)
  {
    this.realm = realm;

    if (realm.log != null)
      realm.log.debug("loading realm properties: " + RDBMSRealm.RDBMS_PROPS);

    try
    {
      properties = new RealmProperties(RDBMSRealm.class, RDBMSRealm.RDBMS_PROPS);
    }
    catch (IOException e)
    {
      throw new RDBMSException("could not find properties", e);
    }

    getGroupNewStatement = properties.getBoolean("getGroupNewStatement", true);

    String action = "properties.get";    // used to help diagnose an exception

    try
    {
      String driver = properties.get(DRIVER);
      
      if (realm.log != null)
	realm.log.debug("driver is " + driver);

      action = "driver.newInstance()";
      Class.forName(driver).newInstance();

      action = "properties.get";
      String url = properties.get(DB_URL);
      String user = properties.get(DB_USER);
      String passwd = properties.get(DB_PASSWORD);
      
      if (realm.log != null)
	realm.log.debug("connecting to " + url);

      action = "DriverManager.getConnection";
      conn = DriverManager.getConnection(url, user, passwd);
    }
    catch (Exception e)
    {
      throw new RDBMSException("realm initialization failed, action '" +
			       action + "', ", e);
    }

    // pkey is only used to help diagnose an SQLException; prepare()
    // generates an RDBMSException if the property key is not found.

    String pkey = null;

    try
    {
      if (realm.log != null)
	realm.log.debug("preparing statements from " + RDBMSRealm.RDBMS_PROPS);

      getUserStmt = prepare(pkey = "getUser");
      getAclEntriesStmt = prepare(pkey = "getAclEntries");
      getUsersStmt = prepare(pkey = "getUsers");
      getGroupsStmt = prepare(pkey = "getGroups");
      getAclsStmt = prepare(pkey = "getAcls");
      getPermissionStmt = prepare(pkey = "getPermission");
      getPermissionsStmt = prepare(pkey = "getPermissions");

      if (getGroupNewStatement == false)
      {
	getGroupMembersStmt = prepare(pkey = "getGroupMembers");
      }

      newUserStmt = prepare(pkey = "newUser");
      addGroupMemberStmt = prepare(pkey = "addGroupMember");

      removeGroupMemberStmt = prepare(pkey = "removeGroupMember");
      deleteUserStmt1 = prepare(pkey = "deleteUser1");
      deleteUserStmt2 = prepare(pkey = "deleteUser2");
      deleteUserStmt3 = prepare(pkey = "deleteUser3");

      deleteGroupStmt1 = prepare(pkey = "deleteGroup1");
      deleteGroupStmt2 = prepare(pkey = "deleteGroup2");
    }
    catch (SQLException se)
    {
      String sqlStr = properties.get(pkey);  // this call will succeed, it has already worked in prepare()
      throw new RDBMSException("realm initialization failed, Connection.prepareStatement() failed on statement \"" +
                               sqlStr + "\", ", se);
    }
  }


  /**
   * We use this class to indicate to a caller that a method has
   * reached the end of a ResultSet.  This is <i>not</i> an indication
   * of error; it's just a slightly uncommon (in Java, anyway) way of
   * returning more than one value.
   */
  protected static class Finished
    extends Throwable
  {
    /**
     * The value associated with this object.
     */
    private Object value;


    /**
     * Create a new object, with given value.
     */
    Finished(Object value)
    {
      this.value = value;
    }


    /**
     * Return the value associated with this object.
     */
    Object getValue()
    {
      return value;
    }
  }

  
  /**
   * Get a user from the database, or null if the user doesn't exist.
   */
  public User getUser(String name)
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getUser(\"" + name + "\")");
    
    getUserStmt.setString(1, name);

    ResultSet rs = getUserStmt.executeQuery();

    try
    {
      // If the ResultSet is empty, the user doesn't exist in the
      // database.
      
      return rs.next()
	? realm.createUser(rs.getString(1), rs.getString(2)) : null;
    }
    finally
    {
      // Politely clean up after ourselves.
      
      rs.close();
    }
  }


  /**
   * Get all users from the database.
   */
  public Enumeration getUsers()
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getUsers()");

    ResultSet rs = getUsersStmt.executeQuery();
    Vector users = new Vector(1);

    try
    {
      while (rs.next())
      {
	users.addElement(realm.createUser(rs.getString(1), rs.getString(2)));

      }

      return users.elements();
    }
    finally
    {
      rs.close();
    }
  }

  
  /**
   * Get the named group from the database, or null if it doesn't
   * exist.
   */
  public Group getGroup(String name)
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getGroup(\"" + name + "\")");

    // If getGroupNewStatement is enabled, this may incur more
    // overhead than it needs to, because it creates a new
    // PreparedStatement that is only used once.
    
    PreparedStatement stmt = getGroupNewStatement
      ? prepare("getGroupMembers")
      : getGroupMembersStmt;

    stmt.setString(1, name);

    ResultSet rs = stmt.executeQuery();

    try
    {
      return rs.next() ? getGroupInternal(name, rs) : null;
      
    }
    catch (Finished f)
    {
      // If getGroupInternal threw us a Finished object, we just
      // return the value inside it.

      return (RDBMSGroup) f.getValue();
    }
    finally
    {
      rs.close();

      if (getGroupNewStatement)
      {
	stmt.close();
      }
    }
  }


  /**
   * Get all groups from the database.  Note that in this realm, empty
   * groups cannot currently exist.
   */
  public Enumeration getGroups()
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getGroups()");

    ResultSet rs = getGroupsStmt.executeQuery();
    Vector groups = new Vector(1);

    try
    {
      // We loop forever until getGroupInternal throws a Finished
      // object to let us know that we've reached the end of the
      // ResultSet.

      if (rs.next())
      {
	while (true)
	{
	  groups.addElement(getGroupInternal(null, rs));
	}
      }
    }
    catch (Finished f)
    {
      // Add the last element to the set of groups.
      
      groups.addElement(f.getValue());
    }
    finally
    {
      rs.close();
    }

    return groups.elements();
  }

  
  public User newUser(String name, String passwd)
    throws SQLException, SecurityException
  {
    if (realm.log != null)
      realm.log.debug("newUser(\"" + name + "\", \"" + passwd + "\")");

    if (getUser(name) != null)
    {
      throw new SecurityException("user \"" + name + "\" already exists");
    }
    
    newUserStmt.setString(1, name);
    newUserStmt.setString(2, passwd);

    int rows = newUserStmt.executeUpdate();

    if (rows != 1)
    {
      throw new RDBMSException("insert updated " + rows + " rows (should be 1)");
    }
    
    return realm.createUser(name, passwd);
  }


  public void deleteUser(User user)
    throws SQLException
  {
    String name = user.getName();

    deleteUserStmt1.setString(1, name);
    deleteUserStmt2.setString(1, name);
    deleteUserStmt3.setString(1, name);

    deleteUserStmt1.executeUpdate();
    deleteUserStmt2.executeUpdate();
    deleteUserStmt3.executeUpdate();
  }
  

  public void deleteGroup(Group group)
    throws SQLException
  {
    String name = group.getName();

    deleteGroupStmt1.setString(1, name);
    deleteGroupStmt2.setString(1, name);

    deleteGroupStmt1.executeUpdate();
    deleteGroupStmt2.executeUpdate();
  }
  

  /**
   * This method is called both by getGroup and getGroups.  It looks
   * through the given ResultSet and gathers group members until it
   * either hits a differently-named group or the end of the
   * ResultSet.
   */
  protected Group getGroupInternal(String name, ResultSet rs)
    throws Finished, SQLException
  {
    // All of the other methods in this class with similar names are
    // patterned after this one.

    // We expect the ResultSet that we are reading to cluster all the
    // members of a given group together in contiguous rows.  If this
    // is not the case, this code will fail miserably.

    Hashtable members = new Hashtable();
    boolean more = true;

    // We expect our ResultSet to already point at the first member of
    // a group, hence this being a "do ... while" loop.
    do
    {
      String groupName = rs.getString(1);
      String memberName = rs.getString(2);
	  
      if (name == null)
      {
	name = groupName;
      }
      else if (groupName.equals(name) == false)
      {
	// We've encountered a group with a different name than the
	// one we were interested in, so we're done for the current
	// invocation.

	break;
      }

      Principal p = getPrincipal(memberName);

      if (p == null)
      {
	throw new RDBMSException("group \"" + name + "\" contains nonexistent " +
				 "principal \"" + memberName + "\"");
      }
      
      members.put(memberName, p);
    } while (more = rs.next());

    RDBMSGroup result = realm.createGroup(name, members);
    
    // Sanity-check the new group to ensure that it doesn't contain
    // any groups that contain it.  You can turn this off if your
    // database can't get corrupted in this way.
    
    if (true)
    {
      Enumeration enum = members.elements();
    
      while (enum.hasMoreElements())
      {
	Object obj = enum.nextElement();

	if (obj instanceof Group)
	{
	  Group g = (Group) obj;

	  if (g.isMember(result))
	  {
	    throw new RDBMSException("group membership circularity between \"" +
				     g.getName() + "\" and \"" + name + "\"");
	  }
	}
      }
    }

    if (more == false)
    {
      // We've hit the end of the ResultSet, so let our caller know.
      
      throw new Finished(result);
    }
    
    // We have not hit the end of the ResultSet, so just return
    // normally.
    
    return result;
  }

  
  public boolean addGroupMember(RDBMSGroup group, Principal member)
    throws SQLException
  {
    addGroupMemberStmt.setString(1, group.getName());
    addGroupMemberStmt.setString(2, member.getName());

    int rows = addGroupMemberStmt.executeUpdate();

    if (rows != 1)
    {
      throw new RDBMSException("insert updated " + rows + " rows (should be 1)");
    }

    return true;
  }

  
  public boolean removeGroupMember(RDBMSGroup group, Principal member)
    throws SQLException
  {
    removeGroupMemberStmt.setString(1, group.getName());
    removeGroupMemberStmt.setString(2, member.getName());

    int rows = removeGroupMemberStmt.executeUpdate();

    if (rows != 1)
    {
      throw new RDBMSException("delete updated " + rows + " rows (should be 1)");
    }

    return true;
  }
  

  /**
   * Get an ACL from the database, or null if the ACL doesn't exist.
   */
  public Acl getAcl(String name)
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getAcl(\"" + name + "\")");

    getAclEntriesStmt.setString(1, name);

    ResultSet rs = getAclEntriesStmt.executeQuery();

    try
    {
      return rs.next() ? getAclInternal(name, rs) : null;
    }
    catch (Finished f)
    {
      return (Acl) f.getValue();
    }
    finally
    {
      rs.close();
    }
  }


  /**
   * Get all ACLs from the database.
   */
  public Enumeration getAcls()
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getAcls()");

    ResultSet rs = getAclsStmt.executeQuery();
    Vector acls = new Vector(1);
    
    try
    {
      if (rs.next())
      {
	while (true)
	{
	  acls.addElement(getAclInternal(null, rs));
	}
      }
    }
    catch (Finished f)
    {
      acls.addElement(f.getValue());
    }
    finally
    {
      rs.close();
    }

    return acls.elements();
  }

  
  /**
   * Called by both getAcl and getAcls.
   */
  protected Acl getAclInternal(String name, ResultSet rs)
    throws Finished, SQLException
  {
    // This method follows a similar pattern to getGroupInternal, but
    // has the added twist that it expects rows for a given ACL to be
    // grouped together by principal.

    boolean more = true;
    
    AclImpl result = new AclImpl(aclOwner, null);
    AclEntryImpl entry = null;
    String currentPrincipal = null;

    try
    {
      do
      {
	String aclName = rs.getString(1);
	String principal = rs.getString(2);
	String permission = rs.getString(3);

	if (name == null)
	{
	  name = aclName;
	}
	else if (aclName.equals(name) == false)
	{
	  break;
	}
      
	if (currentPrincipal == null || currentPrincipal.equals(principal) == false)
	{
	  // We're dealing with a new principal, so create a new AclEntry.
	  
	  currentPrincipal = principal;

	  // There's an ordering constraint imposed here by the
	  // AclImpl implementation: we must add all the Permissions
	  // we will need to an AclEntry before adding the AclEntry to
	  // the Acl.

	  if (entry != null)
	  {
	    result.addEntry(aclOwner, entry);
	  }

	  Principal p = getPrincipal(principal);
	  
	  if (p == null)
	  {
	    throw new RDBMSException("acl \"" + name + "\" contains nonexistent " +
				     "principal \"" + principal + "\"");
	  }
	  
	  entry = new AclEntryImpl(p);
	}

	entry.addPermission(new PermissionImpl(permission));
      } while (more = rs.next());

      if (entry != null)
      {
	result.addEntry(aclOwner, entry);
      }
      
      result.setName(aclOwner, name);
    }
    catch (NotOwnerException e)
    {
      throw new RDBMSException("caught unexpected exception", e);
    }

    if (more == false)
    {
      throw new Finished(result);
    }

    return result;
  }


  /**
   * Resolve a name to a User or Group.  If the principal in question
   * doesn't exist in the database, we return null.
   */
  public Principal getPrincipal(String name)
    throws SQLException
  {
    Principal result = getUser(name);

    if (result == null)
    {
      result = getGroup(name);
    }

    return result;
  }

  
  /**
   * Obtain the named permission from the database, or null if none.
   */
  public Permission getPermission(String name)
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getPermission(\"" + name + "\")");

    getPermissionStmt.setString(1, name);

    ResultSet rs = getPermissionStmt.executeQuery();

    try
    {
      return rs.next() ? new PermissionImpl(rs.getString(1)) : null;
    }
    finally
    {
      rs.close();
    }
  }

  
  /**
   * Return an Enumeration of the permissions for this realm.
   */
  public Enumeration getPermissions()
    throws SQLException
  {
    if (realm.log != null)
      realm.log.debug("getPermissions()");

    ResultSet rs = getPermissionsStmt.executeQuery();
    Vector permissions = new Vector(1);

    try
    {
      while (rs.next())
      {
	permissions.addElement(new PermissionImpl(rs.getString(1)));
      }

      return permissions.elements();
    }
    finally
    {
      rs.close();
    }
  }


  /**
   * Clean up after ourselves.
   */
  protected void finalize()
  {
    close();
  }

  
  /**
   * Clean up after ourselves.
   */
  public void close()
  {
    try
    {
      try
      {
	if (conn != null)
	  conn.close();
      }
      catch (SQLException e)
      {
	// ignore
      }
      try
      {
	if (conn2 != null)
	  conn2.close();
      }
      catch (SQLException e)
      {
	// ignore
      }
      try
      {
	if (conn3 != null)
	  conn3.close();
      }
      catch (SQLException e)
      {
	// ignore
      }
      try
      {
	if (conn4 != null)
	  conn4.close();
      }
      catch (SQLException e)
      {
	// ignore
      }
    }
    finally
    {
      realm = null;
      
      conn = null;
      conn2 = null;
      conn3 = null;
      conn4 = null;

      getUserStmt = null;
      getGroupMembersStmt = null;
      getPermissionStmt = null;
      getAclEntriesStmt = null;
      getUsersStmt = null;
      getGroupsStmt = null;
      getAclsStmt = null;
      getPermissionsStmt = null;
    }
  }

  
  /**
   * This is the factory class that creates instances of the
   * RDBMSDelegate class for pooling.
   */
  static class DFactory implements Factory
  {
    /**
     * The realm that owns all delegates created by this factory.
     */
    private RDBMSRealm owner;

    /**
     * Create an instance of the factory, owned by the given realm.
     */
    DFactory(RDBMSRealm owner)
    {
      this.owner = owner;
    }

    /**
     * Create a new delegate.
     */
    public Object newInstance()
      throws InvocationTargetException
    {
      if (owner.log != null)
	owner.log.debug("new instance");

      return new RDBMSDelegate(owner);
    }

    /**
     * Destroy a delegate.
     */
    public void destroyInstance(Object obj)
    {
      if (owner.log != null)
	owner.log.debug("destroy instance");

      ((RDBMSDelegate) obj).close();
    }
  }
}
