diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ad2fb6fc..577bf225c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Document HTTP/2 support ([#330](https://github.com/opensearch-project/opensearch-java/pull/330)) - Require two maintainers to approve release ([#383](https://github.com/opensearch-project/opensearch-java/pull/383)) - Add support for mapping limit settings ([#382](https://github.com/opensearch-project/opensearch-java/pull/382)) +- Add buffered lookahead for Jackson ([#338](https://github.com/opensearch-project/opensearch-java/pull/338)) ### Dependencies - Bumps `classgraph` from 4.8.149 to 4.8.154 diff --git a/java-client/src/main/java/org/opensearch/client/json/DelegatingJsonParser.java b/java-client/src/main/java/org/opensearch/client/json/DelegatingJsonParser.java new file mode 100644 index 0000000000..b34606e20a --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/DelegatingJsonParser.java @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.client.json; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonLocation; +import jakarta.json.stream.JsonParser; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.stream.Stream; + +public abstract class DelegatingJsonParser implements JsonParser { + + private final JsonParser parser; + + public DelegatingJsonParser(JsonParser parser) { + this.parser = parser; + } + + @Override + public boolean hasNext() { + return parser.hasNext(); + } + + @Override + public Event next() { + return parser.next(); + } + + @Override + public String getString() { + return parser.getString(); + } + + @Override + public boolean isIntegralNumber() { + return parser.isIntegralNumber(); + } + + @Override + public int getInt() { + return parser.getInt(); + } + + @Override + public long getLong() { + return parser.getLong(); + } + + @Override + public BigDecimal getBigDecimal() { + return parser.getBigDecimal(); + } + + @Override + public JsonLocation getLocation() { + return parser.getLocation(); + } + + @Override + public JsonObject getObject() { + return parser.getObject(); + } + + @Override + public JsonValue getValue() { + return parser.getValue(); + } + + @Override + public JsonArray getArray() { + return parser.getArray(); + } + + @Override + public Stream getArrayStream() { + return parser.getArrayStream(); + } + + @Override + public Stream> getObjectStream() { + return parser.getObjectStream(); + } + + @Override + public Stream getValueStream() { + return parser.getValueStream(); + } + + @Override + public void skipArray() { + parser.skipArray(); + } + + @Override + public void skipObject() { + parser.skipObject(); + } + + @Override + public void close() { + parser.close(); + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/JsonLocationImpl.java b/java-client/src/main/java/org/opensearch/client/json/JsonLocationImpl.java new file mode 100644 index 0000000000..7b3502a048 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/JsonLocationImpl.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.client.json; + +import jakarta.json.stream.JsonLocation; + +class JsonLocationImpl implements JsonLocation { + + private final long columnNo; + private final long lineNo; + private final long offset; + + JsonLocationImpl(long lineNo, long columnNo, long streamOffset) { + this.lineNo = lineNo; + this.columnNo = columnNo; + this.offset = streamOffset; + } + + @Override + public long getLineNumber() { + return lineNo; + } + + @Override + public long getColumnNumber() { + return columnNo; + } + + @Override + public long getStreamOffset() { + return offset; + } + + @Override + public String toString() { + return "(line no=" + lineNo + ", column no=" + columnNo + ", offset=" + offset + ")"; + } +} diff --git a/java-client/src/main/java/org/opensearch/client/json/JsonpUtils.java b/java-client/src/main/java/org/opensearch/client/json/JsonpUtils.java index 80c9e85baf..c6e1938999 100644 --- a/java-client/src/main/java/org/opensearch/client/json/JsonpUtils.java +++ b/java-client/src/main/java/org/opensearch/client/json/JsonpUtils.java @@ -37,6 +37,7 @@ import jakarta.json.JsonString; import jakarta.json.JsonValue; import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; import jakarta.json.stream.JsonParsingException; @@ -135,20 +136,46 @@ public static void serialize(T value, JsonGenerator generator, @Nullable Jso public static Map.Entry lookAheadFieldValue( String name, String defaultValue, JsonParser parser, JsonpMapper mapper ) { - // FIXME: need a buffering parser wrapper so that we don't roundtrip through a JsonObject and a String - // FIXME: resulting parser should return locations that are offset with the original parser's location - JsonObject object = parser.getObject(); - String result = object.getString(name, null); + JsonLocation location = parser.getLocation(); - if (result == null) { - result = defaultValue; - } + if (parser instanceof LookAheadJsonParser) { + // Fast buffered path + Map.Entry result = ((LookAheadJsonParser) parser).lookAheadFieldValue(name, defaultValue); + if (result.getKey() == null) { + throw new JsonParsingException("Property '" + name + "' not found", location); + } + return result; - if (result == null) { - throw new JsonParsingException("Property '" + name + "' not found", parser.getLocation()); + } else { + // Unbuffered path: parse the object into a JsonObject, then extract the value and parse it again + JsonObject object = parser.getObject(); + String result = object.getString(name, null); + + if (result == null) { + result = defaultValue; + } + + if (result == null) { + throw new JsonParsingException("Property '" + name + "' not found", location); + } + + JsonParser newParser = objectParser(object, mapper); + + // Pin location to the start of the look ahead, as the new parser will return locations in its own buffer + newParser = new DelegatingJsonParser(newParser) { + @Override + public JsonLocation getLocation() { + return new JsonLocationImpl(location.getLineNumber(), location.getColumnNumber(), location.getStreamOffset()) { + @Override + public String toString() { + return "(in object at " + super.toString().substring(1); + } + }; + } + }; + + return new AbstractMap.SimpleImmutableEntry<>(result, newParser); } - - return new AbstractMap.SimpleImmutableEntry<>(result, objectParser(object, mapper)); } /** diff --git a/java-client/src/main/java/org/opensearch/client/json/LookAheadJsonParser.java b/java-client/src/main/java/org/opensearch/client/json/LookAheadJsonParser.java new file mode 100644 index 0000000000..b4a4231d14 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/LookAheadJsonParser.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.client.json; + +import jakarta.json.stream.JsonParser; + +import java.util.Map; + +public interface LookAheadJsonParser extends JsonParser { + + /** + * Look ahead the value of a text property in the JSON stream. The parser must be on the {@code START_OBJECT} event. + * + * @param name the field name to look up. + * @param defaultValue default value if the field is not found. + * @return a pair containing the field value (or {@code null} if not found), and a parser to be used to read the JSON object. + */ + Map.Entry lookAheadFieldValue(String name, String defaultValue); + + /** + * In union types, find the variant to be used by looking up property names in the JSON stream until we find one that + * uniquely identifies the variant. + * + * @param the type of variant descriptors used by the caller. + * @param variants a map of variant descriptors, keyed by the property name that uniquely identifies the variant. + * @return a pair containing the variant descriptor (or {@code null} if not found), and a parser to be used to read the JSON object. + */ + Map.Entry findVariant(Map variants); +} diff --git a/java-client/src/main/java/org/opensearch/client/json/NamedDeserializer.java b/java-client/src/main/java/org/opensearch/client/json/NamedDeserializer.java index 212ee8e6ff..309295e313 100644 --- a/java-client/src/main/java/org/opensearch/client/json/NamedDeserializer.java +++ b/java-client/src/main/java/org/opensearch/client/json/NamedDeserializer.java @@ -71,17 +71,19 @@ public EnumSet acceptedEvents() { @Override public T deserialize(JsonParser parser, JsonpMapper mapper) { - if (mapper.>attribute(name) == null) { - throw new JsonParsingException("Missing deserializer", parser.getLocation()); + JsonpDeserializer deserializer = mapper.attribute(name); + if (deserializer == null) { + throw new JsonParsingException("Missing deserializer for generic type: " + name, parser.getLocation()); } - return mapper.>attribute(name).deserialize(parser, mapper); + return deserializer.deserialize(parser, mapper); } @Override public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) { - if (mapper.>attribute(name) == null) { - throw new JsonParsingException("Missing deserializer", parser.getLocation()); + JsonpDeserializer deserializer = mapper.attribute(name); + if (deserializer == null) { + throw new JsonParsingException("Missing deserializer for generic type: " + name, parser.getLocation()); } - return mapper.>attribute(name).deserialize(parser, mapper, event); + return deserializer.deserialize(parser, mapper, event); } } diff --git a/java-client/src/main/java/org/opensearch/client/json/UnionDeserializer.java b/java-client/src/main/java/org/opensearch/client/json/UnionDeserializer.java index 3905139a80..0e77419d46 100644 --- a/java-client/src/main/java/org/opensearch/client/json/UnionDeserializer.java +++ b/java-client/src/main/java/org/opensearch/client/json/UnionDeserializer.java @@ -34,6 +34,7 @@ import org.opensearch.client.util.ObjectBuilder; import jakarta.json.JsonObject; +import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; import jakarta.json.stream.JsonParsingException; @@ -223,12 +224,12 @@ public JsonpDeserializer build() { private final BiFunction buildFn; private final EnumSet nativeEvents; private final Map> objectMembers; - private final Map> otherMembers; + private final Map> nonObjectMembers; private final EventHandler fallbackObjectMember; public UnionDeserializer( List> objectMembers, - Map> otherMembers, + Map> nonObjectMembers, BiFunction buildFn ) { this.buildFn = buildFn; @@ -245,17 +246,17 @@ public UnionDeserializer( } } - this.otherMembers = otherMembers; + this.nonObjectMembers = nonObjectMembers; this.nativeEvents = EnumSet.noneOf(Event.class); - for (EventHandler member: otherMembers.values()) { + for (EventHandler member: nonObjectMembers.values()) { this.nativeEvents.addAll(member.nativeEvents()); } if (objectMembers.isEmpty()) { fallbackObjectMember = null; } else { - fallbackObjectMember = this.otherMembers.remove(Event.START_OBJECT); + fallbackObjectMember = this.nonObjectMembers.remove(Event.START_OBJECT); this.nativeEvents.add(Event.START_OBJECT); } } @@ -280,17 +281,31 @@ public Union deserialize(JsonParser parser, JsonpMapper mapper) { @Override public Union deserialize(JsonParser parser, JsonpMapper mapper, Event event) { - EventHandler member = otherMembers.get(event); + EventHandler member = nonObjectMembers.get(event); + JsonLocation location = parser.getLocation(); if (member == null && event == Event.START_OBJECT && !objectMembers.isEmpty()) { - // Parse as an object to find matching field names - JsonObject object = parser.getObject(); + if (parser instanceof LookAheadJsonParser) { + Map.Entry, JsonParser> memberAndParser = + ((LookAheadJsonParser) parser).findVariant(objectMembers); - for (String field: object.keySet()) { - member = objectMembers.get(field); - if (member != null) { - break; + member = memberAndParser.getKey(); + // Parse the buffered parser + parser = memberAndParser.getValue(); + + } else { + // Parse as an object to find matching field names + JsonObject object = parser.getObject(); + + for (String field: object.keySet()) { + member = objectMembers.get(field); + if (member != null) { + break; + } } + + // Traverse the object we have inspected + parser = JsonpUtils.objectParser(object, mapper); } if (member == null) { @@ -298,14 +313,12 @@ public Union deserialize(JsonParser parser, JsonpMapper mapper, Event event) { } if (member != null) { - // Traverse the object we have inspected - parser = JsonpUtils.objectParser(object, mapper); event = parser.next(); } } if (member == null) { - throw new JsonParsingException("Cannot determine what union member to deserialize", parser.getLocation()); + throw new JsonParsingException("Cannot determine what union member to deserialize", location); } return member.deserialize(parser, mapper, event, buildFn); diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpGenerator.java b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpGenerator.java index 38b5f48bc5..5b6e5efdff 100644 --- a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpGenerator.java +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpGenerator.java @@ -195,7 +195,7 @@ public JsonGenerator write(String name, double value) { public JsonGenerator write(String name, boolean value) { try { generator.writeFieldName(name); - generator.writeBooleanField(name, value); + generator.writeBoolean(value); } catch (IOException e) { throw JacksonUtils.convertException(e); } diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpMapper.java b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpMapper.java index b6b44e66f3..3a92266ca0 100644 --- a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpMapper.java +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpMapper.java @@ -54,18 +54,20 @@ public class JacksonJsonpMapper extends JsonpMapperBase { private final ObjectMapper objectMapper; public JacksonJsonpMapper(ObjectMapper objectMapper) { - this(objectMapper, new JsonFactory()); + // Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec() + this(objectMapper, objectMapper.getFactory()); } public JacksonJsonpMapper(ObjectMapper objectMapper, JsonFactory jsonFactory) { this.provider = new JacksonJsonProvider(jsonFactory); - this.objectMapper = objectMapper - .configure(SerializationFeature.INDENT_OUTPUT, false) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); + this.objectMapper = objectMapper; } public JacksonJsonpMapper() { - this(new ObjectMapper()); + this(new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, false) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + ); } /** diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpParser.java b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpParser.java index de95dbce0e..eb75a2bb63 100644 --- a/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpParser.java +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/JacksonJsonpParser.java @@ -33,15 +33,20 @@ package org.opensearch.client.json.jackson; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.util.JsonParserSequence; +import com.fasterxml.jackson.databind.util.TokenBuffer; import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.json.JsonValue; import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParsingException; +import org.opensearch.client.json.LookAheadJsonParser; +import org.opensearch.client.json.UnexpectedJsonEventException; import java.io.IOException; import java.math.BigDecimal; +import java.util.AbstractMap; import java.util.EnumMap; import java.util.Map; import java.util.NoSuchElementException; @@ -55,7 +60,7 @@ * getter method (e.g. {@link #getInt()} or {@link #getString()} should be called until the next call to {@link #next()}. * Such calls will throw an {@code IllegalStateException}. */ -public class JacksonJsonpParser implements JsonParser { +public class JacksonJsonpParser implements LookAheadJsonParser { private final com.fasterxml.jackson.core.JsonParser parser; @@ -319,7 +324,100 @@ public Stream getArrayStream() { */ @Override public Stream getValueStream() { - return JsonParser.super.getValueStream(); + return LookAheadJsonParser.super.getValueStream(); + } + + //----- Look ahead methods + + public Map.Entry lookAheadFieldValue(String name, String defaultValue) { + + TokenBuffer tb = new TokenBuffer(parser, null); + + try { + // The resulting parser must contain the full object, including START_EVENT + tb.copyCurrentEvent(parser); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + expectEvent(JsonToken.FIELD_NAME); + // Do not copy current event here, each branch will take care of it + + String fieldName = parser.getCurrentName(); + if (fieldName.equals(name)) { + // Found + tb.copyCurrentEvent(parser); + expectNextEvent(JsonToken.VALUE_STRING); + tb.copyCurrentEvent(parser); + + return new AbstractMap.SimpleImmutableEntry<>( + parser.getText(), + new JacksonJsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } catch (IOException e) { + throw JacksonUtils.convertException(e); + } + + // Field not found + return new AbstractMap.SimpleImmutableEntry<>( + defaultValue, + new JacksonJsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } + + @Override + public Map.Entry findVariant(Map variants) { + // We're on a START_OBJECT event + TokenBuffer tb = new TokenBuffer(parser, null); + + try { + // The resulting parser must contain the full object, including START_EVENT + tb.copyCurrentEvent(parser); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + expectEvent(JsonToken.FIELD_NAME); + String fieldName = parser.getCurrentName(); + + Variant variant = variants.get(fieldName); + if (variant != null) { + tb.copyCurrentEvent(parser); + return new AbstractMap.SimpleImmutableEntry<>( + variant, + new JacksonJsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } catch (IOException e) { + throw JacksonUtils.convertException(e); + } + + // No variant found: return the buffered parser and let the caller decide what to do. + return new AbstractMap.SimpleImmutableEntry<>( + null, + new JacksonJsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } + + private void expectNextEvent(JsonToken expected) throws IOException { + JsonToken event = parser.nextToken(); + if (event != expected) { + throw new UnexpectedJsonEventException(this, tokenToEvent.get(event), tokenToEvent.get(expected)); + } + } + + private void expectEvent(JsonToken expected) { + JsonToken event = parser.currentToken(); + if (event != expected) { + throw new UnexpectedJsonEventException(this, tokenToEvent.get(event), tokenToEvent.get(expected)); + } } } diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/JacksonJsonpParserTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/JacksonJsonpParserTest.java index 33c736b168..21d7ff3fa2 100644 --- a/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/JacksonJsonpParserTest.java +++ b/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/JacksonJsonpParserTest.java @@ -32,15 +32,25 @@ package org.opensearch.client.opensearch.json.jackson; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.JsonpMapperBase; import org.opensearch.client.json.jackson.JacksonJsonProvider; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.opensearch.core.MsearchResponse; +import org.opensearch.client.opensearch.model.ModelTestCase; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; -import org.junit.Assert; import org.junit.Test; +import javax.annotation.Nullable; import java.io.StringReader; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; -public class JacksonJsonpParserTest extends Assert { +public class JacksonJsonpParserTest extends ModelTestCase { private static final String json = "{ 'foo': 'fooValue', 'bar': { 'baz': 1}, 'quux': [true] }".replace('\'', '"'); @@ -108,4 +118,136 @@ public void testForbidValueGettersAfterHasNext() { // expected } } + + @Test + public void testMultiSearchResponse() { + String json = + "{\n" + + " \"took\" : 1,\n" + + " \"responses\" : [\n" + + " {\n" + + " \"error\" : {\n" + + " \"root_cause\" : [\n" + + " {\n" + + " \"type\" : \"index_not_found_exception\",\n" + + " \"reason\" : \"no such index [foo_bar]\",\n" + + " \"resource.type\" : \"index_or_alias\",\n" + + " \"resource.id\" : \"foo_bar\",\n" + + " \"index_uuid\" : \"_na_\",\n" + + " \"index\" : \"foo_bar\"\n" + + " }\n" + + " ],\n" + + " \"type\" : \"index_not_found_exception\",\n" + + " \"reason\" : \"no such index [foo_bar]\",\n" + + " \"resource.type\" : \"index_or_alias\",\n" + + " \"resource.id\" : \"foo_bar\",\n" + + " \"index_uuid\" : \"_na_\",\n" + + " \"index\" : \"foo_bar\"\n" + + " },\n" + + " \"status\" : 404\n" + + " },\n" + + " {\n" + + " \"took\" : 1,\n" + + " \"timed_out\" : false,\n" + + " \"_shards\" : {\n" + + " \"total\" : 1,\n" + + " \"successful\" : 1,\n" + + " \"skipped\" : 0,\n" + + " \"failed\" : 0\n" + + " },\n" + + " \"hits\" : {\n" + + " \"total\" : {\n" + + " \"value\" : 5,\n" + + " \"relation\" : \"eq\"\n" + + " },\n" + + " \"max_score\" : 1.0,\n" + + " \"hits\" : [\n" + + " {\n" + + " \"_index\" : \"foo\",\n" + + " \"_id\" : \"Wr0ApoEBa_iiaABtVM57\",\n" + + " \"_score\" : 1.0,\n" + + " \"_source\" : {\n" + + " \"x\" : 1,\n" + + " \"y\" : true\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"status\" : 200\n" + + " }\n" + + " ]\n" + + "}\n"; + + JsonpMapper mapper = new AttributedJacksonJsonpMapper() + .withAttribute("org.opensearch.client:Deserializer:_global.msearch.TDocument", JsonpDeserializer.of(Foo.class)); + + @SuppressWarnings("unchecked") + MsearchResponse response = fromJson(json, MsearchResponse.class, mapper); + + assertEquals(2, response.responses().size()); + assertEquals(404, response.responses().get(0).failure().status()); + assertEquals(200, response.responses().get(1).result().status()); + } + + public static class AttributedJacksonJsonpMapper extends JacksonJsonpMapper { + private Map attributes; + + public AttributedJacksonJsonpMapper() { + super(); + } + + public AttributedJacksonJsonpMapper(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T attribute(String name) { + return attributes == null ? null : (T)attributes.get(name); + } + + /** + * Updates attributes to a copy of the current ones with an additional key/value pair. + * Mutates the current mapper, intended to be used in implementations of {@link #withAttribute(String, Object)} + */ + protected JsonpMapperBase addAttribute(String name, Object value) { + if (attributes == null) { + this.attributes = Collections.singletonMap(name, value); + } else { + Map newAttrs = new HashMap<>(attributes.size() + 1); + newAttrs.putAll(attributes); + newAttrs.put(name, value); + this.attributes = newAttrs; + } + return this; + } + + @Override + public JsonpMapper withAttribute(String name, T value) { + return new AttributedJacksonJsonpMapper(objectMapper()).addAttribute(name, value); + } + } + + public static class Foo { + private int x; + private boolean y; + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public boolean isY() { + return y; + } + + public void setY(boolean y) { + this.y = y; + } + } + } diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/model/VariantsTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/model/VariantsTest.java index 86732b5524..2124f3e729 100644 --- a/java-client/src/test/java/org/opensearch/client/opensearch/model/VariantsTest.java +++ b/java-client/src/test/java/org/opensearch/client/opensearch/model/VariantsTest.java @@ -190,6 +190,14 @@ public void testNestedTaggedUnionWithDefaultTag() { assertEquals(256, mappings.properties().get("id").text().fields().get("keyword").keyword().ignoreAbove().longValue()); } + @Test + public void testEmptyProperty() { + // Edge case where we have a property with no fields and no type + String json = "{}"; + Property property = fromJson(json, Property.class); + assertEquals(Property.Kind.Object, property._kind()); + } + @Test public void testNestedVariantsWithContainerProperties() {