GH-9383: Introduce JsonIndexAccessor

Fixes: #9383

Issue link: https://github.com/spring-projects/spring-integration/issues/9383

* Polish `JsonPropertyAccessor[Tests]`
* Introduce `JsonIndexAccessor`

This commit introduces a `JsonIndexAccessor` as a complement to the
existing `JsonPropertyAccessor`.

When a `JsonIndexAccessor` is registered with the SpEL `EvaluationContext`,
JSON arrays can be consistently indexed via integer literals (e.g.,[1]) instead of string literals representing integers (e.g., ['1']).
This commit is contained in:
Sam Brannen
2024-09-04 16:25:46 +02:00
committed by GitHub
parent c179c06134
commit 49a0aaa793
5 changed files with 620 additions and 243 deletions

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2024 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
*
* https://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 org.springframework.integration.json;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable;
/**
* A SpEL {@link IndexAccessor} that knows how to read indexes from JSON arrays, using
* Jackson's {@link ArrayNode} API.
*
* <p>Supports indexes supplied as an integer literal &mdash; for example, {@code myJsonArray[1]}.
* Also supports negative indexes &mdash; for example, {@code myJsonArray[-1]} which equates
* to {@code myJsonArray[myJsonArray.length - 1]}. Furthermore, {@code null} is returned for
* any index that is out of bounds (see {@link ArrayNode#get(int)} for details).
*
* @author Sam Brannen
* @since 6.4
* @see JsonPropertyAccessor
*/
public class JsonIndexAccessor implements IndexAccessor {
private static final Class<?>[] SUPPORTED_CLASSES = { ArrayNode.class };
@Override
public Class<?>[] getSpecificTargetClasses() {
return SUPPORTED_CLASSES;
}
@Override
public boolean canRead(EvaluationContext context, Object target, Object index) {
return (target instanceof ArrayNode && index instanceof Integer);
}
@Override
public TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException {
ArrayNode arrayNode = (ArrayNode) target;
Integer intIndex = (Integer) index;
if (intIndex < 0) {
// negative index: get from the end of array, for compatibility with JsonPropertyAccessor.ArrayNodeAsList.
intIndex = arrayNode.size() + intIndex;
}
return JsonPropertyAccessor.typedValue(arrayNode.get(intIndex));
}
@Override
public boolean canWrite(EvaluationContext context, Object target, Object index) {
return false;
}
@Override
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
throw new UnsupportedOperationException("Write is not supported");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2023 the original author or authors.
* Copyright 2013-2024 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.
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
@@ -35,7 +36,7 @@ import org.springframework.util.StringUtils;
/**
* A SpEL {@link PropertyAccessor} that knows how to read properties from JSON objects.
* Uses Jackson {@link JsonNode} API for nested properties access.
* <p>Uses Jackson {@link JsonNode} API for nested properties access.
*
* @author Eric Bottard
* @author Artem Bilan
@@ -43,8 +44,10 @@ import org.springframework.util.StringUtils;
* @author Gary Russell
* @author Pierre Lakreb
* @author Vladislav Fefelov
* @author Sam Brannen
*
* @since 3.0
* @see JsonIndexAccessor
*/
public class JsonPropertyAccessor implements PropertyAccessor {
@@ -80,23 +83,22 @@ public class JsonPropertyAccessor implements PropertyAccessor {
// Cannot parse - treat as not a JSON
return false;
}
Integer index = maybeIndex(name);
if (node instanceof ArrayNode) {
return index != null;
return maybeIndex(name) != null;
}
return true;
}
private JsonNode asJson(Object target) throws AccessException {
if (target instanceof JsonNode) {
return (JsonNode) target;
if (target instanceof JsonNode jsonNode) {
return jsonNode;
}
else if (target instanceof JsonNodeWrapper) {
return ((JsonNodeWrapper<?>) target).getRealNode();
else if (target instanceof JsonNodeWrapper<?> jsonNodeWrapper) {
return jsonNodeWrapper.getRealNode();
}
else if (target instanceof String) {
else if (target instanceof String content) {
try {
return this.objectMapper.readTree((String) target);
return this.objectMapper.readTree(content);
}
catch (JsonProcessingException e) {
throw new AccessException("Exception while trying to deserialize String", e);
@@ -160,8 +162,8 @@ public class JsonPropertyAccessor implements PropertyAccessor {
return true;
}
private static TypedValue typedValue(JsonNode json) throws AccessException {
if (json == null) {
static TypedValue typedValue(JsonNode json) throws AccessException {
if (json == null || json instanceof NullNode) {
return TypedValue.NULL;
}
else if (json.isValueNode()) {
@@ -199,8 +201,8 @@ public class JsonPropertyAccessor implements PropertyAccessor {
if (json == null) {
return null;
}
else if (json instanceof ArrayNode) {
return new ArrayNodeAsList((ArrayNode) json);
else if (json instanceof ArrayNode arrayNode) {
return new ArrayNodeAsList(arrayNode);
}
else if (json.isValueNode()) {
return getValue(json);
@@ -212,8 +214,6 @@ public class JsonPropertyAccessor implements PropertyAccessor {
interface JsonNodeWrapper<T> extends Comparable<T> {
String toString();
JsonNode getRealNode();
}
@@ -309,10 +309,8 @@ public class JsonPropertyAccessor implements PropertyAccessor {
@Override
public int compareTo(Object o) {
if (o instanceof JsonNodeWrapper<?>) {
return this.delegate.equals(((JsonNodeWrapper<?>) o).getRealNode()) ? 0 : 1;
}
return this.delegate.equals(o) ? 0 : 1;
Object that = (o instanceof JsonNodeWrapper<?> wrapper ? wrapper.getRealNode() : o);
return this.delegate.equals(that) ? 0 : 1;
}
}