Skip to content

Commit 49a0aaa

Browse files
authored
GH-9383: Introduce JsonIndexAccessor
Fixes: #9383 Issue link: #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']).
1 parent c179c06 commit 49a0aaa

File tree

5 files changed

+626
-249
lines changed

5 files changed

+626
-249
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2013-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.json;
18+
19+
import com.fasterxml.jackson.databind.node.ArrayNode;
20+
21+
import org.springframework.expression.AccessException;
22+
import org.springframework.expression.EvaluationContext;
23+
import org.springframework.expression.IndexAccessor;
24+
import org.springframework.expression.TypedValue;
25+
import org.springframework.lang.Nullable;
26+
27+
/**
28+
* A SpEL {@link IndexAccessor} that knows how to read indexes from JSON arrays, using
29+
* Jackson's {@link ArrayNode} API.
30+
*
31+
* <p>Supports indexes supplied as an integer literal &mdash; for example, {@code myJsonArray[1]}.
32+
* Also supports negative indexes &mdash; for example, {@code myJsonArray[-1]} which equates
33+
* to {@code myJsonArray[myJsonArray.length - 1]}. Furthermore, {@code null} is returned for
34+
* any index that is out of bounds (see {@link ArrayNode#get(int)} for details).
35+
*
36+
* @author Sam Brannen
37+
* @since 6.4
38+
* @see JsonPropertyAccessor
39+
*/
40+
public class JsonIndexAccessor implements IndexAccessor {
41+
42+
private static final Class<?>[] SUPPORTED_CLASSES = { ArrayNode.class };
43+
44+
@Override
45+
public Class<?>[] getSpecificTargetClasses() {
46+
return SUPPORTED_CLASSES;
47+
}
48+
49+
@Override
50+
public boolean canRead(EvaluationContext context, Object target, Object index) {
51+
return (target instanceof ArrayNode && index instanceof Integer);
52+
}
53+
54+
@Override
55+
public TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException {
56+
ArrayNode arrayNode = (ArrayNode) target;
57+
Integer intIndex = (Integer) index;
58+
if (intIndex < 0) {
59+
// negative index: get from the end of array, for compatibility with JsonPropertyAccessor.ArrayNodeAsList.
60+
intIndex = arrayNode.size() + intIndex;
61+
}
62+
return JsonPropertyAccessor.typedValue(arrayNode.get(intIndex));
63+
}
64+
65+
@Override
66+
public boolean canWrite(EvaluationContext context, Object target, Object index) {
67+
return false;
68+
}
69+
70+
@Override
71+
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
72+
throw new UnsupportedOperationException("Write is not supported");
73+
}
74+
75+
}

spring-integration-core/src/main/java/org/springframework/integration/json/JsonPropertyAccessor.java

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2023 the original author or authors.
2+
* Copyright 2013-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import com.fasterxml.jackson.databind.JsonNode;
2525
import com.fasterxml.jackson.databind.ObjectMapper;
2626
import com.fasterxml.jackson.databind.node.ArrayNode;
27+
import com.fasterxml.jackson.databind.node.NullNode;
2728

2829
import org.springframework.expression.AccessException;
2930
import org.springframework.expression.EvaluationContext;
@@ -35,16 +36,18 @@
3536

3637
/**
3738
* A SpEL {@link PropertyAccessor} that knows how to read properties from JSON objects.
38-
* Uses Jackson {@link JsonNode} API for nested properties access.
39+
* <p>Uses Jackson {@link JsonNode} API for nested properties access.
3940
*
4041
* @author Eric Bottard
4142
* @author Artem Bilan
4243
* @author Paul Martin
4344
* @author Gary Russell
4445
* @author Pierre Lakreb
4546
* @author Vladislav Fefelov
47+
* @author Sam Brannen
4648
*
4749
* @since 3.0
50+
* @see JsonIndexAccessor
4851
*/
4952
public class JsonPropertyAccessor implements PropertyAccessor {
5053

@@ -80,23 +83,22 @@ public boolean canRead(EvaluationContext context, Object target, String name) th
8083
// Cannot parse - treat as not a JSON
8184
return false;
8285
}
83-
Integer index = maybeIndex(name);
8486
if (node instanceof ArrayNode) {
85-
return index != null;
87+
return maybeIndex(name) != null;
8688
}
8789
return true;
8890
}
8991

9092
private JsonNode asJson(Object target) throws AccessException {
91-
if (target instanceof JsonNode) {
92-
return (JsonNode) target;
93+
if (target instanceof JsonNode jsonNode) {
94+
return jsonNode;
9395
}
94-
else if (target instanceof JsonNodeWrapper) {
95-
return ((JsonNodeWrapper<?>) target).getRealNode();
96+
else if (target instanceof JsonNodeWrapper<?> jsonNodeWrapper) {
97+
return jsonNodeWrapper.getRealNode();
9698
}
97-
else if (target instanceof String) {
99+
else if (target instanceof String content) {
98100
try {
99-
return this.objectMapper.readTree((String) target);
101+
return this.objectMapper.readTree(content);
100102
}
101103
catch (JsonProcessingException e) {
102104
throw new AccessException("Exception while trying to deserialize String", e);
@@ -160,8 +162,8 @@ private static boolean isNumeric(String str) {
160162
return true;
161163
}
162164

163-
private static TypedValue typedValue(JsonNode json) throws AccessException {
164-
if (json == null) {
165+
static TypedValue typedValue(JsonNode json) throws AccessException {
166+
if (json == null || json instanceof NullNode) {
165167
return TypedValue.NULL;
166168
}
167169
else if (json.isValueNode()) {
@@ -199,8 +201,8 @@ public static Object wrap(JsonNode json) throws AccessException {
199201
if (json == null) {
200202
return null;
201203
}
202-
else if (json instanceof ArrayNode) {
203-
return new ArrayNodeAsList((ArrayNode) json);
204+
else if (json instanceof ArrayNode arrayNode) {
205+
return new ArrayNodeAsList(arrayNode);
204206
}
205207
else if (json.isValueNode()) {
206208
return getValue(json);
@@ -212,8 +214,6 @@ else if (json.isValueNode()) {
212214

213215
interface JsonNodeWrapper<T> extends Comparable<T> {
214216

215-
String toString();
216-
217217
JsonNode getRealNode();
218218

219219
}
@@ -309,10 +309,8 @@ public Object next() {
309309

310310
@Override
311311
public int compareTo(Object o) {
312-
if (o instanceof JsonNodeWrapper<?>) {
313-
return this.delegate.equals(((JsonNodeWrapper<?>) o).getRealNode()) ? 0 : 1;
314-
}
315-
return this.delegate.equals(o) ? 0 : 1;
312+
Object that = (o instanceof JsonNodeWrapper<?> wrapper ? wrapper.getRealNode() : o);
313+
return this.delegate.equals(that) ? 0 : 1;
316314
}
317315

318316
}

0 commit comments

Comments
 (0)