diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java index e5fcc1ef5..0902ab639 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/DocumentAdapters.java @@ -24,12 +24,18 @@ import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.NestedIdentity; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; +import jakarta.json.JsonArray; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import java.math.BigInteger; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -85,22 +91,8 @@ final class DocumentAdapters { Map matchedQueries = hit.matchedQueries(); - Function, EntityAsMap> fromFields = fields -> { - StringBuilder sb = new StringBuilder("{"); - final boolean[] firstField = { true }; - hit.fields().forEach((key, jsonData) -> { - if (!firstField[0]) { - sb.append(','); - } - sb.append('"').append(key).append("\":") // - .append(jsonData.toJson(jsonpMapper).toString()); - firstField[0] = false; - }); - sb.append('}'); - return new EntityAsMap().fromJson(sb.toString()); - }; - - EntityAsMap hitFieldsAsMap = fromFields.apply(hit.fields()); + EntityAsMap hitFieldsAsMap = new EntityAsMap(); + hit.fields().forEach((key, jsonData) -> hitFieldsAsMap.put(key, toJavaObject(jsonData, jsonpMapper))); Map> documentFields = new LinkedHashMap<>(); hitFieldsAsMap.forEach((key, value) -> { @@ -144,6 +136,61 @@ final class DocumentAdapters { documentFields, highlightFields, innerHits, nestedMetaData, explanation, matchedQueries, hit.routing()); } + @Nullable + private static Object toJavaObject(JsonData jsonData, JsonpMapper jsonpMapper) { + return toJavaObject(jsonData.toJson(jsonpMapper)); + } + + @Nullable + private static Object toJavaObject(JsonValue jsonValue) { + + return switch (jsonValue.getValueType()) { + case OBJECT -> toMap(jsonValue.asJsonObject()); + case ARRAY -> toList(jsonValue.asJsonArray()); + case STRING -> ((JsonString) jsonValue).getString(); + case NUMBER -> toNumber((JsonNumber) jsonValue); + case TRUE -> Boolean.TRUE; + case FALSE -> Boolean.FALSE; + case NULL -> null; + }; + } + + private static Map toMap(JsonObject jsonObject) { + + Map result = new LinkedHashMap<>(); + jsonObject.forEach((key, value) -> result.put(key, toJavaObject(value))); + return result; + } + + private static List<@Nullable Object> toList(JsonArray jsonArray) { + + List<@Nullable Object> result = new ArrayList<>(jsonArray.size()); + jsonArray.forEach(value -> result.add(toJavaObject(value))); + return result; + } + + private static Number toNumber(JsonNumber jsonNumber) { + + if (!jsonNumber.isIntegral()) { + return jsonNumber.doubleValue(); + } + + try { + return jsonNumber.intValueExact(); + } catch (ArithmeticException ignored) { + // continue with a wider numeric type + } + + try { + return jsonNumber.longValueExact(); + } catch (ArithmeticException ignored) { + // continue with a wider numeric type + } + + BigInteger value = jsonNumber.bigIntegerValue(); + return value; + } + public static SearchDocument from(CompletionSuggestOption completionSuggestOption) { Document document = completionSuggestOption.source() != null ? Document.from(completionSuggestOption.source()) diff --git a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java index 93f4aeb97..884a6832b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/client/elc/DocumentAdaptersUnitTests.java @@ -19,6 +19,7 @@ import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.json.JsonData; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import jakarta.json.JsonValue; import java.util.Arrays; import java.util.Collections; @@ -77,6 +78,49 @@ class DocumentAdaptersUnitTests { softly.assertAll(); } + @Test // #3178 + @DisplayName("should adapt parser backed search Hit fields to Java types") + void shouldAdaptParserBackedSearchHitFieldsToJavaTypes() { + + Hit searchHit = new Hit.Builder() // + .index("index") // + .id("my-id") // + .fields("objectField", JsonData.fromJson( + "{\"string\":\"value\",\"integer\":2,\"decimal\":1.5,\"nested\":{\"flag\":true}}")) // + .fields("listField", JsonData.fromJson("[\"listValue\",{\"nested\":3},true,null]")) // + .fields("stringField", JsonData.fromJson("\"stringValue\"")) // + .build(); // + + SearchDocument document = DocumentAdapters.from(searchHit, jsonpMapper); + + SoftAssertions softly = new SoftAssertions(); + + Object objectFieldValue = document.get("objectField"); + Object listFieldValue = document.get("listField"); + Object objectFieldFirstValue = document.getFieldValue("objectField"); + Object listFieldFirstValue = document.getFieldValue("listField"); + + softly.assertThat(objectFieldValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class); + softly.assertThat(listFieldValue).isInstanceOf(List.class).isNotInstanceOf(JsonValue.class); + softly.assertThat(document.get("stringField")).isEqualTo("stringValue"); + softly.assertThat(objectFieldFirstValue).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class); + softly.assertThat(listFieldFirstValue).isEqualTo("listValue"); + + // noinspection unchecked + Map objectField = (Map) objectFieldValue; + softly.assertThat(objectField.get("string")).isEqualTo("value"); + softly.assertThat(objectField.get("integer")).isEqualTo(2); + softly.assertThat(objectField.get("decimal")).isEqualTo(1.5d); + softly.assertThat(objectField.get("nested")).isInstanceOf(Map.class).isNotInstanceOf(JsonValue.class); + + // noinspection unchecked + List listField = (List) listFieldValue; + softly.assertThat(listField).containsExactly("listValue", Collections.singletonMap("nested", 3), true, null); + softly.assertThat(listField.get(1)).isNotInstanceOf(JsonValue.class); + + softly.assertAll(); + } + @Test // #1973 @DisplayName("should adapt search Hit from source") void shouldAdaptSearchHitFromSource() {