package org.jboss.seam.security.management;

import static org.jboss.seam.ScopeType.APPLICATION;
import static org.jboss.seam.annotations.Install.BUILT_IN;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;

import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.annotations.security.management.PasswordSalt;
import org.jboss.seam.annotations.security.management.RoleConditional;
import org.jboss.seam.annotations.security.management.RoleGroups;
import org.jboss.seam.annotations.security.management.RoleName;
import org.jboss.seam.annotations.security.management.UserEnabled;
import org.jboss.seam.annotations.security.management.UserFirstName;
import org.jboss.seam.annotations.security.management.UserLastName;
import org.jboss.seam.annotations.security.management.UserPassword;
import org.jboss.seam.annotations.security.management.UserPrincipal;
import org.jboss.seam.annotations.security.management.UserRoles;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.Expressions;
import org.jboss.seam.core.Expressions.ValueExpression;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.security.Identity;
import org.jboss.seam.security.Role;
import org.jboss.seam.security.SimplePrincipal;
import org.jboss.seam.security.crypto.BinTools;
import org.jboss.seam.util.AnnotatedBeanProperty;
import org.jboss.seam.util.TypedBeanProperty;

/**
 * The default identity store implementation, uses JPA as its persistence mechanism.
 * 
 * @author Shane Bryzak
 */
@Name("org.jboss.seam.security.identityStore")
@Install(precedence = BUILT_IN, value=false) 
@Scope(APPLICATION)
@BypassInterceptors
public class JpaIdentityStore implements IdentityStore, Serializable
{  

   private static final long serialVersionUID = -3627993296654916436L;

   public static final String AUTHENTICATED_USER = "org.jboss.seam.security.management.authenticatedUser";
   
   public static final String EVENT_USER_CREATED = "org.jboss.seam.security.management.userCreated";
   public static final String EVENT_PRE_PERSIST_USER = "org.jboss.seam.security.management.prePersistUser";
   public static final String EVENT_USER_AUTHENTICATED = "org.jboss.seam.security.management.userAuthenticated";
   
   public static final String EVENT_PRE_PERSIST_USER_ROLE = "org.jboss.seam.security.management.prePersistUserRole";
   
   private static final LogProvider log = Logging.getLogProvider(JpaIdentityStore.class);    
   
   protected FeatureSet featureSet;
   
   private ValueExpression<EntityManager> entityManager;  
   
   private Class userClass;
   private Class roleClass;   
   private Class xrefClass;
   private TypedBeanProperty xrefUserProperty;
   private TypedBeanProperty xrefRoleProperty;
   
   private AnnotatedBeanProperty<UserPrincipal> userPrincipalProperty;
   private AnnotatedBeanProperty<UserPassword> userPasswordProperty;
   private AnnotatedBeanProperty<PasswordSalt> passwordSaltProperty;
   private AnnotatedBeanProperty<UserRoles> userRolesProperty;
   private AnnotatedBeanProperty<UserEnabled> userEnabledProperty;
   private AnnotatedBeanProperty<UserFirstName> userFirstNameProperty;
   private AnnotatedBeanProperty<UserLastName> userLastNameProperty;   
   private AnnotatedBeanProperty<RoleName> roleNameProperty;
   private AnnotatedBeanProperty<RoleGroups> roleGroupsProperty;
   private AnnotatedBeanProperty<RoleConditional> roleConditionalProperty;
   
   public Set<Feature> getFeatures()
   {
      return featureSet.getFeatures();
   }
   
   public void setFeatures(Set<Feature> features)
   {
      featureSet = new FeatureSet(features);
   }
   
   public boolean supportsFeature(Feature feature)
   {
      return featureSet.supports(feature);
   }
   
   @Create
   public void init()
   {                  
      if (featureSet == null)
      {
         featureSet = new FeatureSet();
         featureSet.enableAll();
      }      
      
      if (entityManager == null)
      {
         entityManager = Expressions.instance().createValueExpression("#{entityManager}", EntityManager.class);
      }      
      
      if (userClass == null)
      {
         log.error("Error in JpaIdentityStore configuration - userClass must be configured.");
         return;
      }    
      
      initProperties();   
   }
   
   private void initProperties()
   {
      userPrincipalProperty = new AnnotatedBeanProperty(userClass, UserPrincipal.class);
      userPasswordProperty = new AnnotatedBeanProperty(userClass, UserPassword.class);
      passwordSaltProperty = new AnnotatedBeanProperty(userClass, PasswordSalt.class);
      userRolesProperty = new AnnotatedBeanProperty(userClass, UserRoles.class);
      userEnabledProperty = new AnnotatedBeanProperty(userClass, UserEnabled.class);
      userFirstNameProperty = new AnnotatedBeanProperty(userClass, UserFirstName.class);
      userLastNameProperty = new AnnotatedBeanProperty(userClass, UserLastName.class);
             
      if (!userPrincipalProperty.isSet()) 
      {
         throw new IdentityManagementException("Invalid userClass " + userClass.getName() + 
               " - required annotation @UserPrincipal not found on any Field or Method.");
      }
      
      if (!userRolesProperty.isSet())
      {
         throw new IdentityManagementException("Invalid userClass " + userClass.getName() + 
         " - required annotation @UserRoles not found on any Field or Method.");         
      }      
      
      if (roleClass != null)
      {         
         roleNameProperty = new AnnotatedBeanProperty(roleClass, RoleName.class);
         roleGroupsProperty = new AnnotatedBeanProperty(roleClass, RoleGroups.class);
         roleConditionalProperty = new AnnotatedBeanProperty(roleClass, RoleConditional.class);
         
         if (!roleNameProperty.isSet())
         {
            throw new IdentityManagementException("Invalid roleClass " + roleClass.getName() + 
            " - required annotation @RoleName not found on any Field or Method.");         
         }         
                 
         Type type = userRolesProperty.getPropertyType();
         if (type instanceof ParameterizedType && 
               Collection.class.isAssignableFrom((Class) ((ParameterizedType) type).getRawType()))
         {
            Type genType = Object.class;

            for (Type t : ((ParameterizedType) type).getActualTypeArguments())
            {
               genType = t;
               break;
            }                 
         
            // If the @UserRoles property isn't a collection of <roleClass>, then assume the relationship
            // is going through a cross-reference table            
            if (!genType.equals(roleClass))
            {
               xrefClass = (Class) genType;
               xrefUserProperty = new TypedBeanProperty(xrefClass, userClass);
               xrefRoleProperty = new TypedBeanProperty(xrefClass, roleClass);
               
               if (!xrefUserProperty.isSet())
               {
                  throw new IdentityManagementException("Error configuring JpaIdentityStore - it looks like " +
                        "you're using a cross-reference table, however the user property cannot be determined.");
               }
               
               if (!xrefRoleProperty.isSet())
               {
                  throw new IdentityManagementException("Error configuring JpaIdentityStore - it looks like " +
                  "you're using a cross-reference table, however the role property cannot be determined.");                  
               }
            }
         }
      }
   }
   
   public boolean createUser(String username, String password, String firstname, String lastname)
   {
      try
      {
         if (userClass == null)
         {
            throw new IdentityManagementException("Could not create account, userClass not set");
         }
         
         if (userExists(username))
         {
            throw new IdentityManagementException("Could not create account, already exists");
         }
         
         Object user = userClass.newInstance();

         userPrincipalProperty.setValue(user, username);

         if (userFirstNameProperty.isSet()) userFirstNameProperty.setValue(user, firstname);         
         if (userLastNameProperty.isSet()) userLastNameProperty.setValue(user, lastname);
         
         if (password == null)
         {
            if (userEnabledProperty.isSet()) userEnabledProperty.setValue(user, false);
         }
         else
         {  
            setUserPassword(user, password);
            if (userEnabledProperty.isSet()) userEnabledProperty.setValue(user, true);
         }
         
         if (Events.exists()) Events.instance().raiseEvent(EVENT_PRE_PERSIST_USER, user);
         
         persistEntity(user);
         
         if (Events.exists()) Events.instance().raiseEvent(EVENT_USER_CREATED, user);
         
         return true;
      }
      catch (Exception ex)
      {
         if (ex instanceof IdentityManagementException)
         {
            throw (IdentityManagementException) ex;
         }
         else
         {
            throw new IdentityManagementException("Could not create account", ex);
         }
      }      
   }
   
   protected void setUserPassword(Object user, String password)
   {
      if (passwordSaltProperty.isSet())
      {
         byte[] salt = generateUserSalt(user);               
         passwordSaltProperty.setValue(user, BinTools.bin2hex(salt));
         userPasswordProperty.setValue(user, generatePasswordHash(password, salt));
      }
      else
      {
         userPasswordProperty.setValue(user, generatePasswordHash(password, getUserAccountSalt(user)));
      }
   }
   
   /**
    * @deprecated Use JpaIdentityStore.generateRandomSalt(Object) instead
    */
   @Deprecated
   protected String getUserAccountSalt(Object user)
   {      
      // By default, we'll use the user's username as the password salt
      return userPrincipalProperty.getValue(user).toString();
   }
   
   /**
    * Generates a 64 bit random salt value
    */
   public byte[] generateUserSalt(Object user)
   {
      return PasswordHash.instance().generateRandomSalt();
   }
   
   public boolean createUser(String username, String password)
   {
      return createUser(username, password, null, null);
   }
   
   public boolean deleteUser(String name)
   {
      Object user = lookupUser(name);
      if (user == null) 
      {
         throw new NoSuchUserException("Could not delete, user '" + name + "' does not exist");
      }
      
      removeEntity(user);
      return true;
   }
   
   public boolean grantRole(String username, String role)
   {
      if (roleClass == null) return false;
      
      Object user = lookupUser(username);
      if (user == null)
      {
         if (userPasswordProperty.isSet())
         {
            // If no userPasswordProperty is set, it means that authentication is being performed
            // by another identity store and this one is just managing roles
            throw new NoSuchUserException("Could not grant role, no such user '" + username + "'");
         }
         else
         {
            // We need to create a new user object
            if (createUser(username, null))
            {
               user = lookupUser(username);
            }
            else
            {
               throw new IdentityManagementException(
                     "Could not grant role - user does not exist and an attempt to create the user failed.");
            }
         }
      }
      
      Object roleToGrant = lookupRole(role);
      if (roleToGrant == null)
      {
         throw new NoSuchRoleException("Could not grant role, role '" + role + "' does not exist");
      }
      
      Collection userRoles = (Collection) userRolesProperty.getValue(user); 
      if (userRoles == null)
      {
         Type propType = userRolesProperty.getPropertyType();
         Class collectionType;
         
         if (propType instanceof Class && Collection.class.isAssignableFrom((Class) propType))
         {
            collectionType = (Class) propType;
         }
         else if (propType instanceof ParameterizedType &&
                  Collection.class.isAssignableFrom((Class) ((ParameterizedType) propType).getRawType()))
         {
            collectionType = (Class) ((ParameterizedType) propType).getRawType();
         }
         else
         {
            throw new IllegalStateException("Could not determine collection type for user roles.");
         }
         
         // This should either be a Set, or a List...
         if (Set.class.isAssignableFrom(collectionType))
         {
            userRoles = new HashSet();
         }
         else if (List.class.isAssignableFrom(collectionType))
         {
            userRoles = new ArrayList();
         }
         
         userRolesProperty.setValue(user, userRoles);
      }
      else if (((Collection) userRolesProperty.getValue(user)).contains(roleToGrant))
      {
         return false;
      }

      if (xrefClass == null)
      {
         // If this is a Many-To-Many relationship, simply add the role 
         ((Collection) userRolesProperty.getValue(user)).add(roleToGrant);
      }
      else
      {
         // Otherwise we need to insert a cross-reference entity instance
         try
         {
            Object xref = xrefClass.newInstance();            
            xrefUserProperty.setValue(xref, user);
            xrefRoleProperty.setValue(xref, roleToGrant);
            
            Events.instance().raiseEvent(EVENT_PRE_PERSIST_USER_ROLE, xref);
            
            ((Collection) userRolesProperty.getValue(user)).add(mergeEntity(xref));
         }
         catch (Exception ex)
         {
            throw new IdentityManagementException("Error creating cross-reference role record.", ex);
         }
      }
      
      return true;
   }   
   
   public boolean revokeRole(String username, String role)
   {
      Object user = lookupUser(username);
      if (user == null)
      {
         throw new NoSuchUserException("Could not revoke role, no such user '" + username + "'");
      }
      
      Object roleToRevoke = lookupRole(role);
      if (roleToRevoke == null)
      {
         throw new NoSuchRoleException("Could not revoke role, role '" + role + "' does not exist");
      }      
             
      boolean success = false;
      
      if (xrefClass == null)
      {
         success = ((Collection) userRolesProperty.getValue(user)).remove(roleToRevoke);
      }
      else
      {
         Collection roles = ((Collection) userRolesProperty.getValue(user));

         for (Object xref : roles)
         {
            if (xrefRoleProperty.getValue(xref).equals(roleToRevoke))
            {
               success = roles.remove(xref);
               break;
            }
         }
      }

      return success;
   }
   
   public boolean addRoleToGroup(String role, String group)
   {
      if (!roleGroupsProperty.isSet()) return false;      
      
      Object targetRole = lookupRole(role);
      if (targetRole == null)
      {
         throw new NoSuchUserException("Could not add role to group, no such role '" + role + "'");
      }
      
      Object targetGroup = lookupRole(group);
      if (targetGroup == null)
      {
         throw new NoSuchRoleException("Could not grant role, group '" + group + "' does not exist");
      }
      
      Collection roleGroups = (Collection) roleGroupsProperty.getValue(targetRole); 
      if (roleGroups == null)
      {
         // This should either be a Set, or a List...
         Class rawType = null;
         if (roleGroupsProperty.getPropertyType() instanceof ParameterizedType)
         {
            rawType = (Class) ((ParameterizedType) roleGroupsProperty.getPropertyType()).getRawType();
         }
         else
         {
            return false;
         }                   
          
         if (Set.class.isAssignableFrom(rawType))
         {
            roleGroups = new HashSet();
         }
         else if (List.class.isAssignableFrom(rawType))
         {
            roleGroups = new ArrayList();
         }
         
         roleGroupsProperty.setValue(targetRole, roleGroups);
      }
      else if (((Collection) roleGroupsProperty.getValue(targetRole)).contains(targetGroup))
      {
         return false;
      }

      ((Collection) roleGroupsProperty.getValue(targetRole)).add(targetGroup);
      
      return true;
   }

   public boolean removeRoleFromGroup(String role, String group)
   {
      if (!roleGroupsProperty.isSet()) return false;
      
      Object roleToRemove = lookupRole(role);
      if (role == null)
      {
         throw new NoSuchUserException("Could not remove role from group, no such role '" + role + "'");
      }
      
      Object targetGroup = lookupRole(group);
      if (targetGroup == null)
      {
         throw new NoSuchRoleException("Could not remove role from group, no such group '" + group + "'");
      }      
       
      boolean success = ((Collection) roleGroupsProperty.getValue(roleToRemove)).remove(targetGroup);
      return success;
   }      
   
   public boolean createRole(String role)
   {
      try
      {
         if (roleClass == null)
         {
            throw new IdentityManagementException("Could not create role, roleClass not set");
         }
         
         if (roleExists(role))
         {
            throw new IdentityManagementException("Could not create role, already exists");
         }
         
         Object instance = roleClass.newInstance();         
         roleNameProperty.setValue(instance, role);         
         persistEntity(instance);
         
         return true;
      }
      catch (Exception ex)
      {
         if (ex instanceof IdentityManagementException)
         {
            throw (IdentityManagementException) ex;
         }
         else
         {
            throw new IdentityManagementException("Could not create role", ex);
         }
      }      
   }
   
   public boolean deleteRole(String role)
   {      
      Object roleToDelete = lookupRole(role);
      if (roleToDelete == null)
      {
         throw new NoSuchRoleException("Could not delete role, role '" + role + "' does not exist");
      }        
      
      if (xrefClass != null)
      {
         lookupEntityManager().createQuery("delete " + xrefClass.getName() + " where role = :role")
         .setParameter("role", roleToDelete)
         .executeUpdate();
      }
      else
      {
         List<String> users = listUserMembers(role);
         for (String user : users)
         {
            revokeRole(user, role);
         }
      }
      
      List<String> roles = listRoleMembers(role);
      for (String r : roles)
      {
         removeRoleFromGroup(r, role);
      }
            
      removeEntity(roleToDelete);
      return true;
   }
   
   public boolean enableUser(String name)
   {
      if (!userEnabledProperty.isSet())
      {
         log.debug("Can not enable user, no @UserEnabled property configured in userClass " + userClass.getName());
         return false;
      }
      
      Object user = lookupUser(name);
      if (user == null)
      {
         throw new NoSuchUserException("Could not enable user, user '" + name + "' does not exist");
      }
      
      // Can't enable an already-enabled user, return false
      if (((Boolean) userEnabledProperty.getValue(user)) == true)
      {
         return false;
      }
      
      userEnabledProperty.setValue(user, true);   
      return true;
   }
   
   public boolean disableUser(String name)
   {
      if (!userEnabledProperty.isSet())
      {
         log.debug("Can not disable user, no @UserEnabled property configured in userClass " + userClass.getName());
         return false;
      }
      
      Object user = lookupUser(name);
      if (user == null)
      {
         throw new NoSuchUserException("Could not disable user, user '" + name + "' does not exist");
      }
      
      // Can't disable an already-disabled user, return false
      if (((Boolean) userEnabledProperty.getValue(user)) == false)
      {
         return false;
      }          
      
      userEnabledProperty.setValue(user, false);     
      return true;
   }
   
   public boolean changePassword(String username, String password)
   {
      Object user = lookupUser(username);
      if (user == null)
      {
         throw new NoSuchUserException("Could not change password, user '" + username + "' does not exist");
      }
      
      setUserPassword(user, password);
      
      return true;
   }
   
   public boolean userExists(String name)
   {
      return lookupUser(name) != null;
   }
   
   public boolean roleExists(String name)
   {
      return lookupRole(name) != null;
   }
   
   public boolean isUserEnabled(String name)
   {
      Object user = lookupUser(name);
      return user != null && (!userEnabledProperty.isSet() || (((Boolean) userEnabledProperty.getValue(user))) == true);
   }
   
   public List<String> getGrantedRoles(String name)
   {
      Object user = lookupUser(name);
      if (user == null)
      {
         throw new NoSuchUserException("No such user '" + name + "'");      
      }

      List<String> roles = new ArrayList<String>();
      
      Collection userRoles = (Collection) userRolesProperty.getValue(user);
      if (userRoles != null)
      {
         for (Object role : userRoles)
         {
            if (xrefClass == null)
            {
               roles.add((String) roleNameProperty.getValue(role));
            }
            else
            {
               Object xref = roleNameProperty.getValue(role);
               Object userRole = xrefRoleProperty.getValue(xref);
               roles.add((String) roleNameProperty.getValue(userRole));
            }
         }
      }
      
      return roles;     
   }
   
   public List<String> getRoleGroups(String name)
   {
      Object role = lookupRole(name);
      if (role == null)
      {
         throw new NoSuchUserException("No such role '" + name + "'");
      }

      List<String> groups = new ArrayList<String>();
      
      if (roleGroupsProperty.isSet())
      {
         Collection roleGroups = (Collection) roleGroupsProperty.getValue(role);
         if (roleGroups != null)
         {
            for (Object group : roleGroups)
            {
               groups.add((String) roleNameProperty.getValue(group));
            }
         }
      }
      
      return groups;      
   }
   
   public List<String> getImpliedRoles(String name)
   {
      Object user = lookupUser(name);
      if (user == null) 
      {
         throw new NoSuchUserException("No such user '" + name + "'"); 
      }

      Set<String> roles = new HashSet<String>();
      Collection userRoles = (Collection) userRolesProperty.getValue(user);
      if (userRoles != null)
      {
         for (Object role : userRoles)
         {
            addRoleAndMemberships((String) roleNameProperty.getValue(role), roles);
         }
      }
      
      return new ArrayList<String>(roles);
   }
   
   private void addRoleAndMemberships(String role, Set<String> roles)
   {
      if (roles.add(role))
      {      
         Object instance = lookupRole(role);
         
         if (roleGroupsProperty.isSet())
         {
            Collection groups = (Collection) roleGroupsProperty.getValue(instance);
            
            if (groups != null)
            {
               for (Object group : groups)
               {
                  addRoleAndMemberships((String) roleNameProperty.getValue(group), roles);
               }
            }
         }
      }
   }
   
   public String generatePasswordHash(String password, byte[] salt)
   {
      if (passwordSaltProperty.isSet())
      {
         try
         {
            return PasswordHash.instance().createPasswordKey(password.toCharArray(), salt, 
                  userPasswordProperty.getAnnotation().iterations());
         }
         catch (GeneralSecurityException ex)
         {
            throw new IdentityManagementException("Exception generating password hash", ex);
         }
      }
      else
      {
         return generatePasswordHash(password, new String(salt));
      }
   }
   
   /**
    * 
    * @deprecated Use JpaIdentityStore.generatePasswordHash(String, byte[]) instead
    */
   @Deprecated
   protected String generatePasswordHash(String password, String salt)
   {    
      String algorithm = userPasswordProperty.getAnnotation().hash();
      
      if (algorithm == null || "".equals(algorithm))
      {
         if (salt == null || "".equals(salt))
         {
            return PasswordHash.instance().generateHash(password);
         }
         else
         {
            return PasswordHash.instance().generateSaltedHash(password, salt);
         }
      }
      else if ("none".equalsIgnoreCase(algorithm))
      {
         return password;
      }      
      else
      {
         if (salt == null || "".equals(salt))
         {
            return PasswordHash.instance().generateHash(password, algorithm);
         }
         else
         {
            return PasswordHash.instance().generateSaltedHash(password, salt, algorithm);
         }
      }
   }
   
   public boolean authenticate(String username, String password)
   {
      Object user = lookupUser(username);          
      if (user == null || (userEnabledProperty.isSet() && ((Boolean) userEnabledProperty.getValue(user) == false)))
      {
         return false;
      }
      
      String passwordHash = null;
      
      if (passwordSaltProperty.isSet())
      {
         String encodedSalt = (String) passwordSaltProperty.getValue(user);
         if (encodedSalt == null)
         {
            throw new IdentityManagementException("A @PasswordSalt property was found on entity " + user + 
                  ", but it contains no value");
         }
         
         passwordHash = generatePasswordHash(password, BinTools.hex2bin(encodedSalt));
      }
      else
      {
         passwordHash = generatePasswordHash(password, getUserAccountSalt(user));   
      }
      
       
      boolean success = passwordHash.equals(userPasswordProperty.getValue(user));
            
      if (success && Events.exists())
      {
         if (Contexts.isEventContextActive())
         {
            Contexts.getEventContext().set(AUTHENTICATED_USER, user);
         }
         
         Events.instance().raiseEvent(EVENT_USER_AUTHENTICATED, user);
      }
      
      return success;
   }
   
   @Observer(Identity.EVENT_POST_AUTHENTICATE)
   public void setUserAccountForSession()
   {
      if (Contexts.isEventContextActive() && Contexts.isSessionContextActive())
      {
         Contexts.getSessionContext().set(AUTHENTICATED_USER, 
               Contexts.getEventContext().get(AUTHENTICATED_USER));
      }
   }
   
   public Object lookupUser(String username)       
   {
      try
      {
         Object user = lookupEntityManager().createQuery(
            "select u from " + userClass.getName() + " u where " + userPrincipalProperty.getName() +
            " = :username")
            .setParameter("username", username)
            .getSingleResult();
         
         return user;
      }
      catch (NoResultException ex)
      {
         return null;        
      }      
   }
   
   public String getUserName(Object user)
   {
      return (String) userPrincipalProperty.getValue(user);
   }
   
   public String getRoleName(Object role)
   {
      return (String) roleNameProperty.getValue(role);
   }
   
   public boolean isRoleConditional(String role)
   {      
      return roleConditionalProperty.isSet() ? (Boolean) roleConditionalProperty.getValue(
            lookupRole(role)) : false;
   }
   
   public Object lookupRole(String role)       
   {
      try
      {
         Object value = lookupEntityManager().createQuery(
            "select r from " + roleClass.getName() + " r where " + roleNameProperty.getName() +
            " = :role")
            .setParameter("role", role)
            .getSingleResult();
         
         return value;
      }
      catch (NoResultException ex)
      {
         return null;        
      }
   }   
   
   public List<String> listUsers()
   {
      return lookupEntityManager().createQuery(
            "select u." + userPrincipalProperty.getName() + " from " + userClass.getName() + " u")
            .getResultList();      
   }
   
   public List<String> listUsers(String filter)
   {
      return lookupEntityManager().createQuery(
            "select u." + userPrincipalProperty.getName() + " from " + userClass.getName() + 
            " u where lower(" + userPrincipalProperty.getName() + ") like :username")
            .setParameter("username", "%" + (filter != null ? filter.toLowerCase() : "") + 
                  "%")
            .getResultList();
   }

   public List<String> listRoles()
   {     
      return lookupEntityManager().createQuery(
            "select r." + roleNameProperty.getName() + " from " + roleClass.getName() + " r").getResultList();
   }
   
   public List<Principal> listMembers(String role)
   {
      List<Principal> members = new ArrayList<Principal>();
      
      for (String user : listUserMembers(role))
      {
         members.add(new SimplePrincipal(user));
      }
      
      for (String roleName : listRoleMembers(role))
      {
         members.add(new Role(roleName));
      }
      
      return members;
   }
   
   private List<String> listUserMembers(String role)
   {      
      Object roleEntity = lookupRole(role);

      if (xrefClass == null)
      {      
         return lookupEntityManager().createQuery("select u." + userPrincipalProperty.getName() + 
               " from " + userClass.getName() + " u where :role member of u." + userRolesProperty.getName())
               .setParameter("role", roleEntity)
               .getResultList();
      }
      else
      {
         List xrefs = lookupEntityManager().createQuery("select x from " + xrefClass.getName() + " x where x." +
               xrefRoleProperty.getName() + " = :role")
               .setParameter("role", roleEntity)
               .getResultList();

         List<String> members = new ArrayList<String>();
         
         for (Object xref : xrefs)
         {
            Object user = xrefUserProperty.getValue(xref);
            members.add(userPrincipalProperty.getValue(user).toString());
         }
         
         return members;
      }
     
   }
   
   private List<String> listRoleMembers(String role)
   {                
      
      if (roleGroupsProperty.isSet())
      {
         Object roleEntity = lookupRole(role);                  
         
         return lookupEntityManager().createQuery("select r." + roleNameProperty.getName() +
               " from " + roleClass.getName() + " r where :role member of r." + roleGroupsProperty.getName())
               .setParameter("role", roleEntity)
               .getResultList();
      }
      
      return new ArrayList<String>();
   }
   
   public List<String> listGrantableRoles()
   {
      StringBuilder roleQuery = new StringBuilder();
      
      roleQuery.append("select r.");
      roleQuery.append(roleNameProperty.getName());
      roleQuery.append(" from ");
      roleQuery.append(roleClass.getName());
      roleQuery.append(" r");
      
      if (roleConditionalProperty.isSet())
      {
         roleQuery.append(" where r.");
         roleQuery.append(roleConditionalProperty.getName());
         roleQuery.append(" = false");
      }
      
      return lookupEntityManager().createQuery(roleQuery.toString()).getResultList();
   }
   
   protected void persistEntity(Object entity)
   {
      lookupEntityManager().persist(entity);
   }
   
   protected Object mergeEntity(Object entity)
   {
      return lookupEntityManager().merge(entity);
   }
   
   protected void removeEntity(Object entity)
   {
      lookupEntityManager().remove(entity);
   }
   
   public Class getUserClass()
   {
      return userClass;
   }
   
   public void setUserClass(Class userClass)
   {
      this.userClass = userClass;
   }   
   
   public Class getRoleClass()
   {
      return roleClass;
   }
   
   public void setRoleClass(Class roleClass)
   {
      this.roleClass = roleClass;
   }
   
   private EntityManager lookupEntityManager()
   {
      return entityManager.getValue();
   }
   
   public ValueExpression getEntityManager()
   {
      return entityManager;
   }
   
   public void setEntityManager(ValueExpression expression)
   {
      this.entityManager = expression;
   }      
}
