Extending JSONPath Support

This commit is contained in:
Fabio Sinibaldi 2022-02-15 19:02:00 +01:00
parent aac6bdf49f
commit b4478380ec
6 changed files with 368 additions and 30 deletions

View File

@ -44,7 +44,7 @@
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<version>2.7.0</version>
</dependency>
<dependency>

View File

@ -2,18 +2,23 @@ package org.gcube.application.geoportal.common.model;
import com.jayway.jsonpath.*;
import com.mongodb.util.JSON;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.gcube.application.geoportal.common.JSONSerializationProvider;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class JSONPathWrapper {
public static Configuration JSON_PATH_ALWAYS_LIST_CONFIG=null;
public static Configuration JSON_PATH_PATHS_CONFIGURATION=null;
@ -44,48 +49,176 @@ public class JSONPathWrapper {
@Getter
DocumentContext ctx=null;
DocumentContext valueCTX =null;
DocumentContext pathsCTX =null;
public JSONPathWrapper(String json) {
ctx=JsonPath.using(JSON_PATH_ALWAYS_LIST_CONFIG).parse(json);
valueCTX =JsonPath.using(JSON_PATH_ALWAYS_LIST_CONFIG).parse(json);
pathsCTX =JsonPath.using(JSON_PATH_PATHS_CONFIGURATION).parse(json);
}
public List<Object> getByPath(String path){
return ctx.read(path);
return valueCTX.read(path);
}
public List<String> getMatchingPaths(String path){return pathsCTX.read(path); }
public <T> List<T> getByPath(String path,Class<T> clazz){
List<T> l= ctx.read(path, new TypeRef<List<T>>() {});
List<T> l= valueCTX.read(path, new TypeRef<List<T>>() {});
l.removeIf(p->p==null);
return l;
}
public JSONPathWrapper set(String path, Object toSet){
ctx.set(path,toSet);
/**
* Changes the value of an existent field at @path
*
* @param path
* @param toSet
* @return
*/
public JSONPathWrapper setElement(String path, Object toSet){
log.debug("Setting Path {} = {}",path,toSet);
JsonPath jPath=JsonPath.compile(path);
if(jPath.isDefinite()){
valueCTX.set(path,toSet);
} else{
log.debug("Path is not definte, evaluating matching paths..");
for(String p : getMatchingPaths(path)){
setElement(p,toSet);
}
}
return this;
}
// public static final DocumentContext addElement(DocumentContext ctx,String path) throws JsonPathException{
// JsonPath jPath=JsonPath.compile(path);
// if(jPath.isDefinite()) {
// String parent=path.substring(0,path.lastIndexOf("."));
// List<String> found=ctx.read(parent);
// if(found==null || found.size()==0 || found.get(0)==null) {
// //missing parent, use recursion
// addElement(ctx,parent);
// }
// // found parent, adding element
// String element=path.substring(path.lastIndexOf(".")+1);
//
// Object value=new HashMap<String,String>();
// if(element.contains("[")) {
// value=new ArrayList<Object>(Collections.singletonList(value));
//
// element=element.substring(0,element.indexOf("["));
// }
//
// ctx.put(parent, element, value);
// return ctx;
// }else throw new JsonPathException("Unable to initialize non-definite path : "+path);
// }
/**
* Creates a new element @elementName == @toSet as child of all matching @path
*
* @param path
* @param elementName
* @param toSet
* @return
*/
public JSONPathWrapper putElement(String path,String elementName, Object toSet){
log.debug("Putting {} = {} at Path {}",elementName,toSet,path);
JsonPath jPath=JsonPath.compile(path);
if(jPath.isDefinite()){
valueCTX.put(path,elementName,toSet);
} else{
log.debug("Path is not definte, evaluating matching paths..");
for(String p : getMatchingPaths(path)){
putElement(p,elementName,toSet);
}
}
return this;
}
/**
* Appends a new element @toSet as child of all arrays matching @path
*
* @param path
* @param toAdd
* @return
*/
public JSONPathWrapper addElementToArray(String path, Object toAdd){
log.debug("Setting Path {} = {}",path,toAdd);
JsonPath jPath=JsonPath.compile(path);
if(jPath.isDefinite()){
valueCTX.add(path,toAdd);
} else{
log.debug("Path is not definte, evaluating matching paths..");
for(String p : getMatchingPaths(path)){
addElementToArray(p,toAdd);
}
}
return this;
}
//***** EXTENSIONS
private static Pattern DOTTED_PATH_PATTERN=Pattern.compile("(\\$?\\.\\w*(\\[((\\?\\(.*\\))|(\\d+|\\*))\\])?)(?=(\\.|$)?)");
private static Pattern BRACKET_PATH_PATTERN=Pattern.compile("(\\$?\\['\\w*'\\](\\[((\\?\\(.*\\))|(\\d+|\\*))\\])?)");
/* private static final DocumentContext addElement(DocumentContext ctx,String path,Object value) throws JsonPathException{
log.debug("Inserting object at {}",path);
JsonPath jPath=JsonPath.compile(path);
if(jPath.isDefinite()) {
List<String> tokens=tokenizePath(path);
DocumentContext currentCtx=ctx;
String parentPath = null;
for(int i = 0; i<tokens.size();i++){
String s= tokens.get(i);
log.trace("Managing {}",s);
if(parentPath== null) parentPath="$";
else parentPath +=s;
String elementName = extractFieldNameFromPathElement(s);
if(i==tokens.size()-1) {
// IF LEAF SET
log.trace("{} is leaf, setting as {}", s, value);
currentCtx.put("$",elementName,value);
}else {
// Intermediate element
List<String> foundElements = currentCtx.read(s);
foundElements.removeIf(v->(v==null));
if(foundElements.isEmpty()){
Document newElement=null;
log.trace("{} not found..");
// add child
if (s.matches("")){
log.trace("{} is array, checking existence",s,value);
// ARRAY
// if array exists add elements else create new
List<?> foundArray= currentCtx.read(elementName,List.class);
throw new RuntimeException("Implement array support");
} else {
// Map
newElement = new Document();
}
currentCtx.put("$",elementName,newElement);
log.trace("Set {}, json is {}",s,currentCtx.jsonString());
currentCtx= JsonPath.parse(newElement.toJson());
}else currentCtx = JsonPath.parse(currentCtx.read(s,Document.class));
}
} return ctx;
}else throw new JsonPathException("Unable to initialize non-definite path : "+path);
}
*/
static List<String> tokenizePath(String path){
List<String> toReturn=null;
log.debug("Tokenizing JSON Path {} ",path);
// if path is $.element.child
if(path.matches("(\\$\\.)?\\w.*"))
toReturn= getByRegex(path, DOTTED_PATH_PATTERN);
else if (path.matches("(\\$)?\\[\\'.*"))
toReturn = getByRegex(path, BRACKET_PATH_PATTERN);
log.debug("Path {} as tokens {}",path,toReturn);
return toReturn;
}
private static List<String> getByRegex(String s,Pattern p){
ArrayList<String> toReturn = new ArrayList<>();
Matcher m = p.matcher(s);
log.trace("Groups from {} with {} : ",s,p.pattern());
while(m.find()) {
String found=m.group();
toReturn.add(found);
}
return toReturn;
}
static String extractFieldNameFromPathElement(String p){
Matcher m=Pattern.compile("\\$?\\.?(\\[')?([a-zA-Z_]+)").matcher(p);
m.find();
return m.group(2);
}
}

View File

@ -8,6 +8,8 @@ import java.util.List;
@Data
public class LifecycleInformation {
public static final String DRAFT_PHASE="DRAFT";
public static final String PHASE="phase";
public static final String LAST_INVOKED_STEP="lastInvokedStep";
public static final String LAST_OPERATION_STATUS="lastOperationStatus";

View File

@ -0,0 +1,38 @@
package org.gcube.application.geoportal.common;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import java.util.EnumSet;
import java.util.Set;
public class JacksonProvider implements JSONSerializationProvider {
@Override
public void setJSONWrapperDefaults() {
Configuration.setDefaults(new Configuration.Defaults() {
private JsonProvider jacksonProvider = new JacksonJsonProvider();
private final MappingProvider mappingProvider = new JacksonMappingProvider();
@Override
public JsonProvider jsonProvider() {
return jacksonProvider;
}
@Override
public Set<Option> options() {
return EnumSet.noneOf(Option.class);
}
@Override
public MappingProvider mappingProvider() {
return mappingProvider;
}
});
}
}

View File

@ -0,0 +1,147 @@
package org.gcube.application.geoportal.common.model;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.bson.Document;
import org.gcube.application.geoportal.common.JSONSerializationProvider;
import org.gcube.application.geoportal.common.model.document.ProfiledDocument;
import org.gcube.application.geoportal.common.model.document.lifecycle.TriggeredEvents;
import org.gcube.application.geoportal.common.utils.Files;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import static junit.framework.TestCase.*;
public class JSONPathTests {
static File baseFolder=new File("../test-data/profiledDocuments");
private JSONPathWrapper profileNavigator() throws IOException {
return getNavigator("basicProfile.json");
}
private JSONPathWrapper getNavigator(String filename) throws IOException {
return new JSONPathWrapper(Files.readFileAsString(new File(baseFolder, filename).getAbsolutePath(), Charset.defaultCharset()));
}
private ProfiledDocument getDocument(String filename){
return null;
}
@Test
public void readElements() throws IOException {
}
@Test
public void writeTest() throws IOException {
JSONPathWrapper wrapper =getNavigator("emptyProfiledDocument.json");
checkSetValue(wrapper,"firstField","value1");
checkSetValue(wrapper,"$.firstField","value");
checkSetValue(wrapper,"$..event","fakeEvent");
checkSetValue(wrapper,"$['firstField']","value");
TriggeredEvents event=new TriggeredEvents();
event.setEvent("newEvent");
checkAddElement(wrapper,"$..triggeredEvents",event);
wrapper=getNavigator("basicProfile.json");
checkPutElement(wrapper,"$..[?(@.id == 'SDI-Default-Materializer')].configuration","additional",
Collections.singletonMap("myKey","myField"));
}
private void checkPutElement(JSONPathWrapper wrapper, String path, String elementName, Object value){
wrapper.putElement(path,elementName,value);
System.out.println("JSON is "+wrapper.getValueCTX().jsonString());
List<Map<?,?>> foundElements= wrapper.getByPath(path,Document.class);
assertTrue(foundElements!=null);
assertTrue(!foundElements.isEmpty());
foundElements.forEach(d->{
assertTrue(d.containsKey(elementName));
assertTrue(d.get(elementName).equals(value));
});
}
private void checkSetValue(JSONPathWrapper wrapper, String element, Object value){
wrapper.setElement(element,value);
System.out.println("JSON is "+wrapper.getValueCTX().jsonString());
List<Object> foundElements= wrapper.getByPath(element);
assertTrue(foundElements!=null);
assertTrue(!foundElements.isEmpty());
assertTrue(foundElements.get(0).toString().equals(value));
}
private void checkAddElement(JSONPathWrapper wrapper, String element, Object value){
int sizeBefore = wrapper.getByPath(element,List.class).get(0).size();
wrapper.addElementToArray(element,value);
System.out.println("JSON is "+wrapper.getValueCTX().jsonString());
List<Object> foundElements= wrapper.getByPath(element,List.class).get(0);
assertTrue(foundElements!=null);
foundElements.removeIf(f->f==null);
assertTrue(!foundElements.isEmpty());
assertTrue(foundElements.size()==sizeBefore+1);
}
@Test
public void testTokenizer(){
assertTrue(JSONPathWrapper.tokenizePath("$.firstField").size()==1);
assertTrue(JSONPathWrapper.tokenizePath("$.firstField.child").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$.firstField[3].child").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$.book[?(@.isbn)].child").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$.store.book[*].author").size()==3);
assertTrue(JSONPathWrapper.tokenizePath("$['firstField']").size()==1);
assertTrue(JSONPathWrapper.tokenizePath("$['firstField']['child']").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$['firstField'][3]['child']").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$['book'][?(@.isbn)]['child']").size()==2);
assertTrue(JSONPathWrapper.tokenizePath("$['store'].['book'][*].['author']").size()==3);
}
@Test
public void testGetFieldNameFromPathElement(){
assertEquals(JSONPathWrapper.extractFieldNameFromPathElement("$.firstField"),"firstField");
assertEquals(JSONPathWrapper.extractFieldNameFromPathElement("firstField"),"firstField");
assertEquals(JSONPathWrapper.extractFieldNameFromPathElement("$.firstField[3]"),"firstField");
assertEquals(JSONPathWrapper.extractFieldNameFromPathElement("$['book'][?(@.isbn)]"),"book");
}
@Test
public void testDefinitePath(){
assertTrue(JsonPath.isPathDefinite("$.firstField"));
assertTrue(JsonPath.isPathDefinite("$.firstField.child"));
assertTrue(JsonPath.isPathDefinite("$.firstField[3].child"));
assertFalse(JsonPath.isPathDefinite("$.book[?(@.isbn)].child"));
assertFalse(JsonPath.isPathDefinite("$.store.book[*].author"));
assertTrue(JsonPath.isPathDefinite("$['firstField']"));
assertTrue(JsonPath.isPathDefinite("$['firstField']['child']"));
assertTrue(JsonPath.isPathDefinite("$['firstField'][3]['child']"));
assertFalse(JsonPath.isPathDefinite("$['book'][?(@.isbn)]['child']"));
assertFalse(JsonPath.isPathDefinite("$['store'].['book'][*].['author']"));
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} | %logger{0}:%L - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.gcube.application" level="TRACE"/>
<logger name="com.jayway" level="DEBUG"/>
<root level="ERROR">
<appender-ref ref="STDOUT" />
</root>
</configuration>