Browse Source

Jakarta EE 9 JSON-P/JSON-B provider (#734)

* Add support for JSON-P API (JSR 374)

* Add support for JSON-B API (JSR 367)

* Fix Jakarta EE 9 breaking unit tests

* Remove Import-Package instr for JSON-P/JSON-B implementations

* Fix whitespace in unit test classes

* Proxy JSON-P objects and arrays to add mutability

* Update project README for Jakarta JSON providers

Co-authored-by: Leonid Malikov <leonid@percival.co.uk>
pull/469/head
Leonid 3 years ago committed by GitHub
parent
commit
be070ec2c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      README.md
  2. 5
      build.gradle
  3. 16
      json-path/build.gradle
  4. 1033
      json-path/src/main/java/com/jayway/jsonpath/spi/json/JakartaJsonProvider.java
  5. 636
      json-path/src/main/java/com/jayway/jsonpath/spi/mapper/JakartaMappingProvider.java
  6. 23
      json-path/src/test/java/com/jayway/jsonpath/BaseTest.java
  7. 10
      json-path/src/test/java/com/jayway/jsonpath/Configurations.java
  8. 4
      json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java
  9. 381
      json-path/src/test/java/com/jayway/jsonpath/JakartaJsonProviderTest.java
  10. 21
      json-path/src/test/java/com/jayway/jsonpath/ProviderInTest.java

16
README.md

@ -248,7 +248,7 @@ String json = "{\"date_as_long\" : 1411455611975}";
Date date = JsonPath.parse(json).read("$['date_as_long']", Date.class);
```
If you configure JsonPath to use `JacksonMappingProvider` or `GsonMappingProvider` you can even map your JsonPath output directly into POJO's.
If you configure JsonPath to use `JacksonMappingProvider`, `GsonMappingProvider`, or `JakartaJsonProvider` you can even map your JsonPath output directly into POJO's.
```java
Book book = JsonPath.parse(json).read("$.store.book[0]", Book.class);
@ -440,11 +440,11 @@ JsonPath is shipped with five different JsonProviders:
* [JacksonJsonProvider](https://github.com/FasterXML/jackson)
* [JacksonJsonNodeJsonProvider](https://github.com/FasterXML/jackson)
* [GsonJsonProvider](https://code.google.com/p/google-gson/)
* [JsonOrgJsonProvider](http://www.json.org/java/index.html)
* [JsonOrgJsonProvider](https://github.com/stleary/JSON-java)
* [JakartaJsonProvider](https://javaee.github.io/jsonp/)
Changing the configuration defaults as demonstrated should only be done when your application is being initialized. Changes during runtime is strongly discouraged, especially in multi threaded applications.
```java
Configuration.setDefaults(new Configuration.Defaults() {
@ -470,6 +470,16 @@ Configuration.setDefaults(new Configuration.Defaults() {
Note that the JacksonJsonProvider requires `com.fasterxml.jackson.core:jackson-databind:2.4.5` and the GsonJsonProvider requires `com.google.code.gson:gson:2.3.1` on your classpath.
Both of Jakarta EE 9 [JSON-P (JSR-342)](https://javaee.github.io/jsonp/) and [JSON-B (JSR-367)](http://json-b.net/) providers expect at least Java 8 and require compatible JSON API implementations (such as [Eclipse Glassfish](https://projects.eclipse.org/projects/ee4j.jsonp) and [Eclipse Yasson](https://projects.eclipse.org/projects/ee4j.yasson)) on application runtime classpath; such implementations may also be provided by Java EE application container. Please also note that Apache Johnzon is not classpath-compatible with Jakarta EE 9 specification yet, and if JSON-B mapping provider is chosen then JSON-P provider must be configured and used, too.
One peculiarity of Jakarta EE 9 specifications for JSON processing and databinding (mapping) is immutability of Json arrays and objects as soon as they are fully parsed or written to. To respect the API specification, but allow JsonPath to modify Json documents through add, set/put, replace, and delete operations, `JakartaJsonProvider` has to be initiliazed with optional `true` argument:
* `JsonProvider jsonProvider = new JakartaJsonProvider(true)` (enable mutable Json arrays and objects)
* `JsonProvider jsonProvider = new JakartaJsonProvider()` (default, strict JSON-P API compliance)
All lookup and read operations with JsonPath are supported regardless of initilization mode. Default mode also needs less memory and is more performant.
### Cache SPI
In JsonPath 2.1.0 a new Cache SPI was introduced. This allows API consumers to configure path caching in a way that suits their needs. The cache must be configured before it is accesses for the first time or a JsonPathException is thrown. JsonPath ships with two cache implementations

5
build.gradle

@ -22,12 +22,17 @@ ext {
jsonSmart: 'net.minidev:json-smart:2.4.7',
slf4jApi: 'org.slf4j:slf4j-api:1.7.30',
tapestryJson: 'org.apache.tapestry:tapestry-json:5.7.2',
jakartaJsonP: 'jakarta.json:jakarta.json-api:2.0.1',
jakartaJsonB: 'jakarta.json.bind:jakarta.json.bind-api:2.0.0',
test: [
'commons-io:commons-io:2.8.0',
'junit:junit:4.12',
'org.assertj:assertj-core:3.18.1',
'org.hamcrest:hamcrest:2.2',
'org.glassfish:jakarta.json:2.0.1',
'org.eclipse:yasson:2.0.2',
//'org.apache.johnzon:johnzon-jsonb:1.2.12',
'org.slf4j:slf4j-simple:1.7.30'
]
]

16
json-path/build.gradle

@ -6,7 +6,7 @@ jar {
baseName 'json-path'
bnd (
'Implementation-Title': 'json-path', 'Implementation-Version': version,
'Import-Package': 'org.json.*;resolution:=optional, com.google.gson.*;resolution:=optional, com.fasterxml.jackson.*;resolution:=optional, org.apache.tapestry5.json.*;resolution:=optional, org.codehaus.jettison.*;resolution:=optional, *',
'Import-Package': 'org.json.*;resolution:=optional, com.google.gson.*;resolution:=optional, com.fasterxml.jackson.*;resolution:=optional, org.apache.tapestry5.json.*;resolution:=optional, org.codehaus.jettison.*;resolution:=optional, jakarta.json.*;resolution:=optional, *',
'Export-Package': 'com.jayway.jsonpath,com.jayway.jsonpath.spi,com.jayway.jsonpath.spi.cache,com.jayway.jsonpath.spi.json,com.jayway.jsonpath.spi.mapper'
)
}
@ -19,6 +19,8 @@ dependencies {
compile libs.jsonOrg, optional
compile libs.tapestryJson, optional
compile libs.jettison, optional
compile libs.jakartaJsonP, optional
compile libs.jakartaJsonB, optional
testCompile libs.test
}
@ -37,7 +39,7 @@ task distZip(type: Zip, dependsOn: assemble) {
}
from(project.configurations.compile) {
into 'lib'
exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') }
exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') || it.file.name.contains('jakarta.json') }
}
from(project.configurations.compile) {
into 'lib-optional/jackson'
@ -59,6 +61,10 @@ task distZip(type: Zip, dependsOn: assemble) {
into 'lib-optional/tapestry'
include { it.file.name.contains('tapestry') }
}
from(project.configurations.compile) {
into 'lib-optional/jakartaJson'
include { it.file.name.contains('jakarta.json') }
}
}
task distTar(type: Tar, dependsOn: assemble) {
@ -77,7 +83,7 @@ task distTar(type: Tar, dependsOn: assemble) {
}
from(project.configurations.compile) {
into 'lib'
exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') }
exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') || it.file.name.contains('jakarta.json') }
}
from(project.configurations.compile) {
into 'lib-optional/jackson'
@ -99,6 +105,10 @@ task distTar(type: Tar, dependsOn: assemble) {
into 'lib-optional/tapestry'
include { it.file.name.contains('tapestry') }
}
from(project.configurations.compile) {
into 'lib-optional/jakartaJson'
include { it.file.name.contains('jakarta.json') }
}
}
task dist(){

1033
json-path/src/main/java/com/jayway/jsonpath/spi/json/JakartaJsonProvider.java

File diff suppressed because it is too large Load Diff

636
json-path/src/main/java/com/jayway/jsonpath/spi/mapper/JakartaMappingProvider.java

@ -0,0 +1,636 @@
/*
* Copyright 2011 the original author or authors.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jayway.jsonpath.spi.mapper;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.TypeRef;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonException;
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonString;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.JsonbException;
import jakarta.json.stream.JsonLocation;
import jakarta.json.stream.JsonParser;
public class JakartaMappingProvider implements MappingProvider {
private final Jsonb jsonb;
private Method jsonToClassMethod, jsonToTypeMethod;
public JakartaMappingProvider() {
this.jsonb = JsonbBuilder.create();
this.jsonToClassMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Class.class);
this.jsonToTypeMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Type.class);
}
public JakartaMappingProvider(JsonbConfig jsonbConfiguration) {
this.jsonb = JsonbBuilder.create(jsonbConfiguration);
this.jsonToClassMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Class.class);
this.jsonToTypeMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Type.class);
}
/**
* Maps supplied JSON source {@code Object} to a given target class or collection.
* This implementation ignores the JsonPath's {@link Configuration} argument.
*/
@Override
public <T> T map(Object source, Class<T> targetType, Configuration configuration) {
@SuppressWarnings("unchecked")
T result = (T) mapImpl(source, targetType);
return result;
}
/**
* Maps supplied JSON source {@code Object} to a given target type or collection.
* This implementation ignores the JsonPath's {@link Configuration} argument.
* <p>
* Method <em>may</em> produce a {@code ClassCastException} on an attempt to cast
* the result of JSON mapping operation to a requested target type, especially if
* a parameterized generic type is used.
*/
@Override
public <T> T map(Object source, final TypeRef<T> targetType, Configuration configuration) {
@SuppressWarnings("unchecked")
T result = (T) mapImpl(source, targetType.getType());
return result;
}
private Object mapImpl(Object source, final Type targetType) {
if (source == null || source == JsonValue.NULL) {
return null;
}
if (source == JsonValue.TRUE) {
if (Boolean.class.equals(targetType)) {
return Boolean.TRUE;
} else {
String className = targetType.toString();
throw new MappingException("JSON boolean (true) cannot be mapped to " + className);
}
}
if (source == JsonValue.FALSE) {
if (Boolean.class.equals(targetType)) {
return Boolean.FALSE;
} else {
String className = targetType.toString();
throw new MappingException("JSON boolean (false) cannot be mapped to " + className);
}
} else if (source instanceof JsonString) {
if (String.class.equals(targetType)) {
return ((JsonString) source).getChars();
} else {
String className = targetType.toString();
throw new MappingException("JSON string cannot be mapped to " + className);
}
} else if (source instanceof JsonNumber) {
JsonNumber jsonNumber = (JsonNumber) source;
if (jsonNumber.isIntegral()) {
return mapIntegralJsonNumber(jsonNumber, getRawClass(targetType));
} else {
return mapDecimalJsonNumber(jsonNumber, getRawClass(targetType));
}
}
if (source instanceof JsonArrayBuilder) {
source = ((JsonArrayBuilder) source).build();
} else if (source instanceof JsonObjectBuilder) {
source = ((JsonObjectBuilder) source).build();
}
if (source instanceof Collection) {
// this covers both List<JsonValue> and JsonArray from JSON-P spec
Class<?> rawTargetType = getRawClass(targetType);
Type targetTypeArg = getFirstTypeArgument(targetType);
Collection<Object> result = newCollectionOfType(rawTargetType);
for (Object srcValue : (Collection<?>) source) {
if (srcValue instanceof JsonObject) {
if (targetTypeArg != null) {
result.add(mapImpl(srcValue, targetTypeArg));
} else {
result.add(srcValue);
}
} else {
result.add(unwrapJsonValue(srcValue));
}
}
return result;
} else if (source instanceof JsonObject) {
if (targetType instanceof Class) {
if (jsonToClassMethod != null) {
try {
JsonParser jsonParser = new JsonStructureToParserAdapter((JsonStructure) source);
return jsonToClassMethod.invoke(jsonb, jsonParser, (Class<?>) targetType);
} catch (Exception e){
throw new MappingException(e);
}
} else {
try {
// Fallback databinding approach for JSON-B API implementations without
// explicit support for use of JsonParser in their public API. The approach
// is essentially first to serialize given value into JSON, and then bind
// it to data object of given class.
String json = source.toString();
return jsonb.fromJson(json, (Class<?>) targetType);
} catch (JsonbException e){
throw new MappingException(e);
}
}
} else if (targetType instanceof ParameterizedType) {
if (jsonToTypeMethod != null) {
try {
JsonParser jsonParser = new JsonStructureToParserAdapter((JsonStructure) source);
return jsonToTypeMethod.invoke(jsonb, jsonParser, (Type) targetType);
} catch (Exception e){
throw new MappingException(e);
}
} else {
try {
// Fallback databinding approach for JSON-B API implementations without
// explicit support for use of JsonParser in their public API. The approach
// is essentially first to serialize given value into JSON, and then bind
// the JSON string to data object of given type.
String json = source.toString();
return jsonb.fromJson(json, (Type) targetType);
} catch (JsonbException e){
throw new MappingException(e);
}
}
} else {
throw new MappingException("JSON object cannot be databind to " + targetType);
}
} else {
return source;
}
}
@SuppressWarnings("unchecked")
private <T> T mapIntegralJsonNumber(JsonNumber jsonNumber, Class<?> targetType) {
if (targetType.isPrimitive()) {
if (int.class.equals(targetType)) {
return (T) Integer.valueOf(jsonNumber.intValueExact());
} else if (long.class.equals(targetType)) {
return (T) Long.valueOf(jsonNumber.longValueExact());
}
} else if (Integer.class.equals(targetType)) {
return (T) Integer.valueOf(jsonNumber.intValueExact());
} else if (Long.class.equals(targetType)) {
return (T) Long.valueOf(jsonNumber.longValueExact());
} else if (BigInteger.class.equals(targetType)) {
return (T) jsonNumber.bigIntegerValueExact();
} else if (BigDecimal.class.equals(targetType)) {
return (T) jsonNumber.bigDecimalValue();
}
String className = targetType.getSimpleName();
throw new MappingException("JSON integral number cannot be mapped to " + className);
}
@SuppressWarnings("unchecked")
private <T> T mapDecimalJsonNumber(JsonNumber jsonNumber, Class<?> targetType) {
if (targetType.isPrimitive()) {
if (float.class.equals(targetType)) {
return (T) new Float(jsonNumber.doubleValue());
} else if (double.class.equals(targetType)) {
return (T) Double.valueOf(jsonNumber.doubleValue());
}
} else if (Float.class.equals(targetType)) {
return (T) new Float(jsonNumber.doubleValue());
} else if (Double.class.equals(targetType)) {
return (T) Double.valueOf(jsonNumber.doubleValue());
} else if (BigDecimal.class.equals(targetType)) {
return (T) jsonNumber.bigDecimalValue();
}
String className = targetType.getSimpleName();
throw new MappingException("JSON decimal number cannot be mapped to " + className);
}
private Object unwrapJsonValue(Object jsonValue) {
if (jsonValue == null) {
return null;
}
if (!(jsonValue instanceof JsonValue)) {
return jsonValue;
}
switch (((JsonValue) jsonValue).getValueType()) {
case ARRAY:
// TODO do we unwrap JsonObjectArray proxies?
//return ((JsonArray) jsonValue).getValuesAs(JsonValue.class);
return ((JsonArray) jsonValue).getValuesAs((JsonValue v) -> unwrapJsonValue(v));
case OBJECT:
throw new IllegalArgumentException("Use map() method to databind a JsonObject");
case STRING:
return ((JsonString) jsonValue).getString();
case NUMBER:
if (((JsonNumber) jsonValue).isIntegral()) {
//return ((JsonNumber) jsonValue).bigIntegerValueExact();
try {
return ((JsonNumber) jsonValue).intValueExact();
} catch (ArithmeticException e) {
return ((JsonNumber) jsonValue).longValueExact();
}
} else {
//return ((JsonNumber) jsonValue).bigDecimalValue();
return ((JsonNumber) jsonValue).doubleValue();
}
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case NULL:
return null;
default:
return jsonValue;
}
}
/**
* Creates new instance of {@code Collection} type specified by the
* argument. If the argument refers to an interface, then a matching
* Java standard implementation is returned; if it is a concrete class,
* then method attempts to instantiate an object of that class given
* there is a public no-arg constructor available.
*
* @param collectionType collection type; may be an interface or a class
* @return instance of collection type identified by the argument
* @throws MappingException on a type that cannot be safely instantiated
*/
private Collection<Object> newCollectionOfType(Class<?> collectionType) throws MappingException {
if (Collection.class.isAssignableFrom(collectionType)) {
if (!collectionType.isInterface()) {
@SuppressWarnings("unchecked")
Collection<Object> coll = (Collection<Object>) newNoArgInstance(collectionType);
return coll;
} else if (List.class.isAssignableFrom(collectionType)) {
return new java.util.LinkedList<Object>();
} else if (Set.class.isAssignableFrom(collectionType)) {
return new java.util.LinkedHashSet<Object>();
} else if (Queue.class.isAssignableFrom(collectionType)) {
return new java.util.LinkedList<Object>();
}
}
String className = collectionType.getSimpleName();
throw new MappingException("JSON array cannot be mapped to " + className);
}
/**
* Lists all publicly accessible constructors for the {@code Class}
* identified by the argument, including any constructors inherited
* from superclasses, and uses a no-args constructor, if available,
* to create a new instance of the class. If argument is interface,
* this method returns {@code null}.
*
* @param targetType class type to create instance of
* @return an instance of the class represented by the argument
* @throws MappingException if no-arg public constructor is not there
*/
private Object newNoArgInstance(Class<?> targetType) throws MappingException {
if (targetType.isInterface()) {
return null;
} else {
for (Constructor<?> ctr : targetType.getConstructors()) {
if (ctr.getParameterCount() == 0) {
try {
return ctr.newInstance();
} catch (ReflectiveOperationException e) {
throw new MappingException(e);
} catch (IllegalArgumentException e) {
// never happens
}
}
}
String className = targetType.getSimpleName();
throw new MappingException("Unable to find no-arg ctr for " + className);
}
}
/**
* Locates optional API method on the supplied JSON-B API implementation class with
* the supplied name and parameter types. Searches the superclasses up to
* {@code Object}, but ignores interfaces and default interface methods. Returns
* {@code null} if no {@code Method} can be found.
*
* @param clazz the implementation class to reflect upon
* @param name the name of the method
* @param paramTypes the parameter types of the method
* @return the {@code Method} reference, or {@code null} if none found
*/
private Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
while (clazz != null && !clazz.isInterface()) {
for (Method method : clazz.getDeclaredMethods()) {
final int mods = method.getModifiers();
if (Modifier.isPublic(mods) && !Modifier.isAbstract(mods) &&
name.equals(method.getName()) &&
Arrays.equals(paramTypes, method.getParameterTypes())) {
return method;
}
}
clazz = clazz.getSuperclass();
}
return null;
}
private Class<?> getRawClass(Type targetType) {
if (targetType instanceof Class) {
return (Class<?>) targetType;
} else if (targetType instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) targetType).getRawType();
} else if (targetType instanceof GenericArrayType) {
String typeName = targetType.getTypeName();
throw new MappingException("Cannot map JSON element to " + typeName);
} else {
String typeName = targetType.getTypeName();
throw new IllegalArgumentException("TypeRef not supported: " + typeName);
}
}
private Type getFirstTypeArgument(Type targetType) {
if (targetType instanceof ParameterizedType) {
Type[] args = ((ParameterizedType) targetType).getActualTypeArguments();
if (args != null && args.length > 0) {
if (args[0] instanceof Class) {
return (Class<?>) args[0];
} else if (args[0] instanceof ParameterizedType) {
return (ParameterizedType) args[0];
}
}
}
return null;
}
/**
* Runtime adapter for {@link JsonParser} to pull the JSON objects and values from
* a {@link JsonStructure} content tree instead of plain JSON string.
* <p>
* JSON-B API 1.0 final specification does not include any public methods to read JSON
* content from pre-parsed {@link JsonStructure} tree, so this parser is used by the
* Jakarta EE mapping provider above to feed in JSON content to JSON-B implementation.
*/
private static class JsonStructureToParserAdapter implements JsonParser {
private JsonStructureScope scope;
private Event state;
private final Deque<JsonStructureScope> ancestry = new ArrayDeque<>();
JsonStructureToParserAdapter(JsonStructure jsonStruct) {
scope = createScope(jsonStruct);
}
@Override
public boolean hasNext() {
return !((state == Event.END_ARRAY || state == Event.END_OBJECT) && ancestry.isEmpty());
}
@Override
public Event next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
if (state == null) {
state = scope instanceof JsonArrayScope ? Event.START_ARRAY : Event.START_OBJECT;
} else {
if (state == Event.END_ARRAY || state == Event.END_OBJECT) {
scope = ancestry.pop();
}
if (scope instanceof JsonArrayScope) { // array scope
if (scope.hasNext()) {
scope.next();
state = getState(scope.getValue());
if (state == Event.START_ARRAY || state == Event.START_OBJECT) {
ancestry.push(scope);
scope = createScope(scope.getValue());
}
} else {
state = Event.END_ARRAY;
}
} else { // object scope
if (state == Event.KEY_NAME) {
state = getState(scope.getValue());
if (state == Event.START_ARRAY || state == Event.START_OBJECT) {
ancestry.push(scope);
scope = createScope(scope.getValue());
}
} else {
if (scope.hasNext()) {
scope.next();
state = Event.KEY_NAME;
} else {
state = Event.END_OBJECT;
}
}
}
}
return state;
}
@Override
public String getString() {
switch (state) {
case KEY_NAME:
return ((JsonObjectScope) scope).getKey();
case VALUE_STRING:
return ((JsonString) scope.getValue()).getString();
case VALUE_NUMBER:
return ((JsonNumber) scope.getValue()).toString();
default:
throw new IllegalStateException("Parser is not in KEY_NAME, VALUE_STRING, or VALUE_NUMBER state");
}
}
@Override
public boolean isIntegralNumber() {
if (state == Event.VALUE_NUMBER) {
return ((JsonNumber) scope.getValue()).isIntegral();
}
throw new IllegalStateException("Target json value must a number, not " + state);
}
@Override
public int getInt() {
if (state == Event.VALUE_NUMBER) {
return ((JsonNumber) scope.getValue()).intValue();
}
throw new IllegalStateException("Target json value must a number, not " + state);
}
@Override
public long getLong() {
if (state == Event.VALUE_NUMBER) {
return ((JsonNumber) scope.getValue()).longValue();
}
throw new IllegalStateException("Target json value must a number, not " + state);
}
@Override
public BigDecimal getBigDecimal() {
if (state == Event.VALUE_NUMBER) {
return ((JsonNumber) scope.getValue()).bigDecimalValue();
}
throw new IllegalStateException("Target json value must a number, not " + state);
}
@Override
public JsonLocation getLocation() {
throw new UnsupportedOperationException("JSON-P adapter does not support getLocation()");
}
@Override
public void skipArray() {
if (scope instanceof JsonArrayScope) {
while (scope.hasNext()) {
scope.next();
}
state = Event.END_ARRAY;
}
}
@Override
public void skipObject() {
if (scope instanceof JsonObjectScope) {
while (scope.hasNext()) {
scope.next();
}
state = Event.END_OBJECT;
}
}
@Override
public void close() {
// JSON objects are read-only
}
private JsonStructureScope createScope(JsonValue value) {
if (value instanceof JsonArray) {
return new JsonArrayScope((JsonArray) value);
} else if (value instanceof JsonObject) {
return new JsonObjectScope((JsonObject) value);
}
throw new JsonException("Cannot create JSON iterator for " + value);
}
private Event getState(JsonValue value) {
switch (value.getValueType()) {
case ARRAY:
return Event.START_ARRAY;
case OBJECT:
return Event.START_OBJECT;
case STRING:
return Event.VALUE_STRING;
case NUMBER:
return Event.VALUE_NUMBER;
case TRUE:
return Event.VALUE_TRUE;
case FALSE:
return Event.VALUE_FALSE;
case NULL:
return Event.VALUE_NULL;
default:
throw new JsonException("Unknown value type " + value.getValueType());
}
}
}
private static abstract class JsonStructureScope implements Iterator<JsonValue> {
/**
* Returns current {@link JsonValue}, that the parser is pointing on. Before
* the {@link #next()} method has been called, this returns {@code null}.
*
* @return JsonValue value object.
*/
abstract JsonValue getValue();
}
private static class JsonArrayScope extends JsonStructureScope {
private final Iterator<JsonValue> it;
private JsonValue value;
JsonArrayScope(JsonArray array) {
this.it = array.iterator();
}
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public JsonValue next() {
value = it.next();
return value;
}
@Override
JsonValue getValue() {
return value;
}
}
private static class JsonObjectScope extends JsonStructureScope {
private final Iterator<Map.Entry<String, JsonValue>> it;
private JsonValue value;
private String key;
JsonObjectScope(JsonObject object) {
this.it = object.entrySet().iterator();
}
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public JsonValue next() {
Map.Entry<String, JsonValue> next = it.next();
this.key = next.getKey();
this.value = next.getValue();
return value;
}
@Override
JsonValue getValue() {
return value;
}
String getKey() {
return key;
}
}
}

23
json-path/src/test/java/com/jayway/jsonpath/BaseTest.java

@ -5,12 +5,14 @@ import com.jayway.jsonpath.internal.path.PredicateContextImpl;
import com.jayway.jsonpath.spi.json.GsonJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JakartaJsonProvider;
import com.jayway.jsonpath.spi.json.JettisonProvider;
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider;
import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider;
import com.jayway.jsonpath.spi.json.TapestryJsonProvider;
import com.jayway.jsonpath.spi.mapper.GsonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JakartaMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonOrgMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider;
import com.jayway.jsonpath.spi.mapper.TapestryMappingProvider;
@ -55,10 +57,23 @@ public class BaseTest {
.build();
public static final Configuration TAPESTRY_JSON_CONFIGURATION = Configuration
.builder()
.mappingProvider(new TapestryMappingProvider())
.jsonProvider(TapestryJsonProvider.INSTANCE)
.build();
.builder()
.mappingProvider(new TapestryMappingProvider())
.jsonProvider(TapestryJsonProvider.INSTANCE)
.build();
public static final Configuration JAKARTA_JSON_CONFIGURATION = Configuration
.builder()
.mappingProvider(new JakartaMappingProvider())
.jsonProvider(new JakartaJsonProvider())
.build();
// extension to Jakarta EE 9 JSON-P with mutable objects and array
public static final Configuration JAKARTA_JSON_RW_CONFIGURATION = Configuration
.builder()
.mappingProvider(new JakartaMappingProvider())
.jsonProvider(new JakartaJsonProvider(true))
.build();
public static final String JSON_BOOK_DOCUMENT =
"{ " +

10
json-path/src/test/java/com/jayway/jsonpath/Configurations.java

@ -3,10 +3,12 @@ package com.jayway.jsonpath;
import com.jayway.jsonpath.spi.json.GsonJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JakartaJsonProvider;
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider;
import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider;
import com.jayway.jsonpath.spi.mapper.GsonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JakartaMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonOrgMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider;
@ -44,6 +46,12 @@ public class Configurations {
.jsonProvider(new JsonSmartJsonProvider())
.build();
public static final Configuration JAKARTA_CONFIGURATION = Configuration
.builder()
.mappingProvider(new JakartaMappingProvider())
.jsonProvider(new JakartaJsonProvider())
.build();
public static Iterable<Configuration> configurations() {
return Arrays.asList(
JSON_SMART_CONFIGURATION
@ -51,6 +59,7 @@ public class Configurations {
,JACKSON_CONFIGURATION
,JACKSON_JSON_NODE_CONFIGURATION
,JSON_ORG_CONFIGURATION
,JAKARTA_CONFIGURATION
);
}
@ -59,6 +68,7 @@ public class Configurations {
GSON_CONFIGURATION
,JACKSON_CONFIGURATION
,JACKSON_JSON_NODE_CONFIGURATION
,JAKARTA_CONFIGURATION
);
}
}

4
json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java

@ -193,6 +193,10 @@ public class InlineFilterTest extends BaseTest {
if(conf.jsonProvider().getClass().getSimpleName().startsWith("Jackson")){
return;
}
if(conf.jsonProvider().getClass().getSimpleName().startsWith("Jakarta")){
// single quotes are not valid in JSON; see json.org
return;
}
assertHasOneResult("[\"\\'foo\"]", "$[?(@ == '\\'foo')]", conf);
}

381
json-path/src/test/java/com/jayway/jsonpath/JakartaJsonProviderTest.java

@ -0,0 +1,381 @@
package com.jayway.jsonpath;
import org.junit.Test;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.jayway.jsonpath.JsonPath.parse;
import static com.jayway.jsonpath.JsonPath.using;
import static java.util.Collections.emptyMap;
import static org.assertj.core.api.Assertions.assertThat;
public class JakartaJsonProviderTest extends BaseTest {
private static final Map<String, Object> EMPTY_MAP = emptyMap();
@Test
public void an_object_can_be_read() {
JsonObject book = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_DOCUMENT)
.read("$.store.book[0]");
assertThat(((JsonString) book.get("author")).getChars()).isEqualTo("Nigel Rees");
}
@Test
public void a_property_can_be_read() {
JsonString category = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_DOCUMENT)
.read("$.store.book[0].category");
assertThat(category.getString()).isEqualTo("reference");
}
@Test
public void a_filter_can_be_applied() {
List<Object> fictionBooks = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_DOCUMENT)
.read("$.store.book[?(@.category == 'fiction')]");
assertThat(fictionBooks.size()).isEqualTo(3);
}
@Test
public void result_can_be_mapped_to_object() {
@SuppressWarnings("unchecked")
List<Map<String, Object>> books = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_DOCUMENT)
.read("$.store.book", List.class);
assertThat(books.size()).isEqualTo(4);
}
@Test
public void read_books_with_isbn() {
List<Object> books = using(JAKARTA_JSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$..book[?(@.isbn)]");
assertThat(books.size()).isEqualTo(2);
}
/**
* Functions take parameters, the length parameter for example takes an entire document which we anticipate
* will compute to a document that is an array of elements which can determine its length.
*
* Since we translate this query from $..books.length() to length($..books) verify that this particular translation
* works as anticipated.
*/
@Test
public void read_book_length_using_translated_query() {
Integer result = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_BOOK_STORE_DOCUMENT)
.read("$..book.length()");
assertThat(result).isEqualTo(4);
}
@Test
public void read_book_length() {
Object result = using(JAKARTA_JSON_CONFIGURATION)
.parse(JSON_BOOK_STORE_DOCUMENT)
.read("$.length($..book)");
assertThat(result).isEqualTo(4);
}
@Test
public void issue_97() {
String json = "{ \"books\": [ " +
"{ \"category\": \"fiction\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"fiction\" }, " +
"{ \"category\": \"fiction\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"fiction\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"reference\" }, " +
"{ \"category\": \"reference\" } ] }";
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(json)
.delete("$.books[?(@.category == 'reference')]");
//System.out.println((Object) dc.read("$"));
@SuppressWarnings("unchecked")
List<String> categories = dc.read("$..category", List.class);
assertThat(categories).containsOnly("fiction");
}
@Test
public void test_delete_2() {
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse("[" +
"{\"top\": {\"middle\": null}}," +
"{\"top\": {\"middle\": {} }}," +
"{\"top\": {\"middle\": {\"bottom\": 2} }}" +
"]")
.delete(JsonPath.compile("$[*].top.middle.bottom"));
Object ans = dc.read("$");
//System.out.println(ans);
assert(ans.toString().equals("[{\"top\":{\"middle\":null}},{\"top\":{\"middle\":{}}},{\"top\":{\"middle\":{}}}]"));
}
@Test
public void an_root_property_can_be_updated() {
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$.int-max-property", 1)
.json();
Integer result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read("$.int-max-property", Integer.class);
assertThat(result).isEqualTo(1);
}
@Test
public void an_deep_scan_can_update() {
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$..display-price", 1)
.json();
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read("$..display-price", new TypeRef<List<Integer>>() {});
assertThat(result).containsExactly(1, 1, 1, 1, 1);
}
@Test
public void an_filter_can_update() {
final String updatePathFunction = "$.store.book[?(@.display-price)].display-price";
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set(updatePathFunction, 1)
.json();
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read(updatePathFunction, new TypeRef<List<Integer>>() {});
assertThat(result).containsExactly(1, 1, 1, 1);
}
@Test
public void a_path_can_be_deleted() {
final String deletePath = "$.store.book[*].display-price";
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.delete(deletePath)
.json();
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read(deletePath, new TypeRef<List<Integer>>() {});
assertThat(result).isEmpty();
}
@Test
public void operations_can_chained() {
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.delete("$.store.book[*].display-price")
.set("$.store.book[*].category", "A")
.json();
List<Integer> prices = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read("$.store.book[*].display-price", new TypeRef<List<Integer>>() {});
List<String> categories = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o)
.read("$.store.book[*].category", new TypeRef<List<String>>() {});
assertThat(prices).isEmpty();
assertThat(categories).containsExactly("A", "A", "A", "A");
}
@Test
public void an_array_index_can_be_updated() {
String res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$.store.book[0]", "a")
.read("$.store.book[0]", String.class);
assertThat(res).isEqualTo("a");
}
@Test
public void an_array_slice_can_be_updated() {
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$.store.book[0:2]", "a")
.read("$.store.book[0:2]", new TypeRef<List<String>>() {});
assertThat(res).containsExactly("a", "a");
}
@Test
public void an_array_criteria_can_be_updated() {
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$.store.book[?(@.category == 'fiction')]", "a")
.read("$.store.book[?(@ == 'a')]", new TypeRef<List<String>>() {});
assertThat(res).containsExactly("a", "a", "a");
}
@Test
public void an_array_criteria_can_be_deleted() {
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.delete("$.store.book[?(@.category == 'fiction')]")
.read("$.store.book[*].category", new TypeRef<List<String>>() {});
assertThat(res).containsExactly("reference");
}
@Test
public void an_array_criteria_with_multiple_results_can_be_deleted(){
InputStream stream = this.getClass().getResourceAsStream("/json_array_multiple_delete.json");
String deletePath = "$._embedded.mandates[?(@.count=~/0/)]";
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION).parse(stream).delete(deletePath);
List<Object> result = dc.read(deletePath);
assertThat(result.size()).isEqualTo(0);
}
@Test
public void multi_prop_delete() {
List<Map<String, Object>> res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.delete("$.store.book[*]['author', 'category']")
.read("$.store.book[*]['author', 'category']", new TypeRef<List<Map<String, Object>>>() {});
assertThat(res).containsExactly(EMPTY_MAP, EMPTY_MAP, EMPTY_MAP, EMPTY_MAP);
}
@Test
public void multi_prop_update() {
@SuppressWarnings("serial")
Map<String, Object> expected = new HashMap<String, Object>(){{
put("author", "a");
put("category", "a");
}};
List<Map<String, Object>> res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.set("$.store.book[*]['author', 'category']", "a")
.read("$.store.book[*]['author', 'category']", new TypeRef<List<Map<String, Object>>>() {});
assertThat(res).containsExactly(expected, expected, expected, expected);
}
@Test
public void add_to_array() {
Object res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.add("$.store.book", 1)
.read("$.store.book[4]");
res = JAKARTA_JSON_RW_CONFIGURATION.jsonProvider().unwrap(res);
assertThat(res).isEqualTo(1);
}
@Test
public void add_to_object() {
Object res = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.put("$.store.book[0]", "new-key", "new-value")
.read("$.store.book[0].new-key");
res = JAKARTA_JSON_RW_CONFIGURATION.jsonProvider().unwrap(res);
assertThat(res).isEqualTo("new-value");
}
@Test(expected = InvalidModificationException.class)
public void add_to_object_on_array() {
using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT).put("$.store.book", "new-key", "new-value");
}
@Test(expected = InvalidModificationException.class)
public void add_to_array_on_object() {
using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT).add("$.store.book[0]", "new-value");
}
@Test
public void a_path_can_be_renamed(){
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.renameKey("$.store", "book", "updated-book")
.json();
List<Object> result = parse(o).read("$.store.updated-book");
assertThat(result).isNotEmpty();
}
@Test
public void map_array_items_can_be_renamed(){
Object o = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.renameKey("$.store.book[*]", "category", "renamed-category")
.json();
List<Object> result = parse(o).read("$.store.book[*].renamed-category");
assertThat(result).isNotEmpty();
}
@Test(expected = PathNotFoundException.class)
public void non_existent_key_rename_not_allowed() {
using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.renameKey("$", "fake", "new-fake")
.json();
}
@Test
public void single_match_value_can_be_mapped() {
MapFunction mapFunction = new ToStringMapFunction();
String stringResult = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.map("$.string-property", mapFunction)
.read("$.string-property", String.class);
assertThat(stringResult.endsWith("converted")).isTrue();
}
@Test
public void object_can_be_mapped() {
TypeRef<List<String>> typeRef = new TypeRef<List<String>>() {};
MapFunction mapFunction = new ToStringMapFunction();
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT);
Object list = dc.read("$..book");
assertThat(list).isInstanceOf(List.class);
Object res = dc.map("$..book", mapFunction).read("$..book", typeRef).get(0);
assertThat(res).isInstanceOf(String.class);
assertThat((String) res).endsWith("converted");
}
@Test
public void multi_match_path_can_be_mapped() {
MapFunction mapFunction = new ToStringMapFunction();
List<Double> doubleResult = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.read("$..display-price", new TypeRef<List<Double>>() {});
for (Double dRes : doubleResult){
assertThat(dRes).isInstanceOf(Double.class);
}
List<String> stringResult = using(JAKARTA_JSON_RW_CONFIGURATION)
.parse(JSON_DOCUMENT)
.map("$..display-price", mapFunction)
.read("$..display-price", new TypeRef<List<String>>() {});
for (String sRes : stringResult){
assertThat(sRes).isInstanceOf(String.class);
assertThat(sRes.endsWith("converted")).isTrue();
}
}
// Helper converter implementation for test cases.
private class ToStringMapFunction implements MapFunction {
@Override
public Object map(Object currentValue, Configuration configuration) {
return currentValue.toString()+"converted";
}
}
}

21
json-path/src/test/java/com/jayway/jsonpath/ProviderInTest.java

@ -5,10 +5,12 @@ import com.google.gson.JsonArray;
import com.jayway.jsonpath.spi.json.GsonJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JakartaJsonProvider;
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider;
import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider;
import com.jayway.jsonpath.spi.mapper.GsonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.JakartaMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonOrgMappingProvider;
import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider;
import org.assertj.core.util.Lists;
@ -120,4 +122,23 @@ public class ProviderInTest {
final net.minidev.json.JSONArray singleQuoteInResult = ctx.read(SINGLE_QUOTES_IN_FILTER);
assertEquals(doubleQuoteInResult, singleQuoteInResult);
}
@Test
public void testJsonPathQuotesJakarta() throws Exception {
final Configuration gson = Configuration.builder().jsonProvider(new JakartaJsonProvider()).mappingProvider(new JakartaMappingProvider()).build();
final DocumentContext ctx = JsonPath.using(gson).parse(JSON);
final List<jakarta.json.JsonValue> doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER);
// returned string is surrounded by double quotes in Jakarta EE JSON-P implementations
assertEquals("bar", ((jakarta.json.JsonString) doubleQuoteEqualsResult.get(0)).getChars());
final List<jakarta.json.JsonValue> singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER);
assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult);
final List<jakarta.json.JsonValue> doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER);
assertEquals(doubleQuoteInResult, doubleQuoteEqualsResult);
final List<jakarta.json.JsonValue> singleQuoteInResult = ctx.read(SINGLE_QUOTES_IN_FILTER);
assertEquals(doubleQuoteInResult, singleQuoteInResult);
}
}
Loading…
Cancel
Save