Home - aegisql/java-path GitHub Wiki

JavaPath

JavaPath is a very simple inline script language and toolkit for deep Java object initialization and evaluation.

NOTE: This is an early pre-release version of JavaPath and documentation. Might not reflect the latest changes. JavaPath syntax is also under development.

Dependency.

Check for latest version at the Maven Repository

<dependency>
    <groupId>com.aegisql</groupId>
    <artifactId>java-path</artifactId>
    <version>0.1.3</version>
</dependency>

The README file contains build instructions.

Formal syntax

The latest version of the JavaPath syntax - JavaPath.jjt

What problem JavaPath solves?

In weakly coupled applications you often need to evaluate some randomly parametrized chain of method calls or deliver a piece of information to certain destination, which can be deeply hidden in a very complex data model. Practically speaking, this destination is a field,
container or setter method in a nested class hierarchy. Example of such applications - a command pattern, a message driven application, actor system, or a micro-service, the idea is, sender of the data is aware about data structure, but he has no direct access to the data and knows nothing about the object state on the recipients side. In order to successfully assign a value to the desired field, or access certain method in the chain, all intermediate fields must exist and all getters must return right non null values.

Let's consider a simple example:

We have the following POJO class hierarchy. Class "B" has a field of class "A", which has a String field "name"

class A {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
}

class B {
    private A a;
    public void setA(A a) {
        this.a = a;
    }
    public A getA() {
        return a;
    }
}

If you need to set a name in the "a" field you have to write something like this:

private B b = new B();

....
public void setNameService(String name) {
    if(b.getA() == null) {
        b.setA(new A());
    }
    b.getA().setName("John");
}

A bit of work for every data branch you have to support, and the longer the branch, the more job has to be done to keep code safe and sound.

What if we can automate the process? With Java Path you can script the initialization in a declarative style.

B root = new B(); //Instance of the root object
JavaPath javaPath = new JavaPath(B.class); //JavaPath needs a root class to build the model

....
public void setNameService(String name) {
    javaPath.evalPath("getA.setName", root, name);
}

What is happening inside?

JavaPath uses Java Reflection API to build a deep model of the root class. JavaPath language should be easy to understand to any Java developer, because it resembles very much traditional method or field call chaining in java.

//Java method chain call
b.getA().setName(name);
//identical JavaPath
"getA().setName($)"
//or even shorter
"getA.setName"
//and, in many cases, you do not need any methods to reach the fields
"a.name"

Parser splits the query into tokens separated by 'dots' and then consider every token as either a field, or a method name in the call hierarchy. If field is missing, JavaPath will make some attempts to instantiate it, using default or parametrized constructors, existing or provided by user factory methods.

Limitations

Let's talk about limitations. JavaPath is a language for inline deep data instantiation and chain evaluation. It has no controls, such as "if" or "for" operators in Java. [Almost] everything you can achieve by calling chained methods in Java can be also achieved by writing a JavaPath query. But, if you have to make any decision in the middle of this chain, it must be hidden inside method calls. Other limitation is the same why we not use call chaining in Java every time. If method calls must be parametrized, and those parameters are not trivial values, requiring some not trivial instantiation by themselves, then the chained call becomes a heavy and fragile construction we try to avoid.

Some terminology

  • The Path. Dot delimited string where each element represents field, getter or setter methods that can be found in the actual Java call chain.
  • Type. Fully-qualified Java class name or class name short alias. e.g. java.lang.Integer can be also referred as Integer, Int or I
  • Parameters. Values we pass to the path for evaluation. Simple parameters can be inlined in the body of the path, or they can be provided directly to the evalPath or initPath methods of the JavaPath API.
  • Getters. We call a getter any method that returns any value other than 'void' or primitive type
  • Setters. We call a setter any method, regardless of return type and number of parameters. Thus, any getter can be a setter too.
  • Fields. Think of a field as about getter and setter method at the same place. Field can return its own value, and it can be assigned a value. Instantiation of the field can be parametrized.

In the call path

a0.a1...an

All elements

a0.a1...an-1

must be getters, i.e. return some values of non-primitive types, or fields. The last element an is always a setter or a field. Even if it returns any value it can be ignored.

Syntax description

Basics

Syntax diagram

Java Path Syntax Diagram

Fields


class A {
    String name;
}
path description
"name" Most trivial use case. Access field of the root element by it's name.
"name($)" Reference to a single passed parameter (i.e "John" in the example) not required
"name()" Empty parentheses not required and can be omitted
"name($0)" $ === $0

First thing, since the "name" is the last (and only) path element in the path, we conclude that it is a setter or a field. JavaPath treats methods and fields the same way. Field just a setter and getter for itself. All four paths are equivalent. You can use any style suitable for your situation. Simple "name" better highlights the idea that you access the field, for parameter-less methods, such as POJO getters you might find useful using empty parentheses.

If you pass just one parameter for the evalPath, and setter requires a single parameter, or, passed parameter is the last in the setters' parameter list, then you do not need to mention it as well. If you prefer, however, you can refer the parameter as $ or $0.

Usage example:

A a = new A();
JavaPath jp = new JavaPath(A.class);
jp.evalPath("name", a, "John");

//same as direct Java call 
a.name = "John";

class A {
    String name;
}
class B {
    A a;
}
path description
"a.name" Root object of class 'B' contains field 'a' of class 'A', which has a field 'name'

Usage example:

B b = new B();
JavaPath jp = new JavaPath(B.class);
jp.evalPath("a.name", b, "John");

//same as
b.a.name = "John";

Methods


class A {
    String name;
    public void setName(String name) {
        this.name = name;
    }
}
path description
"setName" Use setter name without parentheses and parameters
"setName($)" Use setter name with parentheses and value placeholder $ or $0

Usage example:

A a = new A();
JavaPath jp = new JavaPath(A.class);
jp.evalPath("setName", a, "John");

//same as
a.setName("John");

Annotations


There are two annotations in the JavaPath library that helps manage names used in the path statements

Annotations can be applied to fields and methods

//used to alternate name by which field or method is visible by the JavaPath
@PathElement("name"[,more names]) 

//excludes name from visibility. Helps to resolve name conflicts
@NoPathElement

class A {
    String name;
    @PathElement("name","first_name")
    public void setName(String name) {
        this.name = name;
    }
}
path description
"name" "name" overlaps with corresponding field name. Labeled method will have priority over the field
"first_name"
"setName" Note that original name still can be used

Usage example:

A a = new A();
JavaPath jp = new JavaPath(A.class);
jp.evalPath("name", a, "John"); //JavaPath will call setName

class A {
    StringBuilder stringBuilder = new StringBuilder();
    public void add(String str) {
        stringBuilder.append(str == null ? "N/A" : str);
    }
    @NoPathElement
    public void add(Object val) {
        stringBuilder.append(val);
    }
}
path description
"add" Sometimes JavaPath cannot make a choice between two methods with the same name and mutually assignable parameter types. Then the @NoPathElement annotation helps to exclude some conflicting methods from consideration

Usage example:

A a = new A();
JavaPath jp = new JavaPath(A.class);
jp.evalPath("add", a, null);

//same as
a.add((String)null);
//and this is a compilation error
a.add(null);

Types

Path elements have types. Either types of fields, or corresponding return types of methods. Instantiation of the path element can become a problem if those types are interfaces or abstract classes. In this case you have to precede path element with exact expected type that can be instantiated in case the value of the path element is missing at the evaluation time. Pair type+path element must be surrounded by parentheses.

public static class A {
    //Map is an interface and cannot be automatically instantiated 
    Map<String,String> map;
}
path description
"(HashMap map).put(firstName)" JavaPath cannot instantiate field map without a hint
"(java.util.HashMap map).put(firstName)" fully qualified name can be used, but it is usually too long
"(map map).put(firstName)" and can be substituted with a shorter alias. JavaPath sets some short names for popular types
"(map map).put(firstName,$)" Although $ is not required, it helps to understand better what kind of method the 'put' is

In example above type java.util.HashMap and its short aliases already included into the library, but, you can register your own classes and short names for them.

interface A {
    String getName();
}
public static class AImpl implements A {
    String name;
    public String getName() {
        return name;
    }
}
public static class B {
    A a;
}
...
B b = new B();
ClassRegistry classRegistry = new ClassRegistry();
classRegistry.registerClass(AImpl.class,"AImpl");
JavaPath pathUtils = new JavaPath(B.class,classRegistry);
pathUtils.evalPath("(AImpl a).name", b, "John"); // A a will be properly instantiated

Available aliases

Class name Short alias
java.util.ArrayList ArrayList, list
java.util.LinkedList LinkedList, linkedlist
java.util.HashMap HashMap, map
java.util.TreeMap TreeMap, treemap
java.util.HashSet HashSet, set
java.util.TreeSet TreeSet, treeset

Parameters

Call Parameters

Calls of path elements - Getters, Setters and even Fields can be parametrized.

public static class A {
    StringBuilder stringBuilder;
}
path description
"stringBuilder(John).append(' ').append" Field can be initialized with parametrized constructor or static factory method

Usage example:

A a = new A();
JavaPath pathUtils = new JavaPath(A.class);
pathUtils.evalPath("stringBuilder(John).append(' ').append", a, "Silver");

//same as
a.stringBuilder = new StringBuilder("John").append(" ").append("Silver");

More details

path description
"a(val)" Trivial parameter expects that method a can accept String as a parameter
"a('val with space')" If you need to include spaces, "$",",", ".", "(",")", "'",""" or "\"
"a(\"val with escape \" \' \\ \")" use singe or double quotes

You can use multiple parameters, if the method or constructor API require it.

path description
"a(val1,val2,val3).b($,val4)" Multiple parameters for methods a and b. Note that $ sign is now required, because it is not the last required parameter for b

Parameter Type

Parameters have types. Default type for the parameter is String. How JavaPath deals with not string parameters? You need to specify the parameter type or alias.

"pathElement(ParamType paramValue)"
//possible Java equavalent
// Constructor with a String parameter
ParamType paramInstance = new ParamType(paramValue);
//Factory method
ParamType paramInstance = ParamType.valueOf(paramValue);

//if pathElement is a method:
root.pathElement(paramInstance);
//if pathElement is a field:
root.pathElement = paramInstance;

JavaPath will find that parameter type is different from String and will try to instantiate the parameter from available string representation. Attempts will be made in the following order:

  • Provided by user String Converter for the type
  • Constructor with a single String parameter
  • Static factory method 'valueOf(String val)' - present in many standard Java classes, such s Integer, Enumerated types, etc.

Example:

There is no need to specify StringConverter for the PhoneType, because all enums in Java inherit static factory valueOf(String value) If you develop your own class you can also implement the valueOf method. It will help to keep code simple.

enum PhoneType{HOME,CELL,WORK}
public static class A {
    String firstName;
    String lastName;
    Map<PhoneType, Set<String>> phones;
    Map<String, PhoneType> reversedPhones;
}
//...
A a = new A();
ClassRegistry  classRegistry = new ClassRegistry();
classRegistry.registerClass(PhoneType.class,PhoneType.class.getSimpleName());
JavaPath pathUtils = new JavaPath(A.class,classRegistry);
//init objects
//pass value explicitly
pathUtils.evalPath("(map phones).put(PhoneType WORK)", a, new HashSet<>());
//or let the Map method do the job
pathUtils.evalPath("phones.computeIfAbsent(PhoneType HOME,key->new HashSet).@", a);
pathUtils.evalPath("phones.computeIfAbsent(PhoneType CELL,key->new HashSet).@", a);
pathUtils.evalPath("(map reversedPhones).@", a);
pathUtils.evalPath("firstName", a, "John");
pathUtils.evalPath("lastName", a, "Smith");
pathUtils.evalPath("phones.get(PhoneType CELL).add", a, "1-101-111-2233");
pathUtils.evalPath("phones.get(PhoneType HOME).add", a, "1-101-111-7865");
pathUtils.evalPath("phones.get(PhoneType WORK).add", a, "1-105-333-1100");
// Dollar sign is not required if it is the last or the only parameter of the method
pathUtils.evalPath("phones.get(PhoneType WORK).add($)", a, "1-105-333-1104");
// Dollar sign is required because it is not the last of two required parameters
pathUtils.evalPath("reversedPhones.put($,PhoneType CELL)", a, "1-101-111-2233");
pathUtils.evalPath("reversedPhones.put($,PhoneType HOME)", a, "1-101-111-7865");
pathUtils.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1100");
pathUtils.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1104");

If necessary, you can provide your own StringConverter.

class A{
public static A getInstance(String value) {
    A instance = new A();
    // Do some fancy stuff
    return instance;
}
}
ClassRegistry classRegistry = new ClassRegistry();
classRegistry.registerStringConverter(A.class,A::getInstance); 

Predefined type aliases

ClassRegistry comes pre-loaded with aliases for popular types

Class name aliases
java.lang.String String, string, str, s
java.lang.Integer Integer, Int, I
java.lang.Long Long, L
java.lang.Byte Byte, B
java.lang.Boolean Boolean, Bool
java.lang.Character Character, Char, Ch, C
java.lang.Short Short
java.lang.Double Double, D
java.lang.Float Float, F

primitive types

type aliases
int int, i
long long, l
byte byte, b
bool bool
char char, c, ch
short short
double double, d
float float, f

Other types

Class name Short alias
java.util.ArrayList ArrayList, list
java.util.LinkedList LinkedList, linkedlist
java.util.HashMap HashMap, map
java.util.TreeMap TreeMap, treemap
java.util.HashSet HashSet, set
java.util.TreeSet TreeSet, treeset
java.util.Function new, key->new

Back References

Value of the root object and all values returned by getters can be used as parameters in the next calls in the path chain. Format of the back reference - #[:digit:]* Root object is #0 or just #

In the example

jp.evalPath("a(#0).b(#1).c(#2)", obj, "VALUE");
// obj -> #0 or #
// a -> #1
// b -> #2
// c -> #3

In the path element you can refer any preceding path element or the root object, but you cannot refer current or following element.

Example

public static class A {
    A parent;
    A child;
    String name;
    public A(A parent) {
        this.parent = parent;
    }
}
...
A a = new A(null);
JavaPath pathUtils = new JavaPath(A.class);
pathUtils.evalPath("name",a,"PARENT");
pathUtils.evalPath("child(#0).name",a,"CHILD");
pathUtils.evalPath("child(#0).child(#1).name",a,"GRAND-CHILD");

assertEquals("PARENT",a.name);
assertEquals("CHILD",a.child.name);
assertEquals("GRAND-CHILD",a.child.child.name);
assertEquals("CHILD",a.child.child.parent.name);
assertEquals("PARENT",a.child.child.parent.parent.name);

Once obtained, back reference creates its own java path branch. So, you can use not only the reference itself, but also it's fields and methods.

path description
"a.b(#1)" b will be parametrized with value stored in field 'a' or returned by method a()
"a.b(#1.x.y)" b will be parametrized with value stored in a.x.y
"a.b('#1.x.y')" b will be parametrized with a string value #1.x.y 😀
"a.b(#1.x(int 100).y)" those calls can be parametrized, but this version does not have any support for secondary back references.

Multiple Values

In previous examples we applied only one parameter to the last element in the path. You can pass as many parameters as you need. In this case they must be explicitly indicated in the path element parameter list. Format of the value reference - $[:digit:]* First value is $0, etc. If you have only one parameter, 0 in $0 can be omitted.

Example:

enum Relation {FATHER,MOTHER}
enum Field{FIRST_NAME,LAST_NAME,AGE}

public static class A {
    Map<Relation,Map<Field,Object>> map;
}
...
A a = new A();
JavaPath pathUtils = new JavaPath(A.class);
pathUtils.evalPath("(HashMap map).computeIfAbsent($0,key->new HashMap).put($1,$2)", a, FATHER, FIRST_NAME,"John");
pathUtils.evalPath("(HashMap map).computeIfAbsent($0,key->new HashMap).put($1,$2)", a, MOTHER, FIRST_NAME,"Ann");
// $0 -> FATHER,MOTHER
// $1 -> FIRST_NAME
// $2 -> "John","Ann"

Choice operator

What if getter returned you null? Classic POJO implementations do not guarantee that getters always return values. You need to call setter first, or do some other kind of initialization.

Operator || (read as 'OR') allows specifying path element that will be executed once if corresponding getter returned null. After that second, and last, attempt will be made to call the getter.

"getValue||init.doSomething"

API

Class Registry

Simplify instantiation of custom types from the path string

JavaPath API

Constructors

public JavaPath(Class<?> aClass, ClassRegistry registry)
public JavaPath(Class<?> aClass)

evalPath

public Object evalPath(String path, Object root, Object... values)

Returns the value returned by the last path element call in the chain.

initPath


Usages

JavaPath is used in the Conveyor project. The SimpleConveyor uses JavaPath as message labels to deliver values into a complex builder structure.


⚠️ **GitHub.com Fallback** ⚠️