Over the last year I've changed how I approach development with OpenText Content Server. The change came after 12 years of conventional approaches, so you may understand it wasn't an easy change to make. However, it has made my job much easier. This blog post will be part of a multipart series that summarizes my motivation for the change, the research I've done, and the framework that came out of it.
Content Server uses a procedural programming approach in the majority of its implementation. What this means is that most implementations have two parts: data and operations on that data. Data represents some type of information (e.g., in the form of an
Record, etc.) and these are passed into functions and procedures to operate on it. A developer must know what a piece of data represents (e.g., a node, user, workflow, permission, category, etc.), what functions are available, and where they are located in order to use them. Furthermore, it's often required a developer convert between representations of data in order to satisfy the parameter requirements of a function.
For example, say you have the
DataID of a node and wish to programatically rename it. You must first fetch the related
DAPINODE (via the
DAPI library), do some error handling, fetch the related
LLNode (via the
$LLIAPI.LLNodeSubsystem subsystem), and then finally call the
Integer DataID = … DAPINODE node = DAPI.GetNodeByID( prgCtx.DapiSess(), DAPI.BY_DATAID, DataID ) if IsDefined(node) && IsNotError(node) Object llnode = $LLIAPI.LLNodeSubsystem.GetItem( node.pSubtype ) Assoc results = llnode.NodeRename(node, "My Renamed Node") if results.OK // all good else // handle error end else // handle error end
This is a lot of code and requires a deep understanding of where various components are located, how they are related, and how they can be linked together to perform the operation.
Similarly, if you wish to create an
open link for a node you must get the
WebNode record representation of the node, the related
WebNode object, the open
WebNodeCMD object, and then finally the URL from the
Record nodeRec = $WebNode.WebNodeUtils.NodeToWebNode( node ) Object webNode = $WebNode.WebNodes.GetItem( nodeRec.SUBTYPE ) Object cmd = webNode.cmd('open') String link = cmd.url( request, nodeRec )
It seems rather verbose to require all this information from various locations just to generate a URL. It also violates the don't repeat yourself (opens new window) (DRY) principle since it's the same steps anytime you wish to generate a URL for a different node.
Another example are workflow
GeneralCallbackScripts. A callback script receives a number of IDs (
taskID, etc.) that have little meaning on their own. You can get more data via function calls (e.g.,
prgCtx.WSession().LoadTaskStatus(), etc.), but these return more data structures that require an even deeper understanding of what they mean before further functions can be called on it.
This pattern is found often, and I'd argue it's the hardest thing to learn in Content Server development. What does a piece of data mean? What functions are available to operate on it? Where do I find these functions? How do I get data into a form that complies with the input parameters of a function? What are the intermediate steps to get my desired result?
I'm of the opinion that much of this can be simplified with an object-oriented programming (OOP) approach. For those unfamiliar with OOP, Wikipedia has a good definition (opens new window):
Object-oriented programming (OOP) is a programming paradigm that represents concepts as "objects" that have data fields (attributes that describe the object) and associated procedures known as methods.
In other words, an object is something that encloses the representation of data and its associated functions into a single representation. Sounds good! Further on:
Objects can be thought of as encapsulating their data within a set of functions designed to ensure that the data are used appropriately, and to assist in that use. The object's methods typically include checks and safeguards specific to the data types the object contains. An object can also offer simple-to-use, standardized methods for performing particular operations on its data, while concealing the specifics of how those tasks are accomplished.
The last line summarizes my motivation and goal: I want to be able to rename a node or get a URL with a minimum number of intermediate steps. I don't want to know anything about
DAPINODE, the various subsystems, the
WebNodes view, the
WebNodes object, or the
WebNodeCMD object to accomplish these tasks.
Fortunately, OScript supports basic OOP concepts. Not everything is possible, but the fundamentals such as classes, objects, inheritance, overriding, encapsulation, and polymorphism (with a little coercion) is possible. There is even a way to do
super calls. Content Server already uses an OOP approach in some areas (e.g., categories and attributes), but it isn't widely used.
Just to be clear: I'm not suggesting a rewrite of core Content Server functionality using OOP. Instead, I believe many complex and verbose patterns can be simplified by abstracting their implementation into an object, which provides a simpler interface to the developer. But also, OOP is a great tool when it comes to developing new modules and features.
Let me illustrate with an example from my
RHCore library. In the library is a class named
RHNode, which represents a node in Content Server (I tend to prefix my stuff with "RH" to minimize naming conflicts and confusion with other modules). It is subclassed from
RHObject, which is my base class (similar to how
java.lang.Object is the base class in Java). An
RHNode object can be constructed from a
Record (DTree or WebNodes), or nickname:
// node can be a DataID, DAPINODE, Record, or nickname Frame mynode = $RHCore.RHNode.New( prgCtx, node )
This returns an "object" (in the OOP-sense, using the Content Server
Frame datatype, and not to be confused with the
Object datatype in Content Server) representing my node. Inside that object is all the data pertaining to the node (e.g., name, dataid, create date, owner, parent, etc.) plus methods to operate on it. For example, it includes a
Assoc results = mynode.rename("My Renamed Node") if results.OK // all good else // handle error end
It also includes a method to create URLs:
String url = mynode.url(request, 'open')
These examples work without having to know anything about a
WebNodes representation of the node, or the various subsystems. That's all abstracted away and managed behind the scenes by
RHNode. Of course, accessors such as
webNode(), etc. are available if you need them.
System attributes (e.g.,
ExtendedData, etc.) are accessible with the
String nodeName = mynode.valueForKey('name') Integer parentid = mynode.valueForKey('parentid')
However, some commonly used attributes have their own method (these are just convenient shortcuts):
String nodeName = mynode.name() Assoc extendeddata = mynode.extendeddata()
Here is where it gets interesting.
RHNode has a number of methods for fetching related information and calculated values that would otherwise require multiple lines of code. A few examples:
subTypeName()method returns the subtype name of the node (e.g., "Folder", "Document", etc.);
size()method returns the size of the node in a human-readable format (e.g. 25KB, 5 items, 3 Sub-Projects, etc.);
isEditable()method returns true if the current user has edit permission on the node;
gif()method returns the path (relative to the support directory) of the node's icon;
versionAsText()method returns the contents of the document (as long as it has a text-based mime type);
categoryvalues()method returns the node's category values in a simplified
Assocthat is easy to traverse;
children()method returns the child objects as an
Iterable(a collection of values that can be looped, but more on the
Iterableclass another time); and
parent()method returns the parent as an
Undefinedif it's not accessible.
The last example means you can fetch the name of the parent like this:
Frame parentNode = mynode.parent() String parentName = parentNode.name()
Or, chain them together into a single line:
String parentName = mynode.parent().name()
Or get the name of the parent's parent:
String grandparentName = mynode.parent().parent().name()
An astute reader may have noticed the problem here: If a call in the chain returns
Undefined it will cause the thread to crash on the subsequent call. Since OScript doesn't support exception handling we are forced to handle this differently.
The concept of key-value coding was made aware to me by Apple when I started developing for the iPhone. From their documentation (opens new window):
A key is a string that identifies a specific property of an object. Typically, a key corresponds to the name of an accessor method or instance variable in the receiving object.
To resolve a key we use the
valueForKey method (defined in the
RHObject base class), which I introduced earlier for resolving system attributes. However, this method also works with accessor methods. Accessor methods are methods that do not accept parameters and do not alter the state of the object. In other words, the
valueForKey() method can be used with all the methods listed above. For example, each of the following pairs return the same value:
node.parent() node.valueForKey('parent') node.size() node.valueForKey('size') node.categoryvalues() node.valueForKey('categoryvalues')
Reading on in the documentation:
A key path is a string of dot separated keys that is used to specify a sequence of object properties to traverse. The property of the first key in the sequence is relative to the receiver, and each subsequent key is evaluated relative to the value of the previous property.
To resolve a key path we use the
valueForKeyPath() method, which
RHNode also inherits from
RHObject. For example:
String grandparentName = node.valueForKeyPath("parent.parent.name")
By default, the
valueForKeyPath() method returns
Undefined if a key path cannot be fully resolved. This can be changed with an optional second parameter:
String grandparentName = node.valueForKeyPath("parent.parent.name", "<< Unknown Name >>")
As an aside: The
valueForKeyPath() function is also available as a standalone function and can be used to traverse any structured data type in Content Server without having to assert if a feature exists before attempting to access it. It's very handy.
That said, the
valueForKeyPath() method permits some otherwise complicated operations to be performed in a single line of code. For example, say you have a node and on the parent is a category (named "My Category") with a multi-valued attribute (named "My Attribute") and you wish to fetch the second value. This is simply:
Dynamic value = node.valueForKeyPath("parent.categoryvalues.'My Category'.'My Attribute'.2")
Another class in the same genre is
RHUser wraps the details of a user or group and abstracts away many of the complicated
$LLIApi.UsersPkg function calls to manipulate users, groups, memberships, and roles. It can be constructed directly:
Frame rhuser = $RHCore.RHUser.New( prgCtx, 1000 )
Or, it can also be retrieved from an
Frame rhuser = node.user() // fetches the node owner as an RHUser Frame rhgroup = node.group() // fetches the node group as an RHUser
Once you have an
RHUser instance you can do some interesting things:
Boolean isUser = rhuser.isUser() // return true Boolean isUser = rhgroup.isUser() // return false String username = rhuser.name() // returns "Admin" // get the Personal Workspace as an RHNode Frame pws = rhuser.personalworkspace() // returns all direct memberships of this user as an iterator Frame groups = rhuser.memberships() // returns all direct & indirect memberships of this user as an iterator Frame allgroups = rhuser.allmemberships() // returns all nodes where the user is the owner Frame nodes = rhuser.ownedNodes() // fetch the last login date from the audit logs Date lastLogin = rhuser.lastLogin()
RHUser also has a method to return the display name as configured in the
String displayName = rhuser.displayName()
RHUser is also key path compliant, which means you can use a key path to retrieve the display name of a node's owner in a single line:
String ownerDisplayName = node.valueForKeyPath('user.displayName')
This only scratches the surface of what's possible. I've written similar wrappers for permissions, workflows, workflow tasks, and audit information, and will soon be adding support for Records Management. Not only is OOP useful for abstracting away Content Server data and functions, but it can also be used as the basis for other frameworks or tools. Take my zip and download wrapper class as an example, which simplifies the generation of zip files:
Frame myZip = $RHCore.ZIP.New(prgCtx, 'myzipfile.zip') // create an object to which I can add documents myZip.add( node ) // node can be an RHNode, DataID, DAPINODE, nodeRec, or nickname of a document myZip.add( node2 ) … String downloadURL = myZip.zip() // zip up the files and return a download URL for myzipfile.zip
Using an object-oriented programming approach has completely changed the way I develop for Content Server. It's become the basis of my
RHCore development framework, and is proving to be extremely effective in my projects. I'm doing much more with less code and fewer bugs.
In my next blog post I will expand on this framework. I will start with the model component, which allows a developer to create a complex relational database schema with a fully functional object-based API for querying, adding, updating, and deleting records (with referential integrity) in only a few lines of code and without a single line of SQL. Stay tuned.