Practical smartUI: Reading complex categories on the Content Server Side

Normally, when building REST Services for smartUI, you’ll have to read complex attributes from categories to send them for display to a widget.

Lets use this example

A complex Category with 4 Sets and Table Key Lookups
A complex category with multible sets and table lookups (click to enlarge)

In your REST handler, you r task would be to get the values of the category attached to a node, read them and send them to the client using REST.

In this writing, we’ll focus on the “Read them” part. Our trick is

object  LLNODE = $LLIAPI.LLNodeSubsystem.GetItem(node.pSubtype)
result = LLNODE.NodeCategoriesGet(node)

Thos gives us all Categories connected to a node.

When we receive the REST call in ther server handler, we should do

  1. find the node id of the the object to examine.
  2. find the categories attached to this node (Red Arrow in the Screenshot)
  3. iterate over all categories and get the attributes we want. This is based on
    1. the attribute definitions (Green Arrow on the Screenshot)
    2. the attribute values (Yellow Arrow on the Screenshot)
  4. if you encounter a set (-18 as attribute type), you also will have to iterate over the set (Red Rectangle)
Get the Values of a complex category
The REST Handler to get the Values of a complex category

Lets take a look on the Values of a single Category. They are inside an assoc with one entry per attribute id.

i

The Value list in CSIDE
The value list in CSIDE

Now you are almost done. Put a switch construct to get all values of the attributes on which you are intersted and get them out of the fdata assoc. The Value is found in the fdata assoc of the n-th Category unter the content of the first Value entry. Then we have to pick the attribute id where we find the content of the attribute in the first Value entry.

The Str,ValueTostring typecasts the output to be a string. So when we finished the job, we’ll see in the entry event.eventcycle the value of the attribute with the name “EventCycle”.

Get the attributes of interest
The switch construct to get the attributes of interest

The only thing to do is put the values in assocs and send them to the smartUI widget under the main clause “events” in the JSON array

Sent the values back
Send the values back. Support 200 (ok) and 500 (error) as status code.

So you see, Categories are no Monsters. They are quite user friendly. Using the same mechanism, you can even update complex Categories with one REST call.

Happy smartUI programming.

New: We are offering custom widget development. Interested? Send an e-mail at merz at ebit-company.de stating the purpose of the widget and requesting an offering

Quick List of all Categories and Attributes in a Content Server

Looking for a quick list of all Categories and Attributes configured in a Content Server? Tired of browsing the Categories Volume? Want to see all attrobutes and their IDs in a list?

Easy.

Simple use this line as your URL

<server><OT base><cgi>?func=attributes.dump

(p.ex. http://myserver/myContentServer/cs.exe?func=attributes.dump)

This will return this list:

The attribute.dump output
The attribute.dump output

Although this is a screenshot from a Contentserver 16.2.6, you can use this command at least back to Version 10.5.

 

Using the CSIDE Code Generation Wizard (1) – Create WebNode Action automatically

In the newer versions of CSIDE, you’ll find a code generation wizard, which writes code snipets for standard tasks automatically.

You can call this Code Generation wizard by clicking on the right mouse button.

Call of the Code Generation Wizard

Next, the Code Generation Wizard gives you a list of all templates, which can be used for the code generation. The amount of templates can vary from CSIDE version to CSIDE version.

List of all Templates to be used for the code generation

When you click on a template, you’ll get further info’s on the template. The amount of info’s can vary from template to template.

Infos on the template

Here in this post, we are using the “Create WebNodeAction” Command. Refer to future posts on examples of the other templates.

A WebNodeAction is a command, which is executed against a node by issuing a WebNodeCommand (in Contenxt/Node or other menus). For example, the “Open” command in a node menu is a WebNodeCommand, which issues the action for opening the node content.

After selecting “Create WebNodeAction”, we must provide two additional definitions.

Additional Infos to provide

First, we must select the package to put this command in. We simply select DELIVERER (the name of our module), but in practical situations, you should select a proper package.

And the name of the command has to be provided. After clicking “Finish”, a file with the name “MyTestCommand.os” will appear in the first level of our OSPACE.

The Command is generated as .os file

When you open the newly created file, you’ll see a complete command definition.

Command Definition Part 1

The fEnabled is set to true. The fWebScript has the default name of “mytestcommand.html” and there is an empty list of fPrototype.

Change the name of the fWebScript to the name you want. Add the Webingo parts of the commands output in this fWebScript.

If you use special parameters in the request, setup the proper fPrototype list.

The _Nodetypes() returns an undefined List, which should be changed immediately to reflect the node subtypes for which the command should be valid.

Command Definition Part 2

There is even a _SubclassExecute stub generated automatically. Add the action code to be executed in this _SubclassExecute stub.

Optional: Connect this WebNodeAction with a WebNodeCommand in a context or node menu or somewhere in the GUI to make it accessible for the end-user.

 

Calling OScript from Java Code (2)

In December 2016, we discussed how to call Java code from OScript . Now, we’ll discuss the other way round, how to call OScript from Java. This can be quite useful, if you want to use your business logic implemented in java against the content server.

The Java code must be put in the ojlib directory (see the previous post on this topic).

As always, if you want to deploy your java code within a module, do this

  • Build your code into jar files.
  • Add the jar files in OTHOME/ojlib/ or OTHOME/module/yourmodule_yourversions/ojlib/ directory (and their subdirectories) to be recognized by Content Serverk JVM’s application classloader.

The base thing is, you call OScript built-in functions through the OScriptObject.runScript method from a java coding.

For example, the java code

OScriptObject.runScript("echo","System.ThreadID()");

will display the unique integer for the current thread ID at the console (or in the logs, if you do not use CSIDE)

You can call all OScript functions and scripts. This example will list all nodes in the enterprise workspace

     // List nodes in the Enterprise workspace.
            result = (Map<String,Object>) OScriptObject.runScript( "$LLIAPI.NodeUtil.ListNodes",
				prgCtx,
				"(ParentID=:A1)",
				args );

prgCtx is the standard rogram Context, args is the ArrayList containing the arguments and A1 points to the first entry in the args array to be used as ParentID.

The next example can be used to get the current user from java coding, extract its userID and then derives the user name from ths user id. A standard logger is used to log the output, replace this with the logger of your preference.

    public static String getUserName( OScriptObject prgCtx )
    throws Exception
    {
        String retval = " user not found";
        
        try
        {        
            // get the user session object
            OScriptObject uSession = (OScriptObject) prgCtx.invokeScript( "USession" );
            // get the userID from the User Session
            Integer userID = (Integer) uSession.getFeature( "fUserId" );
            // display it
            logger.log( Level.INFO, "UserID is " + userID );
            
            retval = "The current login User: " + userID;
            
            // Get the Users name from the User ID
            Map<String,Object> status = (Map<String,Object>)
			OScriptObject.runScript( "$LLIAPI.UsersPkg.NameGetByID", uSession, 1000 );
			
            logger.log( Level.INFO, String.valueOf( status ) );
            logger.log( Level.INFO, (String) status.get( "Name" ) );
        }
        catch( Exception e )
        {
            logger.log( Level.SEVERE, "Caught Exception", e );
            throw e;
        }
        
        return retval;
    }

 

In the next posting on this topic, we’ll discuss the Mappings from JAVA to OScript and vice versa.

 

Execute a Livereport from OScript and get the results

From time to time, you may want to execute a livereport from an OScript function and get the results for further processing. This can be done quite easy.

First, you should have the node id of the report to execute.

In this scriptlet, the node id of this livereport to execute is nodeid

Second, you should setup a list with all input parameters. For each input parameter, create an assoc with inputType, label, prompt, textvalue and value of the parameter.

Add this assoc to the list of input parameters.

Store this list in the pExtendedData field of report node.

Object llnode = $LLiApi.LLNodeSubsystem.GetItem( $TypeReport )
Assoc data = llnode.ExecuteReport( prgCtx.fDBConnect, extdata )

Get the llnode of the report by calling the LLNodeSubSystem.GetItem. Execute the report by using llnode.ExecuteReport

The result of the livereport is in data.contents.

The whole call can look like this:

 node = DAPI.GetNodeById(prgCtx.DapiSess(),nodeid)
 if node.pSubType == $TypeReport // Is node a livereport?
     Assoc extdata = node.pExtendedData
     Assoc inp
    //Create input parameters assoc
     Assoc inp
     inp.inputType = "String"
     inp.label = "inputlabel1"
     inp.prompt = "DataID"
     inp.textValue = Str.String ( DataID )
     inp.value = Str.String ( DataID )
 
     //Attach inputs parameters list to extendeddata
     List inputs = {inp}
     extdata.inputs = inputs
     llnode = $LLiApi.LLNodeSubSystem.GetItem($TypeReport)
     Assoc data = llnode.ExecuteReport(prgCtx.fDBConnect, extdata)
     result.OK=true
     result.data = data.contents
     return result
end

 

Calling Java Code from OScript (1)

Sometimes it would be nice to use existing Java coding from a module instead of recoding this in OScript.

There is a facility in the content server which does exactly this bridging from OScript to Java, the so called JavaObject class. You’ll find the exact documentation in the “OScript API/Build-In Package Index”

In this first post of the series we’ll discuss the  basic calls from OScript.

First, you need some Java Code, compiled and in the form of a jar. Put this jar either in OTHOME/ojlib or (much better) in a ojlib directory in your module structure. After installing the module, the jar(s) are copied automatically to the OTHOME/ojlib. Then, the jvm classloader will find your jar(s).

From OScript it is possible to access static classes and instances.

Static
Dynamic InvokeStaticMethod(String classname, String methodName, List parameters)

Dynamic GetStaticField( String classname, String fieldName)

Dynamic SetStaticField( String className, String  fieldName, Dynamic value)

The return values are either Error or Dynamic if the call is successful.

Dynamic
JavaObject New (String className, List parameters)
Instance
Dynamic GetField(String fieldname)

Dynamic SetField(String fieldname, Dynamic value)

Dynamic InvokeMethod(String methodName, List parameter)

The return values are either Error or Dynamic if the call is successful.

An example
function void javaTest()

   JavaObject myObject

   myObject = JavaObject.new("my.own.package.class")
 
   Dynamic res = myObject.InvokeMethod("myMethod",{"aa","bb})

   if (isError(res))

     echo ("Init failed")

     return

   end

   Dynamic res1 = myObject.GetField("myResult")

   if (IsError(res)) 

     echo("Calculation failed")

     return

   end

   echo("The result is "+res1)

end

In the next post we’ll discuss how to get the JNI exceptions and the error stack from the jvm.

 

Checking for the membership in a Group (OScript)

From time to time you have to check, if your user is member of a defined group. The UAPI.RightsListByID returns a list of all groups, in which the specified user is a member (directly or indirectly through membership in another group).

You can use something like this snipped in your function.

object uSession = request.prgCtx.fUSession 

dynamic uGroups 

if isDefined(uSession) && !isError(uSession)

 uGroups = UAPI.RightsListByID(uSession.fSession, uSession.fUserId)

 if isError(uGroups) 

   dynamic check = error.ErrorToString(uGroups) 

  end 

 end 

The Dynamic check contains the RecArray of groups, in which the current user is a member.  This snipped is intended to be run from a request handler.

 

Making Configuration Values Cluster Safe

Normally, there is a file called “opentext.ini” in the config directory of your Content Server instance. This file contains a lot of configuration values.

A content server reads this file at boot time.

But, if you use a cluster of Content Servers, its a problem changing all “opentext.ini” files for a configuration value. And rebooting them will use a lot of down-time.

In a cluster, there is another option to store configuation values, the “KIni Table” in the database. This values are read immediately from the content server(s) and do not require a reboot.

From OScript, use the database storage like this

Retrieve a value (from the “MySoftware” Section)- Default Value 50, if this is not existing)

result = CAPI.IniGet(dbConnect.fLogin, ‘MySoftware’,‘StartJob’, 50)

Set a value ( sets StartJob value of MySoftware to 50)

result = CAPI.IniPut(dbConnect.fLogin, ‘NySoftware’,‘StartJob’, 50)

List all configuration values registered under MySoftware

result = CAPI.IniList(dbConnect.fLogin, ‘MySoftware’)

Delete a configuration value (deletes StartJob value from MySoftware)

result = CAPI.IniDelete(dbConnect.fLogin, ‘MySoftware’, ‘StartJob’)

 

The dbConnect.fLogin is the CAPICONNECT(the login object) to the database.

Upload a file into the content server from OScript

If you want to upload a file into the content server from your module, you can use something like this

function Assoc UploadFile(Record r, Integer FolderID, String fieldName)
    Dynamic retNodeID = -1 
    DAPINODE docNode 
    // Get the ATTACHMENT_FOLDER node and set some options.
    // ParentID = 2000 (Enterprise) 
    Object prgSession = .PrgSession()
    Object session = prgSession.DSession()
    DAPINODE nodeFolder=    DAPI.GetNodeByID(session.fSession,DAPI.BY_DATAID,FolderID,False )
    // Make the node name from the file and eventID. It has to be unique, so
   // check it, and append a number if necessary.
    String fname=$WebDoc.WebDocUtils.GetFileUploadName(r,fieldName)
    Assoc retUniq
    String sTmp=fname
    Integer cnt=0
    retUniq=$LLIApi.NodeUtil.IsNameUnique(sTmp,nodeFolder)
    while !(retUniq.OK)
       cnt+=1
       sTmp=fname+"_"+Str.ValueToString(cnt)
       retUniq=$LLIApi.NodeUtil.IsNameUnique(sTmp,nodeFolder)
    end
    fname=sTmp
    // Upload the document as a new node.
    Assoc result = $WebDoc.WebDocUtils.CreateFileUploadDocument(nodeFolder,r,fieldName,fname)
    return(result) 
  end

First, the folder ID is used to get the DAPINODE of the parent folder. The node name must be unique, so a check is necessary to ensure, that the intended nodename is not existing.

Then the file is uploaded.

Remark: This function does not consider mandatory categories, which must be added at upload time.

Building a Content Server Request Handler for AJAX Client Requests

In the standard request handler usage scenarios, there is always a complete URL, which is send to the server. Then the server processes that and returns the result page to the client.

With a little change, these standard request handlers in a module can be modified to be used from clients asynchronously by the usage of AJAX.

Client Part

The easiest approach is to use AJAX with JQUERY (which is delivered by the content server). If the base page was built from another request handler, it can include weblingo constructs. These are OSCRIPT parts, delimited by `-Delimiters. Like JSP, the final renderer evaluates everything between two delimiters and replaces that with the result of the evaluation.

So a AJAX JQUERY command with intermixed OSCRIPT parts can look like that:

This is a simple AJAX call, calling the request handler on the server as usual. But there is a parameter called func, which contains the actual request handler address and one parameter nodeid, which is the data parameter.

Request Handler
AJAX Part

Server Part

A sample request handler looks like this listing below. There is a prototype declared, this prototype must exist in the request.

Server Request Handler
Server Request Handler

The rest of the data is encapsulated in the JSON array. This array can be parsed into an assoc with the call to

$WebLL.JSONUtils.ParseJSON(request.configData)

and processed in the standard way.

Any responses should be handled in the standard way with an assoc.

String tst = $WebLL.JSONUtils.ToJSON(response)
  Web.Write(ctxOut, $WebLL.JSONUtils.GetJSONHeader(.fExtraHeaders))
  Web.Write(ctxOut, tst);
  .fHTMLFile = Undefined

This assoc is changed with a call to the ToJSON to a normal string.

Here, we do not have a output html file, so we have to use a Web.Write to write the output directly to the port. Do not forget to include the JSONHeaders.

The fHTMLFile stores normally the weblingo output file and has to be set to undefined in this case.

package DELIVERER::PUBLICATOR::AJAXHANDLER

public object UpdateAjax inherits DELIVERER::RequestHandlerOrphans::LLRequestHandler

override Boolean fEnabled = TRUE
  override String fFuncPrefix = 'deliverer'
  override Dynamic fHTMLFile
  override List fPrototype = { { 'configdata', -1, 'The config data changed at the client', FALSE } }
  /*
  * This is the Request handler to handle the Ajax Update Request(s)
  * Based on the original Request handlers, this handler receives an AJAX Request from the NodeAction Browse Page
  * and sends the Response back to the Browse Page (PUBLICATOR.html)
  *
  * @param {Dynamic} ctxIn
  * @param {Dynamic} ctxOut
  * @param {Record} request
  *
  * @return {Dynamic} Undefined
  */
  override function Dynamic Execute( \
  Dynamic ctxIn, \
  Dynamic ctxOut, \
  Record request )

Object prgCtx = .PrgSession()
  Assoc response = Assoc.CreateAssoc()
  Assoc cDat = $WebLL.JSONUtils.ParseJSON(request.configData)
  integer nodeid = cDat.id
  DAPINode node = DAPI.GetNodeByID(prgCtx.DapiSess(),nodeid)
  Assoc.Delete(cDat,"id") // remove the NodeID
  string remark = cDat.remark
  Assoc.Delete(cDat,"remark");
  node.pComment=remark
  // save configdata in pExtendedData
  // do not forget to include other assocs, if needed from the pExtendedData
  Assoc a = node.pExtendedData;
  //TODO Add Things
  a.configdata=cDat
  node.pExtendedData = a

DAPI.UpdateNode(node) // store it

// Send response
  response.ok="OK"
  response.msg="Fine"
  String tst = $WebLL.JSONUtils.ToJSON(response)
  Web.Write(ctxOut, $WebLL.JSONUtils.GetJSONHeader(.fExtraHeaders))
  Web.Write(ctxOut, tst);
  .fHTMLFile = Undefined

return Undefined
  end

end