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 Assoc
, Integer
, RecArray
, 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 NodeRename()
function:
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 url()
function:
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 (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 (workID
, subWorkID
, taskID
, etc.) that have little meaning on their own. You can get more data via function calls (e.g., $WFMain.WAPIPkg.LoadWorkData()
or 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:
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 limited superclass method 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, and by providing 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 DataID
, a DAPINODE
, 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 rename()
method:
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 DAPINODE
, the 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 dapinode()
, nodeRec()
, llnode()
, webNode()
, etc. are available if you need them.
System attributes (e.g., Name
, DataID
, ParentID
, CreateDate
, ExtendedData
, etc.) are accessible with the valueForKey
method:
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:
- the
subTypeName()
method returns the subtype name of the node (e.g., “Folder”, “Document”, etc.); - the
size()
method returns the size of the node in a human-readable format (e.g. 25KB, 5 items, 3 Sub-Projects, etc.); - the
isEditable()
method returns true if the current user has edit permission on the node; - the
gif()
method returns the path (relative to the support directory) of the node’s icon; - the
versionAsText()
method returns the contents of the document (as long as it has a text-based mime type); - the
categoryvalues()
method returns the node’s category values in a simplifiedAssoc
that is easy to traverse; - the
children()
method returns the child objects as anIterable
(a collection of values that can be looped, but more on theIterable
class another time); and - the
parent()
method returns the parent as anRHNode
, orUndefined
if 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:
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
. RHUser
wraps the details of a user or group and abstracts away many of the complicated UAPI
and $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 RHNode
:
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 admin.index
page:
String displayName = rhuser.displayName()
Of course, 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.
Need help developing for Content Server or interested in using RHCore? Contact me at cmeyer@rhouse.ch.
Very well introduction and very well written.I hope after you made your millions you would throw the framework so that lesser mortals like us can use it 🙂
Hi Appu, thanks for your comment. I haven’t figured out how it will be made available for other developers to use. At the moment I’m curious to get feedback from developers who have faced the same problems that I have.
This is very nice approach. I am pretty new to Oscript and found it very confusing. Thanks for putting efforts and sharing your knowledge
Thanks for your comment. What have you found to be the hardest part when learning Content Server development?
I would say figuring out the deeper understanding of where various components are located and how they relate, as you mentioned in your post.
There is not a lot of documentation around this sort of thing I’ve found, and I’ve been left dissecting existing code to determine how certain things are done and then using trial and error when implementing.
At times I enjoy it, but it is time consuming as well.
Hi Chris, this is interesting.
Do you an idea about the impacts on memory and performance ? Creating a new Frame each time isn’t free.
Hi Pierre,
This is a great question and is something I long struggled with. I’ve learned that Frames are not as bad as they seem. When I instantiate an
RHNode
instance I get back something like this (shortened for brevity):The
fParent
is just a reference to myRHNode
class and is not a copy of the structure. This means we’re not allocating much memory to create these things. However, once you start using the object it will fill up with data (e.g., the various caches) and this will take up memory. However, this is true of any implementation and is not unique to my approach.At one point I concerned myself with the iteration of large collections. Say you want to iterate over 10’000 items from
DTree
and have anRHNode
representation on each iteration. This means 10’000 Frame allocations, which I thought would be a problem. I got around this with theIterator
class (which I briefly mention in the blog post), which resuses an instance ofRHNode
on each iteration (i.e., a single instance ofRHNode
throughout the loop).However, testing showed little to no improvement in performance! I can only conclude that Frame allocation has a small performance footprint.Thanks for your comment!
An update: I found an error in my tests and take back what I wrote earlier. The construction of Frames has an impact, but most of the performance issues can be resolved with the use of the
Iterator
class. I’ll likely write a blog post about this another time. Thanks!Really well written and interesting article. I wish that something like this could be build into Livelink/Content Server at a basic level. From an earlier comment by appu and your reply I take it that this is not built into the core product (Content Server) but is instead part of a module you have written?
Hi Tonya,
Correct. The module is something I built myself and isn’t in core. I use it in all my projects now.
Thanks for your comment!
Chris
Hi Chris,
Very well written article about OOP approach in Content Server.
Simply brilliant!!.
Best Regards,
Utsav Arora
Thanks!
Chris,
Very nice article. I have done something like this with Frames before for Workflow objects.
Thanks,
Mahesh
It’s almost necessary, isn’t it? I find the workflow data structures to be the most difficult to figure out. A handy OOP-style frame wrapper works well here. Thanks for your comment.
Hi Chris,
I am new to Opentext and have started learning it recently. It would be great if you could help me with a few initial pointers for the product. I wish to learn Oscript but I have not found any documentation regarding the installation of software for coding in it.
Thanks!
Hi Kanika, thanks for your comment. Do you have access to the OpenText Knowledge Center? There you’ll find guides and forums to get you started.