Overview

The PubServerSDK includes a set of classes, which allow dynamic method invocation based on Strings or XML configuration files.

This can serve several purposes:

These classes, namely the ParameterInterpreter and the ParameterParser class, make up the key technology to load and save placeholders, invoking Java methods from cscript, executing configuration check rules from ISON and others.

If you start implementing Java methods, which are supposed to be called via the ParameterInterpreter, it will be helpful to know some of the backgrounds and common pitfalls.

Requirements

The ParameterParser implementation is based on the Parboiled grammar parser, therefore the parboiled jars (currently parboiled-java-1.1.5.jar and parboiled-core-1.1.5.jar) are required.
These jars are included in the PubServer release resp. developer stack / Eclipse workspaces delivered for PubServer development.

The documentation is valid for any 4.0.5, 4.1.0 or later release of the Publishing Server and PubServerSDK.

Syntax / Call Convention

General Syntax

The general syntax for ParameterInterpreter calls is

    calltype(target)[parameters]
  

Name Mangling 

Given a class name, method name and a list of parameters, the ParameterInterpreter tries to find a method, which matches this call.
Due to automatic casts for some input types (namely number types and boolean), the result can be ambigous: more than one method can match the name and numbers of parameters defined in a command string.

It is important to know, that the ParameterInterpreter does not search for the best match, but invokes the first matching method found. The first match is the method, with

Unfortunately, the ParameterInterpreter is quit permissive regarding the "compatible types": any number can be cast to any other number or boolean type, strings can be cast to numbers and vice versa.

While in general this is a great gain for interoperability, this can cause problems, when methods of a class have similar signatures.

Similar in this context means:

Example:

public class MyUtils {
  public static String concatenate(String str, int v) {
    return "'" str + "', " + "#" + v;
  }

  public static String concatenate(int v, String str) {
    return "#" + v + ", '" str + "'";
  }

  public static String concatenate(int v1, int v2) {
    return "#" + v1 + ", #" + v2;
  }

  public static String concatenate(String s1, String s2) {
    return "'" + s1 + "', '" + s2 + "'";
  }
}

// Test:
String method       = "static(className='MyUtils',methodName='concatenate')";
String[] parameters = { "[17,17]", "['17','17']", "[17,'17']", "['17',17]" };

for (String parameter : parameters) {
  String commandString = method + parameter;
  Object result     = ParameterInterpreter.interpret(commandString, 
                                                     new HasMap<String,Object>());
  System.out.println(result);
}

/* 
 Result is not predictable! 
 All we can say is, that MOST PROBABLY the output will be the same for all runs.
 Beside of this, everything is possible:
  - #17'17'
  - '17'#17
  - '17''17'
  - #17#17
*/
It is impossible to predict, which of the similar methods will be called. It depends, in which order the class has been initialized by the Java class loader and can change after each restart of the application or even during runtime.

A good practice therefore would be

Classes with methods supposed to be called via the ParameterInterpreter should not define similar methods, i.e.: methods with equal names and the same numbers of parameters.

Plugin Method Call using Command String

A Plugin method call starts with the keyword plugin, followed by the call target.
The target is specified by The ParameterInterpreter requests the target Plugin from the PluginManager and dispatches the call to this Plugin bean.

Examples

// call using the Plugin name
String commandString = "plugin(globalName='" + 
                    Constants.MANAGER_ENTITY + 
                    "',methodName='getEntityRootBuckets')" + 
                    "[ /* ... parameters ... */]";
Object result = ParameterInterpreter.interpret(commandString, 
                                               new HashMap<String, Object>());
// call using the JNDI name
String commandString = "plugin(globalName='" +
                    "java:global/PubServerEntityManager/EntityManager!" + 
                    "com.priint.pubserver.plugin.interfaces.EntityManagerLocal" + 
                    "',methodName='getEntityRootBuckets')" + 
                    "[ /* ... parameters ... */]";
Object result = ParameterInterpreter.interpret(commandString, 
                                               new HashMap<String, Object>());

These calls are equivalent to

String sessionId = PluginControlDefault.getSessionId();
EntityManagerLocal entityManager = PluginUtils.getPlugin(Constants.MANAGER_ENTITY, 
                                                         sessionId, 
                                                         EntityManagerLocal.class);
List<Bucket> result = entityManager.getEntityRootBuckets(/* ... parameters ... */);

Plugin Method Call using PluginMethod Object

Instead of using a command string, the ParameterInterpreter can also be innstructed to invoke a method based on a reference to a PluginMethod object.

The PluginMethod object can be read from a config file (e.g. from a DataProvider config) or can be created and initialized in Java.

The equivalent call to the example shown above using PluginMethod would be:

PluginMethod method = new PluginMethod();
method.setPluginMappedName("java:global/PubServerEntityManager/EntityManager!");
method.setMethodName("getEntityRootBuckets");

method.addParameter(Parameter.createStringParameter(
                                      "sessionId", 
                                      PluginControlDefault.getSessionId()));

// ...

ParameterInterpreter.interpret(method, new HashMap<String,Object>());

Which way you prefer, probably depends on the situation: Using PluginMethods

In some situations, the following pattern may be useful:

String commandString = "plugin(globalName='" +
                    "java:global/PubServerEntityManager/EntityManager!" + 
                    "com.priint.pubserver.plugin.interfaces.EntityManagerLocal" + 
                    "',methodName='getEntityRootBuckets')" + 
                    "[ /* ... parameters ... */]";

PluginMethod method = ParameterInterpreter.parse(commandString);

// any modifications / evaluation of the method object

Object result = ParameterInterpreter.interpret(method, new HasMap<String, Object>());

Static Method Call using Command String

A Static method call starts with the keyword static, followed by the call target.
The target is determined by

Calling a static method via the ParameterInterpreter does not provide any extra service (such as lookup target etc.), but allows to to call any static method dynamically without having to fall back on Java reflection or similar.

Example

String commandString = "static(className='" +
                    "com.priint.pubserver.comet.util.PathUtils" + 
                    "',methodName='getClassByAbsolutePath')" + 
                    "[ /* ... parameters ... */]";
Object result = ParameterInterpreter.interpret(commandString, 
                                               new HashMap<String, Object>());

The equivalent static method call would look like this

Class<?> result = com.priint.pubserver.comet.util.PathUtils.getClassByAbsolutePath(
                                                    /* ... parameters ... */
                                                                                  );

Conclusion

Though the benfits of using the ParameterInterpreter may be not that obvious in the examples above, there are situations, when using the ParameterInterpreter is helpful or required:

Parameters

Parameters can be passed to a method call inline in the command string or via inline tags and corresponding values in a String / Object map. The types supported as inline parameters is limited to Java primitive types, such as String, Integers, Doubles etc.

All other types can be used, if passed as Map Arguments and using tags in the command string.

Inline Parameters

Inline parameters are notated directly in the command string as a comma separated list enclosed by [ ]. The following types are supported as inline parameters:

java.lang.String

Any character sequence enclosed by single quotes. Strings wont be decoded or encoded before being passed to the target method as parameters. This applies for escaped single quotes also, see examples below.

Examples:
String value Parameter notation Value passed to method
A String "...['A String']" A String
\0007\000F "...['\u0007\u000F']" \u0007\u000F (binary)
"...['\u20ac']"
double quotes (") in strings "...['double quotes (\") in strings']" double quotes (") in strings
single quotes (') "...['single quotes ('')']" single quotes ('')
back \ slash "...['back \\ slash']" back \ slash

java.lang.Number and derived Types

Any positive or negative short, byte, int, long, float or double value. Allowed characters are digits, one decimal point and the minus signs. Terms or expressions (e.g. 1 + 1) are not allowed.

Depending on the parameter type of the target method, values provided will be cast to a suitable Number type.
If a double value is expected, but "17" provided, the value will be cast to "17.0" and vice versa, if an integer is expected and "17.0" given, this will automatically be cast to "17".

Because of this behaviour, you should avoid methods with similar signatures. Consider the following situation:

public class Math {
  public static double max(double v1, double v2) {
    // ...
  }

  public static int max(int v, int v2) {
    // ...
  }

}
All of the following command strings send to the ParameterInterpreter will call any of these methods - just depending on in which order the Java class loader has initialized the methods:
String callstring = "static(className='Math',methodName='max')[17.0, 17.1]";
String callstring = "static(className='Math',methodName='max')[17, 17.1]";
String callstring = "static(className='Math',methodName='max')[17.1, 17]";
String callstring = "static(className='Math',methodName='max')[17, 18]";
See the Name Mangling section above for further examples and explanation.

java.lang.Boolean

Boolean values can be provided as Strings or Numbers. Value mapping is quite straight forward as listed below

Value description Parameter notation Value passed to method
Almost any String "...['17']" false
String equalsIgnoreCase("true") "...['tRuE']" true
Positive Integer "...[17]" true
Negative Integer "...[-17]" false
Positive Double "...[17.1]" true
Zero Integer or Double "...[0]" false

java.util.Map

Parameters of type Map<String, Object> can be encoded as inline parameters as follows:

Value types in a map can be mixed, furthermore, white spaces are allowed at any position of a map definition. The expected type of the corresponding method parameter is java.util.Map<?>.

Some examples

  String commandString = "plugin(...)[ { 'name' => 'Christoph', 'age' => 32 }]";

  String map = "{ 'recipe' => 'Cake', 'ingredients' => " +   
               "     {" +
               "       'sugar'  => 180.0," +
               "       'butter' => 250," +
               "       'flour'  => '1lb'" +
               "     } }";

Parameters provided as Map Arguments

All ParameterInterpreter.interpret methods accept two parameters

Instead of encoded inline, parameters can also be passed as Map Arguments. In this case, we use a placeholder ("tag") in the callstring and add an entry with the corresponding key to the arguments map.

The syntax for command string tags is

The corresponding key in the arguments map must include the <> brackets.

Example

public class Complex {

        public static int getChildCount(Bucket bucket) {
        // ...
        }
}

String commandString = 
        "plugin(globalName='Complex',methodName='getChildCount')['<bucket>']";

Bucket obj = new Bucket(); 
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("<bucket>", obj);

Object result = ParameterInterpreter.interpret(commandString, arguments);

Benefits

Mainly, there are two benefits when using arguments instead of inline parameters:

Drawbacks

The drawback of using arguments is of course, that the method call cannot be fully serialized to String.

API / Method Overview

ParameterInterpreter

com.priint.pubserver.parameterparser.ParameterInterpreter

interpret(PluginMethod method, Map<String, Object> args)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

interpret(PluginMethod method, Map<String, Object> args, List<Object> outParameters)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

interpret(String commandString, Map<String, Object> args)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

interpret(String commandString, Map<String, Object> args, List<Object> outParameters)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

interpret(String commandString, Map<String, Object> args, LogSettings logSettings)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

interpret(String commandString, Map<String, Object> args, LogSettings logSettings, List<Object> outParameters)

Call interpreter to run a publishing server method using a list of arguments. Log settings will be taken from method.

Returns Object, result of the ParameterInterpreter call

Parameters

parse(String commandString)

Parses a command string and return a Plugin method object representing this call.

This method is particularly useful, if you need information about the number and types of parameters passed to the method call or the name of the Plugin, class and method to be called.
The PluginMethod object returned may be modified and sent again to the ParameterInterpreter using the interpret(PluginMethod, Map, List) method.

Returns PluginMethod, result of the ParameterParser.parse call

Parameters

ParameterParser

com.priint.pubserver.parameterparser.ParameterParser

cacheParse(String input)

Retrieve a parsing result for the input string from the internal cache or create a new one.
If not found a new parse will be triggered and the result stored in the cache.

Returns list of Objects / parsing result

Parameters

clearParsingResults()

Clears the Parser cache

Returns -

Parameters

Parameters
-

getCacheSize()

Retrieves the size of internal cache for parsing results.

Returns size of cache

Parameters
-

parse(String input)

Runs a parse and returns the result as list of Objects.

Parsing errors will be written as warnings to the log.

This method blocks other threads from executing it until it returns. Expect single parses to return in less than 1 msec.

Returns list of Objects / parsing result

Parameters

Parser Grammar

The EBNF grammar for the command string syntax is as follows:

digit           = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" ;
naturalnumber   = digit | digit naturalnumber ;
positivenumber  = ([ naturalnumber ] [ "." ] naturalnumber) | 
                  (naturalnumber [ "." ] [ naturalnumber ]) ;
signednumber    = [ "-" ] positivenumber ;

character       = ? all characters ? ;
whitespace      = ? white space character ?

escapedquote    = "'" "'" ;
string          = "'" { character - "'" | escapedquote } "'" ;

tag             = "<" { character - ("<" | ">" ) } ">" ;

parameter       = signednumber | string | tag | map ;

key             = string ;
association     = "=" ">" ;
keyvalue        = key { whitespace } association { whitespace } parameter ;
keyvaluelist    = { whitespace } keyvalue { whitespace } [ "," whitespace keyvalue keyvaluelist ] ;
map             = "{" keyvaluelist "}" ;

assignement     = "=" ;
class           = ('globalName' | 'className') assignement string ;
method          = 'methodName' assignement string ;
(* no whitespaces allowed in target definition *) 
target          = "(" class "," method ")" ;

parameterlist   = { whitespace } parameter { whitespace } [ "," parameterlist ] ;
parameters      = "[" parameterlist "]" ;

command         = ('static' | 'plugin') target parameters ;