99 Developer's Guide to Groups

This page originally from content posted on the uportal website.

Introduction

A group is a collection of individual things that together form a unit. Groups have a formal mathematical definition, but to most of us, a group is simply a list of objects that share some attribute, category or role. The Groups service provides an API for maintaining groups of portal entities like users (IPersons) and channels (ChannelDefinitions). The API does not address the entities themselves but only their group memberships. The groups API is used by portal frameworks like authorization, layout management and channel management.

This document describes the groups model and API, which depend on the related idea of a portal entity. It is directed primarily at developers who are working with groups, for example, implementing a custom group store. For information about how to configure the composite groups system, see The Composite Group Service Guide.

Portal Entities

IBasicEntity

An entity is a persistent object whose key and type uniquely identify it. The most obvious portal entity is the user or IPerson, with a type of org.jasig.portal.security.IPerson. The key of an IPerson is usually something like "kweiner" or "ab123" and is typically the user's handle in a central directory. An IBasicEntity must be able to return an EntityIdentifier which, in turn, answers a key and a type.

public interface IBasicEntity {
    public EntityIdentifier getEntityIdentifier();
}

Entity Types

Besides implementing IBasicEntity, a portal entity must have its type added to org.jasig.portal.EntityTypes. This is accomplished by inserting a row into the UP_ENTITY_TYPE table in the portal database, a reference table that maps the class name of an IBasicEntity to an integer. The EntityTypes class has a convenience method to do this:

// Add a new EntityType for Thing:
Class newType = Class.forName("org.jasig.portal.Thing");
String description = "An all-purpose object";
EntityTypes.addEntityType(newType, description);

The concurrency and group services operate on IBasicEntities, so once an IBasicEntity has been added to EntityTypes, it can be cached, locked and grouped. {For more information on caching and locking portal entities, see uPortal Concurrency Services.) The org.jasig.portal.ChannelDefinition and org.jasig.portal.security.IPerson types come pre-loaded in EntityTypes.

The Groups Model

The groups framework does not operate on IBasicEntities like IPersons or ChannelDefinitions, but manipulates stub objects whose keys and types point to underlying entities. The stubs are implementations of org.jasig.portal.groups.IEntity, and their only concern is their group memberships. The groups an IEntity belongs to are implementations of org.jasig.portal.groups.IEntityGroup. Both IEntity and IEntityGroup are also IBasicEntities, and as a result, they can be cached and locked.

IEntityGroups are composites; they can contain other IEntityGroups as well as IEntities. This structure is represented by the following interfaces:

IBasicEntity
    IGroupMember extends IBasicEntity
        IEntity extends IGroupMember
        IEntityGroup extends IGroupMember    

IEntityGroups are homogeneous; an IEntityGroup's IEntities must have underlying entities that are, implement or extend a single type. An IEntityGroup that contains IEntities of type of IPerson can contain memberships for IPersons, subtypes of IPerson, and other IPerson groups. An IEntityGroup that contains entities of type Object can contain memberships for entities of any type, as long as their keys do not collide.

Groups form acyclic graphs; a group can belong to 0 or more parent groups, can contain 0 or more child groups, and cannot contain a circular reference. An actual group service implementation, like the Person Attributes Group Service, may impose further constraints.

Using the Groups Service

Clients of the groups service use its façade, org.jasig.portal.services.GroupService to obtain an IGroupMember, which acts as a starting point in the groups system, much like a javax.naming.InitialContext obtained from a jndi service acts as an entry point into a directory. Once you have a reference to a new or pre-existing IGroupMember, you make subsequent requests to the group member itself, to navigate the system, retrieve other group members, and update groups.

The groups service facade lets you get an IGroupMember in 1 of 3 ways: by finding it with a key, by searching for it by name or by creating a new instance. Each of these techniques has a different meaning for IEntities and IEntityGroups.

Group and Entity Keys

The groups service is itself a group, a group of group services. Each individual service is a component, and the service as a whole is a composite. Most of the time – except when you have to configure a composite service or write a component – the composite service and its facade shield you from this knowledge. However you do need to be aware of component services when you use a group key. The key is a compound key composed of a component service name concatenated to the key of the group in the component service, separated by a separator character, typically a period. For example, the key of group "chem101" in the component service "ldap" would be "ldap.chem101". The key of group "chem101" in the "local" service would be "local.chem101".

An entity key is not compound. It has a single node containing the key that all component group services must use to identify the underlying entity. The assumption here is that in the portal, an entity key like a userid will transcend an individual source, service or application. The userid will be understood by ldap, ERP systems, the portal database, etc. In many, perhaps most installations, this will be the case. But suppose for a minute that it isn't. Assume that a person, say Mick Jagger, is known in ldap as "mjagger", in People Soft as "mj1" and in a third system as "J.J.Flash". In this portal, the deployer would have to select one system to be the base source of person entities (perhaps ldap) and provide translations for the other sources. If ldap were chosen, then a request to find groups that contain "mjagger" would have to be understood by the People Soft component as meaning "find groups that contain "mj1", and other systems would have to perform their own translations. Regardless of whether it is necessary to do these translations, a groups client should see an entity as having a single, untranslated key.

In fact, an IEntity has 2 keys, one for its underlying entity and one for itself as an IBasicEntity, which allows it to be cached with other IEntities. This duality is reflected in the IGroupMember interface, which has getters for the basic EntityIdentifier (inherited from IBasicEntity) and the underlying EntityIdentifier:

public EntityIdentifier getUnderlyingEntityIdentifier();
public EntityIdentifier getEntityIdentifier();

In the case of an IEntityGroup, the underlying EntityIdentifier will be the same as the basic EntityIdentifier. In the case of an IEntity, it will be the EntityIdentifier for the underlying entity, the IPerson, ChannelDefinition, etc., as shown below:

IPerson person = new PersonImpl();
person.setID(1);
person.setAttribute(IPerson.USERNAME,"guest");
IGroupMember gm = GroupSevice.getGroupMember(person.getEntityIdentifier());

EntityIdentifier basicEI = gm.getEntityIdentifier();
EntityIdentifier underlyingEI = gm.getUnderlyingEntityIdentifier();

// Key and type used by clients to get the IEntity:
System.out.println(underlyingEI.getKey());  // guest
System.out.println(underlyingEI.getType()); // interface org.jasig.portal.security.IPerson

// Key and type used by groups system to cache the IEntity:
System.out.println(basicEI.getKey());       // 5.guest
System.out.println(basicEI.getType());      // interface org.jasig.portal.Groups.IEntity

For more information on keys in the group system, see the section on group and entity keys in The Composite Group Service Guide.

Finding a Group Member by Key

A key uniquely identifies a group member, therefore an attempt to find one by key will return at most 1 instance. The service attempts to find an IEntityGroup by searching for an existing group. If it doesn't find the group, it returns null:

// Find a group by key:
String key = "local.123";
IEntityGroup myGroup = GroupSevice.findGroup(key); // could be null

On the other hand, when you ask the group service to get an IEntity corresponding to a given key and type, the service is not obligated to verify that the underlying entity actually exists in some external system. The behavior of the local service, the default source of IEntities is to return an IEntity for any key so long as the type exists in EntityTypes. A non-null result does not mean that the underlying entity actually exists or that it has memberships.

// Get an entity by key (should never be null):
String key = "dan";
Class personType = Class.forName("org.jasig.portal.security.IPerson");
IEntity myEntity = GroupService.getEntity(key, personType);

You can override this behavior by asking for an IEntity from a non-default source by passing in the name of the component group service, and reimplementing the entity factory, IEntityStore.

// Get an entity by key (could be null):
String service = "myService";
String key = "dan";
Class personType = Class.forName("org.jasig.portal.security.IPerson");
IEntity myEntity = GroupService.getEntity(key, personType, service);

The service will use the appropriate method if you call one of the getGroupMember() methods:

// Get a group member by key and type:
String key = "100";
Class channelDefType = Class.forName("org.jasig.portal.ChannelDefinition");
IGroupMember chanGroupMember = GroupService.getGroupMember(key, channelDefType);

or:

// Get a group member by EntityIdentifier:
String key = "local.101";
Class groupType = Class.forName("org.jasig.portal.groups.IEntityGroup");
EntityIdentifier eid = new EntityIdentifier(key, groupType);
IGroupMember chanGroupMember = GroupService.getGroupMember(eid);

There are 2 other kinds of groups that can be found by key, albeit indirectly, via entries in portal.properties. A distinguished group has an entry in portal.properties that associates a name with a group key. A root group is a group that you can optionally designate as the group from which all groups of the same entity type descend. The entry in portal.properties for a root group associates a type name with a group key. This designation is informal only and is not enforced by the group system. It exists for the convenience of groups clients like the Groups Manager channel that display the group system as a forest in which all groups of the same type descend from a single tree.

// Find a distinguished group:
String distinguishedName = "administrators";
IEntityGroup administrators = GroupService.findDistinguishedGroup(distinguishedName);

// Find a root group:
Class thingType = Class.forName("org.jasig.portal.Thing");
IEntityGroup rootOfAllThings = GroupService.findRootGroup(thingType);

Searching for Group Members by Name

There are times when you do not know the key of the group member, but instead want to search for it by name. A search can turn up 0 or more instances, and the 4 search methods return an EntityIdentifier[], each element of which can be turned into an IGroupMember via GroupService.getGroupMember():

searchForEntities(String query, int method, Class type)
searchForEntities(String query, int method, Class type, IEntityGroup ancestor)
searchForGroups(String query, int method, Class leaftype)
searchForGroups(String query, int method, Class leaftype, IEntityGroup ancestor)

Two of the search methods take 3 arguments, a search String, (the name of the group member), a search method (see org.jasig.portal.groups.IGroupConstants for a list of the search methods), and a Class (the entity type). The other 2 methods take an additional argument, ancestor group, which confines the search to (recursive) members of the group.

Each component group service is obligated to implement the 4 search methods. In a search for groups, a component service examines its group store and returns results that match the query. The results are EntityIdentifiers for groups that already exist in the groups system. A search for entities, on the other hand, may be conducted to locate an entity that is not yet represented in the group system, for example, a new employee. Here, the search is conducted on one or more stores that constitute entity sources for the component service.

// search for IPersons whose names start with "Khar"
String query = "Khar";
int method = IGroupConstants.STARTS_WITH;
Class type = Class.forName("org.jasig.portal.security.IPerson");
EntityIdentifier[] ents = GroupService.searchForEntities(query,method,type);

Creating a New Group Member

There are 2 methods for creating a new group. In one, you designate the group type and the component service name. In the other, you designate only the type and the default service creates the group. (The default service is designated in the composite group service configuration, compositeGroupServices.xml.)

IEntityGroup newGroup(Class type)
IEntityGroup newGroup(Class type, String serviceName)

Once you have created a new group, you can set its name, add members, make it a member of other groups, etc., but until you update the group, it is not saved in the store.

// Create a new IPerson group in the default service
Class type = Class.forName("org.jasig.portal.security.IPerson");
IEntityGroup newGroup = GroupService.newGroup(type);
newGroup.setName("Test Group");
...
newGroup.update();

A new IEntity is created with the getEntity() and getGroupMember() methods. New instances of IEntity are routinely created and destroyed, but they are not saved persistently in the groups system. An IEntity represents and points to an underlying entity that may exist in some external source. The only way to create the underlying entity is to create it in the external source.
Working With Group Members
Once you get a group member from the service, you can use it to retrieve related group members:

IGroupMember student = GroupService.getEntity("student");

// Find groups that the entity belongs to:
Iterator studentGroups = student.getContainingGroups();
...

// (Recursively) find groups the entity belongs to:
Iterator allStudentGroups = student.getAllContainingGroups();
...

// Find if an entity is a member of a group:
IGroupMember gradStudents = GroupService.findGroup("local.8");
if ( gradStudents != null && student.isMemberOf(gradStudents) )
{
    ...
}

IEntities are not updatable. An IEntityGroup may or may not be, depending on whether its component group service implements update methods and is declared to be updatable. (For information on configuring component group services, see The Composite Group Service Guide.) Changes to an IEntityGroup are only committed to the store when the group is explicitly updated:

IEntityGroup faculty = GroupService.findGroup("local.2");
if ( faculty != null && faculty.hasMembers() )
{
    // Find members of a group:
    for (Iterator itr = faculty.getMembers(); itr.hasNext();)
    {
        IGroupMember facultyMember = (IGroupMember) itr.next();
        faculty.removeMember(facultyMember);
    }
}
faculty.setDescription("Has no members");
faculty.update();

The Group Service also provides a lockable group, a group whose key has been exclusively locked by the Entity Locking Service (see uPortal Concurrency Services for locking service details.) Getting a lockable group doesn't literally guarantee exclusive write access, it guarantees that no other process can get a lockable group for the same key, so as long as all group clients cooperate, lockable groups work.

public static ILockableEntityGroup findLockableGroup(String key, String lockOwner)

If the group is already locked, the group service throws a GroupsException, so you must catch the Exception and decide whether to abandon the attempt or perhaps wait and try again. Lock management for the group is handled by the component service, including checking that the lock is still valid and releasing it after update.

String studentGroupKey = "local.1";
String owner = "dan";
ILockableEntityGroup leg = null;
try
{
    leg = GroupService.findLockableGroup(studentGroupKey, owner);
}
catch (GroupsException gre)
{
    // group is already locked.
}

if ( leg == null )
{
    // group could not be found.
}
else
{
    leg.setDescription("Edited student group");
    ...
    leg.update();
}

The complete IGroupMember interface is listed below.

The IGroupMember Interface

IGroupMember defines the common (component) behavior for both its leaf (IEntity) and composite (IEntityGroup) sub-types. An IGroupMember can answer both its parents and its children but has no api for adding or removing them. These methods, along with methods to update the persistent store, are defined on the group type because you add a member to a group, not vice versa.

All methods that maintain the groups structure or return IGroupMembers throw GroupsExceptions. These Exceptions are thrown for two reasons, an attempt to violate the groups structure, for example by trying to add a group with a duplicate name, or some error accessing the persistent store. In the latter case, the GroupsException wraps an Exception specific to the store, like a SQLException or a NamingException.

public interface IGroupMember extends IBasicEntity {
  public boolean equals(Object o);
  public Class getEntityType();
  public String getKey();
  public Class getLeafType();
  public Class getType();
  public EntityIdentifier getUnderlyingEntityIdentifier();
  public int hashCode();
  public boolean isEntity();
  public boolean isGroup();

  // shallow:
  public boolean contains(IGroupMember gm) throws GroupsException;
  public Iterator getContainingGroups() throws GroupsException;
  public Iterator getEntities() throws GroupsException;
  public IEntityGroup getMemberGroupNamed(String name) throws GroupsException;
  public Iterator getMembers() throws GroupsException;
  public boolean isMemberOf(IGroupMember gm) throws GroupsException;
  public boolean hasMembers() throws GroupsException;

  // deep:
  public boolean deepContains(IGroupMember gm) throws GroupsException;
  public Iterator getAllContainingGroups() throws GroupsException;
  public Iterator getAllEntities() throws GroupsException;
  public Iterator getAllMembers() throws GroupsException;
  public boolean isDeepMemberOf(IGroupMember gm) throws GroupsException;
}

IEntity is the leaf sub-type of IGroupMember. It inherits component and entity behavior from IGroupMember. At present this is just a marker interface.

public interface IEntity extends IGroupMember
{

  // marker interface

}
public interface IEntityGroup extends IGroupMember
{
  // getters and setters:
  public String getCreatorID();
  public String getDescription();
  public String getLocalKey();
  public String getName();
  public Name getServiceName();
  public void setCreatorID(String userID);
  public void setDescription(String name);
  public void setName(String name) throws GroupsException;
  public void setLocalGroupService(IIndividualGroupService groupService)
    throws GroupsException;

  // composite methods:
  public void addMember(IGroupMember gm) throws GroupsException;
  public void delete() throws GroupsException;
  public boolean isEditable() throws GroupsException;
  public void removeMember(IGroupMember gm);
  public void update() throws GroupsException;
  public void updateMembers() throws GroupsException;
}

ILockableEntityGroup extends IEntityGroup and defines a few methods that support exclusive updates:

public interface ILockableEntityGroup extends IEntityGroup {
  public IEntityLock getLock();
  public void setLock(IEntityLock lock);
  public void updateAndRenewLock() throws GroupsException;
  public void updateMembersAndRenewLock() throws GroupsException;
}