Developers Guide to Groups
Contents
Introduction
Portal Entities
The Groups Model
Using the Groups Service
Group and Entity Keys
Finding a Group Member
by Key
Searching for
Group Members by Name
Creating a New Group
Member
Working With Group Members
The IGroupMember Interface
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;
}
last revised: 03/31/2004
|