An alternative way of working with hierarchically structured
data is to take advantage of the support for hierarchical objects
in the Tivoli® Directory Integrator entry object.
Different from earlier versions, Tivoli Directory Integrator v7.1.1
supports the concept of the hierarchical Entry object. The Entry object
represents the root of the hierarchy and each Attribute represents
a node in that hierarchy. Following this logic the values of every
Attribute are leafs in the hierarchy. An API for traversing the hierarchy
exists as well. This API is a partial implementation of the DOM 3
specification. Only a few classes from that specification have been
implemented:
- Org.w3c.dom.Document – implemented by the Entry
class.
- Org.w3c.Element – implemented by the Attribute
class.
- Org.w3c.Attr – implemented by the Property class.
- Org.w3c.Text and org.w3c.CDATASection –
implemented by the AttributeValue class.
These classes are the minimum set of classes provided by the DOM
specification that are needed to represent a hierarchical data. The
pre-v7.0 API is not hierarchy aware (for example, it cannot access/modify/remove
child Elements) for backward compatibility reasons. This is why only
the DOM API can manipulate a hierarchical structure.
To keep the Entry structure backward compatible by default the
Entry always uses flat Attributes. The Entry only becomes hierarchical
on demand – after you call one of the newly provided DOM APIs. This
allows only components aware of the hierarchical nature of the Entry
to make use of it, the rest of the components don't need to be
changed in order to keep running. Starting from Tivoli Directory Integrator v7.0, a new name notation is introduced
in order for users to have an easier way of creating hierarchical
trees. Every name containing a dot in it is thought to be a composite
name compound of simple names; these names are separated by dots.
When such a composite name is passed to a hierarchical Entry, it
breaks it down to simple names and builds the hierarchy that is described
by that composite name.
For example if you run the following JavaScript code:
// create a new empty Entry object
var entry = new com.ibm.di.entry.Entry(true);
// create a new branch of 2 levels
entry.setAttribute("firstLevelChild.secondLevelChild", "level2Value");
// finds the already existing branch and creates a new node on level 3
entry.setAttribute("firstLevelChild.secondLevelChild.thirdLevelChild", "level3Value");
the following structure will be created:
Figure 1. Simple hierarchical entry
Note: It is important to know that the names are not broken down to
simple names until the Entry structure is not converted to a hierarchical
one. For example if the entry is a flat one and only the old methods
are used that would still create the same old flat structure:
Figure 2. Traditional, flat entry
In some cases it may be necessary to have dots in the names of
the Entry's attributes, so the Entry object also understands
escape characters (currently only \\ and \. are supported).
Note: When
working from script the backslash should be properly escaped with
another backlash!
For example if the following hierarchy is
needed:
Figure 3. Another simple hierarchical entry
the following script would create it:
var entry = new com.ibm.di.entry.Entry(true);
entry.setAttribute("first\\.level\\.child.second\\.level\\.child", "level2Value");
entry.setAttribute("first\\.level\\.child.second\\.level\\.child.third\\.level\\.child", "level3Value");
In order to maintain backward compatibility with previous releases
of Tivoli Directory Integrator when implicitly working with
hierarchical Entries, all the old methods exposed by both the Attribute
and the Entry classes have changed slightly. For example, the Entry.getAttributeNames() method
will return an array of full paths to the leafs that are available
in the tree. In reference to the above structure the getAttributeNames
method returns the array:
["first\.level\.child.second\.level\.child", "first\.level\.child.second\.level\.child.third\.level\.child"]
The Entry.size() method returns the total number
of elements in the array returned by getAttributeNames() method.
In our case the size() method would return 2 (the number of leafs
in the whole tree) instead of 1 (as you might expect, considering
that the Entry object has only one attribute as a child).
This is because both getAttributeNames() and size() methods only
work with flat structures. In order to get the real size of the children
you would need to use the DOM API as follows: Entry.getChildNodes().getLength();
If the entry is a flat one the old API will behave as in previous
releases.
The Attribute object
The Attribute object
is enhanced with the ability to create hierarchical structures. These
structures follow the DOM specification and that is why the Attribute
object is aware of XML Namespace concepts.
The Attribute class
also had to be expanded in order to provide new methods for the hierarchical
functionality. The getValue/setValue/addValue methods are also backward
compatible and will return only the values the particular element
has. The difference here is when each value is accessed through the
DOM API it will be wrapped in an AttributeValue class and will be
treated as a Text node. In order to get the child elements of an Attribute
(for example, Attributes and AttributeValues) the DOM API has to be
used.
The Attribute child of an Entry is also able to switch
the Entry's structure to hierarchical one when any of its DOM
methods are accessed. Unlike the Entry class the Attribute class does
this implicitly and does not provide a way to do the switching explicitly.
Tivoli Directory Integrator v7.0 supports extensions to scripting
capabilities of the Server to easily access complex structures. Please
see the section Navigation within scripts for
more details.
The AttributeValue object
This class represents
a value in the hierarchical tree. As per DOM specification the values
in the tree are always Strings. The AttributeValue class however,
has held objects of any kind for the last few releases. This is valid
in the current version as well. The only difference for the AttributeValue
object is that when accessed through DOM it will return a String representation
of the contained object. In order for the AttributeValue class to
represent a Node and have a value at the same time in the terms that
the DOM specification defines, it must implement either the org.w3c.dom.Text
or org.w3c.dom.CDATASection interfaces. The AttributeValue implements
them both, and can represent either of these Nodes depending on your
needs.
The Property object
Attributes can have
zero, one or more Property objects. The Property class implements
the org.w3c.dom.Attr interface and thus represents the attributes
in terms of DOM concepts. Using properties you may declare prefixes/namespaces
in terms of XML concepts.
Transferring Objects
Mapping an Attribute
from one entry to another will always copy the source Attribute.
For
example:
entry.appendChild(conn.getFirstChild());
// or
entry.setAttribute("name", conn.getFirstChild());
Even
when the Attribute is not a first-level child of the Entry it is still
copied. This can also be accomplished by the script:
entry.a.b.c.d.appendChild(conn.e.f.g);
In
order to move an Attribute object between entries without cloning
it you will need to first detach it from its old parent and then attach
it to a new parent.
For example:
var src = entry1.b.source;
entry1.b.removeChild(src);
entry2.a.target.appendChild(src);
When moving an Attribute
object from one parent to another parent in the same Entry the Attribute
is automatically moved. No cloning is done.
For example:
entry.a.target.appendChild(entry.b.source);
In
this example the "source" Attribute is detached from its parent ("entry.b")
and then attached to the "entry.a.target" Attribute. No cloning is
done.
If you do not want to remove the Attribute object from
the source then you can append a copy of the Attribute like this:
entry.a.target.appendChild(entry.b.source.clone());
Navigation within scripts
The Tivoli Directory Integrator ScriptEngine enables you to easily
access the attributes of an entry just by referring to them by name;
for example, entry.attrName returns the attribute
with name attrName.
- The JavaScript Engine
resolves names based on the context object the name was requested
on. For example, if the call entry.a is performed,
the entry name is the context object and a is
the name of the child object to resolve. The JavaScript Engine uses left-to-right interpretation
to evaluate each context object until the final one is resolved. Based
on the diagram below the following call, entry.a.b.c,
is resolved using this procedure: Find the entry object
to use it as the context object for the first step.
- Search the context object for a name a. The entry object
has only one child with name a. Consider that child
to be the next context object for the next step.
- Search the context object for a name b. The
context object has two children named b. Put them
in a list and return that list.
- The final operation searches the list returned in the previous
operation for the name c. Each element in the list
has at least one such child. Get them all and put them in a list,
which is the actual result of resolving the full expression.
The following entry object example diagram illustrates this:
Figure 4. Hierarchical Entry object example
The script engine provided by
IBM® Tivoli Directory Integrator allows
more arbitrary names to be used in child resolving process. For example
if the name of the child contains dots then you can refer to it using
the square-bracket syntax as shown below:
work["{namespace}name:Containing\.invalid\.charaters"]
Notice
that the dots are being escaped to denote that they are part of the
local name and that they must not be treated as path separators by
the script engine.
Depending on the current context object
on which an operation is performed the end result might differ. Tivoli Directory Integrator V7.1.1 adheres
to the standard object manipulation the JavaScript Engine provides by implementing
several enhancements to the following objects:
- Entry
- When the context object is an instance of this type, the name
resolving mechanism will look for the following syntax:
- Attribute
- When the context object is an instance of this type, the name
resolving mechanism will look for the following syntax:
- @<prefix>:<localName> and @<localName> –
searches the Attribute for Property objects that have the same prefix
and/or local name or just the specified local name. Returns either
null if no properties match the specified name, or a single Property
object.
- @{namespaceURI}<localName> – searches the
Attribute for a Property that belongs to the specified namespaceURI
and have the same Local Name as the specified name.
Note: If a prefix
is provided it will be ignored and the name resolving mechanism will
only look for the specified namespaceURI and localName. The resolved
object could be either null or a Property object.
- [<index>] – specifies the position of the
value in the Attribute to retrieve. Using this notation you cannot
access a child of this Attribute. Returns either null, or the Object
at the specified position.
- <prefix>:<localName> and <localName> –
searches the Attribute for a child(ren) with the specified prefix
and/or local name. Returns either null or the child with the specified
name (if only one), or a NodeList with all the child Attributes that
match the criteria.
- {namespaceURI}<localName> – searches the Attribute
for all the children Attributes that belong to the specified namespaceURI
and have the same Local Name as the specified name.
Note: If a prefix
is provided it will be ignored and the name resolving mechanism will
only look for the specified namespaceURI and localName. The resolved
object could be null, a single Attribute object or a NodeList containing
all of the Attributes that match the search name.
- NodeList
- When the context object is an instance of this type the name resolving
mechanism will look for the following syntax:
- [<index>] – specifies the position of the
element in the NodeList to retrieve. Returns either null, or the Object
at the specified position. Throws exception if the index is out of
the bounds.
- @<prefix>:<localName> and @<localName> –
searches each of the elements of the NodeList for a property that
has the same prefix and/or local name. Returns either null, a Property
object (if only one found), or a List of all the Property objects
found in the NodeList.
- @{namespaceURI}<localName> – searches each
of the Attributes for a Property that belongs to the specified namespaceURI
and have the same Local Name as the specified name.
Note: If a prefix
is provided it will be ignored and the name resolving mechanism will
only look for the specified namespaceURI and localName. The resolved
object could be either null or a Property object (if only one found)
or a NodeList of all the Property objects found in the NodeList.
- <prefix>:<localName> and <localName> –
searches each of the elements of the NodeList for a child that has
the same prefix and/or local name. Returns either null, an Attribute
object (if only one found), or a NodeList of all the Attribute objects
found in the NodeList.
- {namespaceURI}<localName> – searches each
of the Attributes for all the children Attributes that belong to the
specified namespaceURI and have the same Local Name as the specified
name.
Note: If a prefix is provided it will be ignored and the name
resolving mechanism will only look for the specified namespaceURI
and localName. The resolved object could be null, a single Attribute
object or a NodeList containing all of the Attributes that match the
search name.
You now have the following options:
- Ability to access all the d elements by referring
to them starting from the top; for example, entry.a.b.c.d –
this returns an object of type NodeList with all the d attributes
that match that path. In our example this will return all the three d elements.
- Ability to access attributes by specifying both prefix and local
name; for example, entry[”a.b.c.pref1:d”] – this
will return a single Attribute only, the one with a prefix of "pref1".
- Ability to access each Attribute of an NodeList using the [ ]
notation, for example, entry.a.b.c.d[0] – this returns
a single Attribute, namely the first d element
of the structure above.
- Ability to navigate through elements using the [ ] notation; for
example, entry.a.b[0].c.d – this returns a List of
Attributes, but this time it contains all the d attributes
form the first b branch.
- Ability to get the property of an attribute (that is, an Attribute
of an Element using the DOM naming convention) using the @ notation;
for example, entry.a.b.c.d[0]["@propPrefix:propName"] –
this returns us a String object containing the value of that property
(that is, the Attribute's value according to DOM). Note here that
the propPrefix and the propName are separated by the colon sign (":"),
and that if the property has a prefix, it is mandatory that both the
prefix and the name be specified in order for the property to be found.
Alternatively the namespace and the propertyName can be used to find
a property that has a prefix.
- Ability to call methods of an Attribute before searching for child
with the name of the method that the user wants to execute; for example, entry.a.b.[0].getChildNodes() –
this returns a NodeList object that holds all the Attribute values
that the first b Attribute contains.
- Ability to access entry attributes by name; for example, entry.attrName –
this will return the Attribute mapped to the attrName key.
- Ability to access entry properties using the .@ notation; for
example, entry.@propName – this will return the Object
mapped as property to the propName key.
- Ability to access child nodes by specifying the namespace those
children belongs to. For example work.a.b[{someNamespace}c] –
this will return all the c objects that belong
to the "someNamespace" namespace.
Note: - If the name of an attribute is the same as the name of a method
of an Entry/Attribute then the method will be called. If you want
to access the attribute, you must use the object's methods like
before (that is, Entry.getAttribute("getAttribute");.)
- If a flat entry is used the script engine will not convert the
entry to a hierarchical unless the namespace of the attribute to find
is specified. For example: entry["{ns}element"].
The script engine will automatically convert the entry to a hierarchical
one if the context object is of type Attribute. For example in this
case: entry.attr.child.
- If the attribute to find contains dots in its name and the entry
is flat the you must use the bracket notation instead of the dot notation,
or force the entry to become hierarchical before resolving the child
node. For example if the entry is flat you cannot access the attribute http.body using
the call entry.http.body. For this to happen you
will have to use this instead: entry["http.body"] or
call entry.enableDOM() prior to calling entry.http.body.
- If you intend to use the reference retrieved from the expression,
for example, a.b.c.d more than once, then it is good
practice to assign that reference to a local variable since each repeating
evaluation of the same expression will result in additional overhead
for retrieving the same reference.
- If, for example, the expression entry.a.b.c[0].d is
used, then this will refer to an Attribute object; but if entry.a.b[0].c.d
is used, then this will refer to a NodeList object. In order to recognize
the referenced object you can check the name of the object, for example:
var obj = a.b.c.d;
if (obj.getClass().getSimpleName().equals("Attribute") ) {
// handle this as Attribute
} else {
// handle this as a list of Attributes
}
If, for example, you need to handle each element returned
by an expression, even if only one element is returned then you can
use a for/in loop structure like this:for (obj in entry.a.b.c.d) {
// the obj will be an object of type Attribute
}
The same usage is now available with the Entry object,
for examplefor (obj in work) {
// the obj will be an Entry's attribute
}
- ScriptEngineOptions also allows assigning values. For example
the following expressions are valid:
- entry.a.b.c.d[0]["@propPrefix:propName"] = "new value";
- This changes the value of the property. If the property does not
exist then it will be created.
- entry.a.b.c.d[0][0] = "new value";
- This replaces the attribute value on position 0.
- entry.a.b.c.d[0][1] = "another value";
- This adds another value to the attribute (if the index is the
same as the size of the array of values.)
- entry.a.b.c.d[0][a.b.c.d[0].size()] = "appended value";
- If you need to append a new value to the list of objects, but
do not know the last element position you can use Attribute#size() to
get the number of children this element has.
- new com.ibm.di.entry.Entry().@propName = "someValue";
- This will create a new property in the Entry object with name
propName if it does not exist, and will set its value to the String
"someValue".
- entry.a.b.c[1] = "value";
- This will resolve entry.a.b.c as a NodeList
and will automatically add as a value the new String object to the
second element of the resolved NodeList object. If, for example, entry.a.b.c resolves
to an Attribute, then the new String value will replace the second
value of the resolved c Attribute.
- entry.a.b.c[2]["pref3:d"] = "value";
- This will add a new pref3:d child to the third c attribute
from the entry.a.b.c list. Note that entry.a.b.c[2] resolves
to an Attribute and to a list.
- entry.a.b.c["{namespaceURI}:d"] = "myTextValue";
- This sets a value to the first child Attribute of c that
has a local name equals to d and belongs to the
namespace namespaceURI. If no such attribute is found,
a new one is created and the value is assigned to it. When creating
an attribute, you might as well associate the namespace with a prefix.
In our case this could be done with this: entry.a.b.c["{namespaceURI}prefix:d"]
= "myTextValue";.
- entry["{http://ibm.com/xmlns/}first.second.third"] = null;
- This will first try to find the element with local name "first"
from the namespace "http://ibm.com/xmlns/". When it fails it will
create the element and then will try to resolve its child element
"second". When it fails it will create it and set its namespace to
the one of the first element. Finally an attempt to resolve the third
element will be made. When it fails the last element will be created
with no values. This is equivalent to entry["{http://ibm.com/xmlns/}first.{http://ibm.com/xmlns/}second.{http://ibm.com/xmlns/}third"]
= null;
Creating the above structure with script
// create a new entry that will hold the structure
var entry = new com.ibm.di.entry.Entry();
// create the first branch
var d = entry.newAttribute("a.b.c.d");
// create a new property and assign it a value
d["@propPrefix:PropName"] = "value";
// create a new value of the d Attribute
d[0] = "D1";
// append a new value for the entry.a.b attribute
entry.a.b.appendChild(entry.createElement("c"));
// create a new Attribute d with a prefix on the second c attribute,
// and assign the string "D2" as a first value
entry.a.b.c[1]["pref1:d"] = "D2";
// create a new child of the a Attribute
entry.a.appendChild(entry.createElement("b"));
// choose the second attribute from the entry.a.b NodeList and create a new child named c
entry.a.b[1].appendChild(entry.createElement("c"));
// create the d child of the c Attribute
entry.a.b[1].c["pref2:d"] = "D3";
Navigation using XPath
The Entry class provides
convenient methods for navigating and retrieving data based on XPath
expressions. Using XPath for querying data from an Entry is much more
advanced than using any simple navigation within scripts. It is much
easier to implement a searching and/or a value matching logic in a
single expression than writing multi-lined scripts in order to achieve
the same results.
The Entry class provides the following methods:
- NodeList getNodeList (String xPath);
- Attribute getFirstAttribute(String xPath);
- String getStringValue(String xPath);
- Number getNumberValue(String xPath);
- Boolean getBooleanValue(String xPath);
Flattening hierarchical structures
When
working with hierarchical data it is sometimes necessary to flatten
the nodes of the hierarchy. To do that you can either write a script
for traversing the tree, or use one of the methods:
- getElementsByTagName(String namespace, String localName);
- getElementsByTagName(String tagName);
These methods traverse the tree and search for elements with
the specified name and namespace. The DOM API allows these methods
to accept the character "*" for both names and namespaces. That character
represents a wildcard, which is used to match any element name/namespace.
Using that character for name flattens the tree by returning all the
Attribute nodes in a NodeList. It should be noted that the structure
of the hierarchy has not been changed. The returned NodeList is just
a container for the Element nodes that were found in the tree.
If
you run the following script on the entry from the structure in section
Navigation within scripts:
var list = entry.getElementsByTagName("*");
then
the
list variable will hold the following structure:
list
|
+ a
|
+ b
|
+ c
|
+ d
|
+ c
|
+ pref:d
|
+ b
|
+ c
|
+ pref2:d
Exceptions
An exception is thrown in the
following cases:
- If the method entry.appendChild(newAttr) is called
and the entry already contains an Attribute with the name of the newAttr
object passed to the method.
- If for any of the methods defined by the Document/Node/Element
and implemented by the Entry/Attribute classes a unexpected parameter
is passed.
- An ArrayIndexOutOfBoundsException is thrown if the supplied script
refers to an index of an Attribute/NodeList that does not exist.