JavaPath scripting language for accessing complex data structures

The article discusses the declarative scripting language JavaPath as an alternative to using Java Reflection and a way to avoid the "service hell" in standalone applications using complex data structures.


Description of the problem


Consider a deeply nested structure


class A{
   B b;
}
class B{
   C c;
}
class C{
   String name;
}

If we need to assign a value to the name field of class C without having direct access to instance A, then an intermediate layer of the service API usually comes to the rescue.


private A a;
public void setNameService(String name) {
    a.b.c.name = name;
}

, , . , .


private A a;
public void setNameService(String name) {
    if(a == null) {
        a = new A();
    }
    if(a.b == null) {
        a.b = new B();
    }
    if(a.b.c == null) {
        a.b.c = new C();
    }
    a.b.c.name = name;
}

, , setNameService(String name) , , a, b c, , .


API, , , API. . , . , , 'C' name, 'B' 'A' .


, - . , , , . - - . , . , , , , .


JavaPath , .


JavaPath


JavaPath.


private A a = new A();
private final JavaPath javaPath = new JavaPath(A.class);

public void setNameService(String name) {
    javaPath.evalPath("b.c.name", a, name);
}

setNameService.


, JavaPath β€” , Java. JavaPath . , . JavaPath . -public. . JavaPath private final .


: β€” JavaPath!


final String str = "VALUE";
JavaPath javaPath = new JavaPath(String.class);
assertEquals("VALUE",str);
//  private final byte[] value;  String 
javaPath.evalPath("value",str,"THE HACK".getBytes());
assertEquals("THE HACK",str);

- , , JavaPath Java Reflection .


, JavaPath . . .


JavaPath


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

evalPath


  • β€” Java
  • A β€” b
  • , . β€” β€” name

, . C. .


class C{
    String name;
    public void setName(String name) {
        this.name = name;
    }   
}

. .


A a = new A();
JavaPath javaPath = new JavaPath(A.class);
public void setNameService(String name) {
    javaPath.evalPath("b.c.setName", a, name);
}

JavaPath , , , :


    javaPath.evalPath("b.c.setName($)", a, name);

$ $0 β€” .



a0.a1.… .an



a0.a1.… .an-1


, , / null. an . , , , evalPath. , . JavaPath .


JavaPath


JavaPath , . . . , , , . , , ,
Java . , , , API.



Syntax Chart



JavaPath . . , , , . . , , , .. β€” .


$ #


Special parameters



, evalPath $[:digit:]* . $ , . .


.


JavaPath
"a($)",
"a($0)"
"a($1.name)"name,


. : #[:digit:]* # #0


.


JavaPath
"a(#)"
"a(#0.name)"name
"a.b(#1)"'a'

, -.


public class A {
    A parent;
    A child;
    String name;
    public A(A parent) {
        this.parent = parent;
    }
}

A a = new A(null);
JavaPath javaPath = new JavaPath(A.class);
javaPath.evalPath("name",a,"PARENT");
javaPath.evalPath("child(#0).name",a,"CHILD");
javaPath.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);

β€” .


"a.set('$0')" //  $0

InLine :


, JavaPath. , , . .


JavaPath
"a.b.c". , evalPath , .
"a.b.c($)", .
"a.b.c($0)"$0 $ .
"a.b.c('THE VALUE')"c 'THE VALUE' evalPath . .
"a().b().c",
"a().b.c".
"a.b(int 1024).c"
"a.b(Int 1024).c"Integer
"a.b(int 1024,' ').c",
a.setX(PhoneType CELL)enum PhoneType{HOME,CELL,WORK}, "CELL"

, , valueOf ( , enum ) .


, valueOf - , StringConverter, .



- null , . .


, , , . β€” . Java, .


"(T a).b"

: map .


public class A {
    Map<String,String> map;
}

A a = new A();
JavaPath javaPath = new JavaPath(A.class);
//       map
javaPath.evalPath("(HashMap map).put(firstName,$)", a, "John");
//    ,     .
javaPath.evalPath("map.put(lastName,$)", a, "Silver");

.


//  HashMap      .
//        0.8 
javaPath.evalPath("(HashMap map(int 100,float '0.8')).put(firstName,$)", a, "John");

, null. map .



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 javaPath = new JavaPath(A.class,classRegistry);

javaPath.evalPath("(map phones).put(PhoneType WORK)", a, new HashSet<>());
javaPath.evalPath("phones.computeIfAbsent(PhoneType HOME,key->new HashSet).@", a);
javaPath.evalPath("phones.computeIfAbsent(PhoneType CELL,key->new HashSet).@", a);
javaPath.evalPath("(map reversedPhones).@", a);

javaPath.evalPath("firstName", a, "John");
javaPath.evalPath("lastName", a, "Smith");

javaPath.evalPath("phones.get(PhoneType CELL).add", a, "1-101-111-2233");
javaPath.evalPath("phones.get(PhoneType HOME).add", a, "1-101-111-7865");
javaPath.evalPath("phones.get(PhoneType WORK).add", a, "1-105-333-1100");
javaPath.evalPath("phones.get(PhoneType WORK).add($)", a, "1-105-333-1104");

javaPath.evalPath("reversedPhones.put($,PhoneType CELL)", a, "1-101-111-2233");
javaPath.evalPath("reversedPhones.put($,PhoneType HOME)", a, "1-101-111-7865");
javaPath.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1100");
javaPath.evalPath("reversedPhones.put($,PhoneType WORK)", a, "1-105-333-1104");

@


@ . , . .


||


null, JavaPath . .


JavaPath
"getA∣∣setA($1).name($0)"getA null setA, getA
"getA∣∣init.name($0)"

::


, -, ::


JavaPath
"(UserInfo::newInstance userInfo).phone.ext"UserInfo UserInfo.newInstance()
"(UserInfo::newInstance userInfo(John,Smith)).phone.ext"UserInfo UserInfo.newInstance(String a, String b)
"a.b(Integer::valueOf 100).c". .
"(HashMap::new map).get"- new . .

ClassRegistry


, , .. ClassRegistry , JavaPath.


ClassRegistry . ClassRegistry, JavaPath.


PhoneType


ClassRegistry  classRegistry = new ClassRegistry();
classRegistry.registerClass(PhoneType.class,PhoneType.class.getSimpleName(),"Phone");
JavaPath javaPath = new JavaPath(A.class,classRegistry);

PhoneType , , Phone.


ClassRegistry . , :: .


. ClassRegistry . , StringConverter β€” .


public class A {
...
static {
    ClassRegistry.registerGlobalStringConverter(A.class,A::stringToA); 
}
public static A stringToA(String str) {
       A a = new A("{"+str+"}"); // - 
       return a;
    }
}

public class B {
    A a;
}

JavaPath javaPath = new JavaPath(B.class); //  A::stringToA

@PathElement @NoPathElement


@PathElement .


public class A {
    String name; //       "name"
    @PathElement({"name","first_name","firstName"})
    public void setName(String name) {
        this.name = name;
    }
}

, setName "name", . .


@NoPathElement JavaPath.


public class A {
    StringBuilder stringBuilder = new StringBuilder();

    @NoPathElement
    private final String protectedField = "IMMUTABLE BY JAVA PATH!";

    public void add(String str) {
        stringBuilder.append(str == null ? "N/A" : str);
    }

    @NoPathElement
    public void add(Object val) {
        stringBuilder.append(val);
    }
}

.





JavaPath . .


public class A {
    String firstName;
    String lastName;
    int age;
}

A a = new A();
JavaPath javaPath = new JavaPath(A.class);
javaPath.evalPath("firstName; lastName; age", a, "John","Smith",55);
// $       ,    
javaPath.evalPath("firstName($); lastName($); age($)", a, "John","Smith",55);
//         .
javaPath.evalPath("firstName($0); lastName($1); age($2)", a, "John","Smith",55);

'$', , . , firstName($) "John" ( , ), age($) β€” 55 ( , ). .


, , .


. .




JavaPath


, . , ? initPath
.


JavaPath . , . , , root. # #0


initPath .


:


//      JavaPath.  
Object instanceOfA = javaPath.initPath("(com.my.project.A #0).b", "test");
//      initPath
A instanceOfA = javaPath.initPath(A.class, "#.b", "test");
//       , #  #0
A instanceOfA = javaPath.initPath(A.class, "root.b", "test");




The setEnablePathCaching (boolean enableCaching) method of the JavaPath class allows you to save the result of the parser in the cache. Not to be confused with non-deactivable caching of field hierarchies and class methods. The path cache is disabled by default because it can lead to uncontrolled memory consumption if the paths are calculated dynamically.


Example - there will be three different paths in the cache:


evalPath("user.name('John'));"
evalPath("user.name('Peter'));"
evalPath("user.name('Mike'));"

Instead, use explicitly passing variable values. The bottom example will save one path.


evalPath("user.name($0)","John");
evalPath("user.name($0)","Peter");
evalPath("user.name($0)","Mike");

Dependencies


Java8 and older


Maven repository


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

All Articles