Join Mapping
Introduction
Penrose supports joining data from multiple sources into a single LDAP entry. This is useful if your data is spread out in multiple locations but they comprise one entity in the virtual directory.
The requirement is you need to have a set of keys that exists on both sources to link the rows in one source to another. One of these sources will be called the primary source. The primary source determines the existance of an entity and all other sources are dependent on it, meaning that if an entity doesn't exist in the primary source it won't show up in Penrose. In a case where you don't have a common attribute, another technique called Pattern Recognition Based Matching can be used.

Goal
Suppose we have a table "groups" which contains the information about a group including name and description. Then we have another table "usergroups" which contains the list of members of each groups in the form of <groupname, username> tuples. A group may contain 0 members. The goal is to create a mapping that produces the following output:
dn: cn=<group name>,ou=Groups,dc=Example,dc=com objectClass: groupOfUniqueNames cn: <group name> description: <group description> uniqueMember: uid=<member's username>,ou=Users,dc=Example,dc=com uniqueMember: uid=<member's username>,ou=Users,dc=Example,dc=com ...
Solution
Assuming you have defined the "groups" and "usergroups" sources in sources.xml, we will refer them as "g" and "ug" respectively. First we create a mapping and specify how each attribute is computed from the sources.
<entry dn="cn=...,ou=Groups,dc=Example,dc=com"> <oc>groupOfUniqueNames</oc> <at name="cn" rdn="true"> <variable>g.groupname</variable> </at> <at name="description"> <variable>g.description</variable> </at> <at name="uniqueMember"> <expression foreach="ug.username" var="username"> "uid="+username+",ou=Users,dc=Example,dc=com" </expression> </at> </entry>
The "cn" attribute is a direct mapping from the groups' groupname field, and it's used as the RDN of this mapping. The "description" attribute is also a direct mapping.
The "uniqueMember" is computed from the usergroups' username field. It uses BeanShell expression to generate the member's DN.
Then in the same mapping we add the reverse mapping, which describe how to compute the source fields from the LDAP attributes.
<entry dn="cn=...,ou=Groups,dc=Example,dc=com"> <source name="g"> <source-name>groups</source-name> <field name="groupname"> <variable>cn</variable> </field> <field name="description"> <variable>description</variable> </field> </source> <source name="ug"> <source-name>usergroups</source-name> <field name="groupname"> <variable>cn</variable> </field> <field name="username"> <expression foreach="uniqueMember" var="um"> int i = um.indexOf("="); int j = um.indexOf(",", i+1); return um.substring(i+1, j); </expression> </field> </source> </source>
Most of the reverse mapping is a direct mapping from the LDAP attributes. The usergroups' username field is computed by extracting the value from the uniqueMember attribute.
Finally in the same source we specify the relationship between the sources.
<entry dn="cn=...,ou=Groups,dc=Example,dc=com">
<relationship>
<expression>g.groupname = ug.groupname</expression>
</relationship>
</entry>