Working with groups in LotusScript

This article describes how you can work with groups in LotusScript, using the built-in NotesAdministrationProcess class as well as two custom LotusScript classes.

Share:

Andre Guirard (Andre_Guirard@us.ibm.com), EI Product Developer, IBM

Andre Guirard is a member of the development team for the Notes Client and Domino Designer. A long-time Notes developer, Andre now focuses on developer enablement. He often speaks at Lotusphere and other IBM conferences, and his articles have previously appeared on developerWorks: Lotus (LDD), as well as in The View magazine and elsewhere. In his free time, he is the Labor Pool for his wife's gardening projects.



13 October 2011 (First published 04 January 2005)

Also available in Japanese Portuguese

A group is a type of Notes document found in the Domino Directory. You can manipulate the Group document using the NotesDocument class. The problem is knowing which values in which fields mean what and dealing with the complexities of groups that contain other groups (nested groups) or that need to exceed the size limit of the member list.

In this article, we discuss two ways to work with groups. First, we look at the built-in LotusScript class, NotesAdministrationProcess, available in Lotus Notes/Domino 6.0 and later. This class can add members to groups, rename them, and delete them (along with many other functions not related to groups). There is no function to remove users from a group; however, if you use the administration process (AdminP) to remove users entirely, they will also be removed from groups. For more on the NotesAdministrationProcess class, see the article, "LotusScript: The NotesAdministrationProcess Class in Notes/Domino 6."

We also present two custom LotusScript classes that support a wider set of group operations. The code was written for Lotus Notes/Domino 6.0 and later; using it in release 5 requires some minor changes. The article contains a few sample agents that show how to use these classes for different tasks. The classes are not usable from COM because they use the List datatype internally, which Visual Basic does not support.

The actual code of the classes, all the sample agents, and complete documentation of all the properties and methods listed in this article are available in the sample database, which you can download from the LotusScript Gold Collection at OpenNTF.org. (In the sample database, use the GMan help action or press F1 from any view to see full documentation of the custom classes.) The custom classes are called NotesGroupManager and NotesGroup. To use them, copy the LotusScript GroupManager script library from the sample database into your own applications and include it in your script with the Use statement. The classes contain functions to:

  • Locate all your address books.
  • Search for Group documents in all address books or in a subset you specify.
  • Create and delete groups.
  • Add and remove members.
  • Sort the member list.
  • Deal with groups that contain other groups.
  • Determine whether or not someone is a member of a group or get a list of all the members of a group, including members of nested groups.
  • Manage groups that have more members than will fit in a single Group document by splitting them into multiple documents.

The example database contains some design elements, copied from the public address book template (pubnames.ntf), so that you can use the tool on the data in the sample database and avoid changing groups in your Domino Directory. You can use the code in any application.

This article assumes that you're an experienced LotusScript developer.

Group Methods of NotesAdministrationProcess Class

As mentioned in the introduction, the NotesAdministrationProcess class allows a few simple group operations (as well as many other functions not related to groups). The relevant methods are listed in the following sections.

Each of these methods works by creating a request document in the Notes administration requests database (admin4.nsf). When AdminP runs on the server, it notices the request and carries out whichever operation is requested. Certain requests may need someone else to approve them, and certain requests may be mailed to other Notes domains for processing there.

The advantages of using this class are:

  • It lets you issue a request even in cases where you may not have immediate access to the server that will carry it out.
  • If a user is waiting for your script to complete, this lets you finish faster because the actual work is being done by the server.
  • AdminP does a more thorough job of removing and renaming groups. It not only deletes or changes the group record, but also tracks down references to the old group name and updates them.

The drawbacks are:

  • There are only a few operations that you can do this way.
  • The request is not necessarily performed immediately.
  • It doesn't handle adding users to a group that contains too many members to fit in a single Group document.

Most of this class's methods return the note ID of the new request document in the administration requests database. If the request cannot be created (for instance, because the current user doesn't have authorization), it returns the empty string ("") or causes an error condition, depending upon the reason for the failure.

The idea of returning the note ID is that, if you want, you can monitor that document to see when the request was completed. AdminP creates a response document to show when the task was completed. Depending upon your server setup, AdminP may run on a different server from the one where you post the request, resulting in a lengthy delay while you wait for replication to and from the administration server. You can't do anything using AdminP that you don't have access to do manually. This just automates it.

We will not describe the full syntax of these methods here, but refer you to the Domino Designer help. Here we provide a summary and useful tidbits of information not found in the documentation.

AddGroupMembers method

Given a group name (string) and an array of user names, this method adds these names to the group. This may involve searching multiple directories to find the group. If there is no group with this name, it is created and given the multipurpose type, meaning it may be used for email or access control. Names already in the group are not added.

NOTE: The Domino Designer documentation states that the members argument may be a string containing a single user name, but this does not appear to be working as of Lotus Notes/Domino 6.5.4. Use a one-element array to add a single user name.

Example 1: Auto-Subscribe to mailing list

The following agent (AdminP\Process Subscription Requests) is set to run on new and changed Memo documents in a mail-in database. It checks the subject line to see if it contains the word subscribe, and if so, adds the user to a group:

Option PublicOption Declare ' always use Option Declare

Sub Initialize
	Dim session As New NotesSession
	Dim adminp As NotesAdministrationProcess
	Dim db As NotesDatabase
	Dim coll As NotesDocumentCollection
	Dim docRequest As NotesDocument
	Dim strID As String
	Dim strNewMembersArray( ) As String
	Dim intNewMemberCount As Integer
	
	Set db = session.CurrentDatabase
	Set coll = db.UnprocessedDocuments
	Set docRequest = coll.GetFirstDocument( )
	Do Until docRequest Is Nothing
		If docRequest.Subject(0) = "subscribe" Then
			Redim Preserve strNewMembersArray(0 To intNewMemberCount) 
			  strNewMembersArray(intNewMemberCount) = 
			  docRequest.GetItemValue("From")(0)
			intNewMemberCount = intNewMemberCount + 1
		End If
		session.UpdateProcessedDoc docRequest
		Set docRequest = coll.GetNextDocument(docRequest)
	Loop
	If intNewMemberCount  > 0 Then ' There are some members to add
		Set adminp = session.CreateAdministrationProcess("bobbity")
		strID = adminp.AddGroupMembers("Hazardous Joke 
		  Mailing List", strNewMembersArray)
		If strID = "" Then ' Probable insufficient access
			Msgbox {Unable to create adminp request "Add or Modify 
			  Group"}, 0, {adminp failure}
		End If
	End If
End Sub

DeleteGroup

Given a group name, this method deletes the group from the Domino Directory. In addition, the group name is removed from the Members list of all other groups, from the ACLs of databases, and from document Reader and Author names fields in all databases. Optionally, this method also deletes the Windows group of the same name, assuming your server uses a Windows operating system.

RenameGroup

The RenameGroup method takes two string arguments: the old name and new name of the group. In addition to changing the name in the actual group record, this method also updates database ACL lists, member lists of other groups, Reader/Author names fields, and other places where the group name may be used.


The GroupManager script library

The GroupManager script library contains two classes as well as several constants that are useful as method arguments and property values and in On Error statements. The classes are:

  • NotesGroupManager keeps track of your Domino Directory and manages an in-memory cache of group information. This lets you efficiently perform multiple operations on multiple groups without having to constantly reload the groups into memory. Saving changes may be deferred until all processing is complete. The NotesGroupManager may be used in recursive or nonrecursive mode. In recursive mode, it notices which groups contain other groups and lets you perform recursive operations, such as removing a user from the current group and all subgroups.
  • NotesGroup contains information about a single group.

Most tasks can be done directly through the NotesGroupManager methods by supplying a group name as argument. If you need to use the NotesGroup class, request a NotesGroup object from the NotesGroupManager using the GetGroup or CreateGroup methods. This is useful for some advanced operations (such as changing the group's ownership) and for simpler code and greater efficiency when doing multiple operations on the same group.

Subgroups, breakout subgroups, and recursive mode

The GroupManager script library includes functionality to deal with hierarchies of groups (groups that contain other groups as members), down to six levels of nesting. In this article, we refer to a group that is a member of another group as a subgroup.

When you create a NotesGroupManager object, you have the option to disable recursive functionality and deal with the groups as "flat" collections of members for better performance. This does not mean that the members of the subgroup appear to be members of the main group. It means you cannot tell they're members, unless they're members of the main group.

There's a special type of subgroup we refer to as a breakout subgroup. Breakout subgroups are used when there are too many members to fit into the main group, given the 32 KB limitation on the size of a summary field (in this case, the field containing the list of members). The group manager automatically creates sufficient additional groups to contain the entire list of members and adds these groups as members to the main group.

The name of a breakout subgroup is the name of the main group, followed by a space and a number. This is a convention of our code, not of Lotus Domino. Lotus Domino doesn't treat groups differently based on their names, but the group manager does.

NOTE: Breakout subgroup functionality only works in recursive mode.

As an example of normal versus breakout subgroups, consider the two hierarchies of groups shown in figure 1.

Figure 1. Normal versus breakout subgroups
Normal versus breakout subgroups

The Sailors group contains names of some sailors, and also two other groups; every Pirate is also a sailor, and every Naval Officer is also a sailor. We keep them in separate groups because it's useful to us. For instance, the Naval Officers group is allowed access to a crew performance rating database that non-officers may not use, and the Pirates group is used as a mailing list for the Pillaging Newsletter, which is not of interest to the general sailors.

The group Hispaniola Crew, by contrast, contains breakout subgroups. Its subgroups are not used anywhere else, and they do not exist for convenience and organizational purposes, but because of the Notes field size limitation. We will hear more about these groups later.

Example 2: More mailing list maintenance

As with Example 1, the agent GMan Process Subscription Requests controls membership of mail-only groups based on mail-in subscription requests from users. To give the agent some data to work with, the sample database contains a Memos view with some mail-in subscribe and unsubscribe requests. Normally, of course, your Domino Directory is never a mail-in database; we put the group records in the same database for demonstration purposes only.

The agent GMan Process Subscription Requests is set to run after new mail has arrived. It processes each memo in the UnprocessedDocuments collection. The code for the agent appears as follows. So that you can try this without creating a mail-in database, there's a similar agent, "GMan Samples\1. Process subscription requests," that you can run on selected documents in the Memos view.

Option Public
Option Declare	' Always use Option Declare
Use "GroupManager"
Sub Initialize
	Dim session As New NotesSession
	Dim db As NotesDatabase
	Dim coll As NotesDocumentCollection
	Dim doc As NotesDocument
	Dim strSubject As String, strVerb As String, strGroupName  
	  As String, strFrom As String
	Dim result As Boolean
	Dim gman As New NotesGroupManager(True)
	Call gman.LoadPublicAddressBooks
	gman.DefaultType = GROUP_TYPE_MAIL ' If we create 
	' a group, make it a mail-only group.
	
	Set db = session.CurrentDatabase
	Set coll = db.UnprocessedDocuments
	Set doc = coll.GetFirstDocument( )

	Do Until doc Is Nothing
		strSubject = Fulltrim(doc.GetItemValue("Subject")(0))
		' We expect subject = "subscribe" or "unsubscribe"  
		' followed by group name.
		strVerb = Lcase(Strleft(strSubject, " "))
		strGroupName = Strright(strSubject, " ")
		strFrom = doc.GetItemValue("From")(0)
		' Make sure group name specified is one we let people
		'subscribe to, e.g., don't allow changes to LocalDomainAdmins 
		'membership.
		If ValidateGroup(strGroupName) Then
			If strVerb = "unsubscribe" Then
				' Remove sender's email from group. Also from 
				' subgroups, in case list is so large that there are 
				' 'breakout' subgroups.
				Call gman.RemoveFromGroup(strGroupName, 
				  strFrom, GROUP_RECURSE)
				SendMessage db, strFrom, "Your unsubscribe request  
				  was processed", {You have been removed from group "} 
				  & strGroupName & {". So long!}
			Elseif strVerb = "subscribe" Then
				' Add member to the group with these options:
				'  -- Create the group if it doesn't exist.
				'  -- Don't add name if it's already in a subgroup.
				'  -- If name is already there but not an exact match, 
				' update it.
				result = gman.AddToGroup(strGroupName, strFrom, 0,  
				  GROUP_CREATE + GROUP_RECURSE + GROUP_UPDATE)
				If result Then
					' Not already in the group (or name changed) -- 
					' welcome them.
					SendMessage db, strFrom, "Your subscription to 
					  "& strGroupName & " was processed.", 
					  {Welcome to the "} & strGroupName & 
					  {" mailing list! Please review our policy 
					  at http://www.iupp.org before posting to 
					  this list.}
				End If
			End If ' Subscribe
		End If ' Group name valid
		Call session.UpdateProcessedDoc(doc)
		Set doc = coll.GetNextDocument(doc)
	Loop
	gman.SaveAll ' Or none of your group changes will be saved
End Sub

The subroutines ValidateGroup and SendMessage are not shown here, but you can find them in the sample database. The implementation of ValidateGroup varies depending upon which groups you want to let this agent manage, and SendMessage is beyond the scope of this article. Unlike Example 1, this agent handles both subscribe and unsubscribe requests. Most of the code gets the information about what needs to be done. The actual execution of it is in the following lines of code:

  • Use "GroupManager" loads the GroupManager library.
  • Dim gman As New NotesGroupManager(True) creates the NotesGroupManager object.
  • Call gman.LoadPublicAddressBooks tells the group manager to use all Domino Directories/public address books.
  • gman.DefaultType = GROUP_TYPE_MAIL specifies that if we create a group automatically (so that we can add someone to it), it will be a mailing list group.
  • Call gman.RemoveFromGroup(strGroupName, strFrom, GROUP_RECURSE) removes someone from a group and from all subgroups. The GROUP_RECURSE option also removes this person from all subgroups. By default, he is only removed from the named group. This is important when working with groups that may be large enough to require breakout subgroups. Unless you remove the person from all subgroups, he is still a member of the main group.
  • result = gman.AddToGroup(strGroupName, strFrom, 0, GROUP_CREATE + GROUP_RECURSE + GROUP_UPDATE) adds someone to a group. Some points about this line:

    The third argument (0) is the zero-based index of the directory that you want to create the group in if it doesn't exist. This index is based on the list of Domino Directories that were loaded. Because we called LoadPublicAddressBooks previously, 0 is the index of the primary Domino Directory, names.nsf.

    The GROUP_RECURSE option specifies that we do not want to add someone to the main group if she is already a member of a subgroup. When you work with groups that may be large enough to overflow into breakout subgroups, you should specify this option for all adds to avoid duplication.

    GROUP_UPDATE controls what happens if there's a member of the group whose email address is the same as the one we want to add, but is not identical (for instance, if "Hester Prynne <hester@bigredletter.org>" is a member and we ask to add "Hester A. Prynne <hester@bigredletter.org>"). By default, the group is not updated in this case; if you specify GROUP_UPDATE, the group is updated to exactly match the new name.
  • gman.SaveAll saves all group changes made through the group manager.

Example 3: Deleting and creating groups

The agent "GMan Samples\2. Delete and Create Groups" shows how to delete and create groups and how to deal with a simple hierarchy of groups. This agent first deletes all the groups shown in figure 1. For the Sailors, Naval Officers, and Pirates groups, this is simple because we know their names and can use gman.RemoveGroup. For the group Hispaniola Crew, though, it's a little more complicated because this group may have an unknown number of breakout subgroups. Deleting the main group does not automatically delete any subgroups, so we must code for that manually. Here's the code to delete all these groups:

	Dim gman As New NotesGroupManager(True)
	Dim group As NotesGroup
. . .
	Call gman.RemoveGroup("Pirates")
	Call gman.RemoveGroup("Sailors")
	Call gman.RemoveGroup("Naval Officers")
	
	' Because we'll be doing several operations on one group, 
	' get a NotesGroup object to make things more efficient.
	Set group = gman.GetGroup("Hispaniola Crew")
	If Not (group Is Nothing) Then
		Dim subgroups
		' Are there subgroups to hold overflow (e.g., "Hispaniola Crew 2")?
		subgroups = group.BreakoutSubgroups
		Forall subgroup In subgroups
			Dim strTemp
			strTemp = subgroup.Name
			Call group.RemoveMembers(strTemp, 0) 'Remove subgroup from 
			  main group.
			Call gman.RemoveGroup(strTemp) 'Delete the group altogether.
		End Forall
		gman.RemoveGroup("Hispaniola Crew")
	End If

The BreakoutSubgroups property returns the name of those subgroups whose name matches the pattern that our code uses to name these (the main group name followed by space and a number). In this case, the NotesGroupManager class didn't have the properties we needed to access the list of breakout subgroups associated with a group, so we had to use a NotesGroup object to get the details of the specific group.

Another way to delete a group and all of its breakout subgroups is to figure out which names they have and delete any group by that name like this:

Sub DeleteBreakoutGroup(gman As NotesGroupManager, strName As String)
	Dim intInd As Integer
	If gman.RemoveGroup(strName) Then
		intInd = 2
		While gman.RemoveGroup(strName & " " & intInd)
			intInd = intInd + 1
		Wend
	End If
End Sub

The RemoveGroup method returns a Boolean value of True if the group you asked to delete actually existed, so you can test the return value to find out if there is any need to look for more groups with similar names. This is easier coding, but less safe because there's no absolute guarantee that someone may not have deleted one of the breakout subgroups manually, so you miss deleting higher-numbered groups.

Next, we want to re-create the groups we just deleted. There are two ways to create groups: Add members using the GROUP_CREATE option as in Example 2 or use gman.CreateGroup as shown in the following:

	Redim values(0 To 3) As String
	Set group = gman.CreateGroup("Naval Officers", GROUP_TYPE_MULTI, 0)
	group.Description = "Current serving officers only."
	values(0) = "Horatio Hornblower/Hotspur/HRMN"
	values(1) = "Jack Aubrey/Surprise/HRMN"
	values(2) = "Stephen Maturin/Surprise/HRMN"
	values(3) = "James Hook/BadGuys/Neverland"
	Call group.AddMembers(values, 0) ' 2nd argument is options

Notice we use the Description property of NotesGroup to assign the list description field. We can only do this if we have a NotesGroup object, which we don't have if we created the group using gman.AddToGroup. Of course, you can always get the NotesGroup object later using GetGroup, for instance:

gman.GetGroup("Pirates").Description = "Scourges of the Sea"

So far we have discussed two ways for adding members to a group, gman.AddToGroup and group.AddMembers. All methods for adding and removing members accept either a single string or an array of strings so that you can work with multiple members at a time. In this case, rather than assign elements of an array and then pass the array to AddMembers, we could instead have written:

	Call group.AddMembers("Horatio Hornblower/Hotspur/HRMN", 0)
	Call group.AddMembers("Jack Aubrey/Surprise/HRMN", 0)
	Call group.AddMembers("Stephen Maturin/Surprise/HRMN", 0)
	Call group.AddMembers("James Hook/BadGuys/Neverland", 0)

Notice we use Notes IDs in abbreviated format. User names in Group documents must be in canonical format ("CN=..."), but the group manager code accepts either format and does this conversion automatically.

A third way to add members to a group uses the Members property of NotesGroup. This is a read/write property, so you can assign it with an array value to change the entire group membership at once. Here's more code from the sample agent:

	Set group = gman.CreateGroup("Sailors", GROUP_TYPE_MULTI, 0)
	group.Description = "All navvies"
	values(0) = "Queequeg/Pequod/Whalers"
	values(1) = "Jack Dawson/Titanic/White Star"
	values(2) = "Pirates" ' the group
	values(3) = "Naval Officers" ' the group
	group.Members = values

This way of assigning the group membership is always a "flat" add. The members list is exactly what you specify, even if there are some duplicate names in subgroups.

Example 4: Creating a breakout subgroup

There's no difference between creating a breakout subgroup and creating normal groups as in the previous example. All you have to do is make sure the NotesGroupManager object is created in recursive mode (in other words, that the argument to the New method is True), and this will be handled automatically if the group gets too large.

The agent "GMan Samples\3. Add many members" shows this. To create a group large enough to require a breakout subgroup, it contains a pirate name generator that randomly selects from lists of adjectives, first names, and last names to put together names, such as Long John Silver or Whistling Ned Doggett. The random name is added to the Hispaniola Crew group as follows:

		If group.AddMembers(strCrewName, GROUP_RECURSE) Then
			intCount = intCount + 1
		End If

When working with groups that may be large enough to contain breakout subgroups, it's important to use the GROUP_RECURSE option. Otherwise, you may add someone to the main group who's already in a breakout subgroup. The same applies when removing members.

We use the return value of AddMembers to determine whether or not a name was really added. If the person was not already a member, this method returns True. In this case, we do this to keep track of how many unique names were added, so we know when to stop.

Run this agent multiple times if you want to see just how many members you can cram into one group. There are about 16,000 different combinations of adjective, first name, and last name; you can edit the agent and add to the lists to increase the number of combinations.

Example 5: Searching for groups

NotesGroupManager has a property, CachedGroups, that is a list of all the groups it has loaded in its cache. Ordinarily, groups are loaded as you request them by name; in recursive mode, all subgroups of requested groups are also loaded.

You can search for groups that match a full-text query using group.FTSearch. This takes as argument a query string that is used as an argument to the FTSearch method in each of the Domino Directories that the group manager knows about. The Group documents that match the search are held in the cache.

If the Domino Directory is full-text indexed, you can use this method to quickly find all groups in which a certain person is a member by searching for her common name or email address in the Members field. You could then use the group.IsMember property to weed out false matches. The following code comes from the agent "GMan Samples\4. Full Text Search":

	strSearch = Trim(Inputbox( "Search string:"))
. . .
	lngCount = gman.FTSearch(strSearch)
	Dim groupList As Variant
	Dim strGroupDesc As String
	
	If lngCount = 0 Then
		Msgbox "No groups matched your query."
	Else
		groupList = gman.CachedGroups
		Forall group In groupList
			strGroupDesc =  strGroupDesc & ", " & group.Name
		End Forall
		Msgbox "The following " & lngCount & " groups matched 
		  your query: " & Mid$(strGroupDesc, 3)
	End If

If you want only the groups that match the search results, and not their subgroups, you have to use the group manager in nonrecursive mode.

There are a few other methods that manipulate the cache: LoadAllGroups, LoadGroup, Uncache, and ClearCache. LoadAllGroups gets information about all the groups in all the directories so that you can use a loop, such as the one described earlier, to iterate through them.


Conclusion

The samples presented in this article are intended to get you started using this custom code. There are other methods and properties we haven't mentioned that let you control group ownership, sort the member list, access the Group document directly to change any fields that don't have a corresponding property, and so on. All these are fully documented in the sample database found in the LotusScript Gold Collection at OpenNTF.org.

Once you have the source code, you can also modify or extend the classes as you see fit. If you modify this version, we encourage you to submit it to OpenNTF.org for others to use. If you encounter issues with the sample database documentation or in any of the code, please email the author.

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into IBM collaboration and social software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Lotus
ArticleID=32422
ArticleTitle= Working with groups in LotusScript
publish-date=10132011