From af5e622b21f9f17285a7087a281b76e9f895373e Mon Sep 17 00:00:00 2001 From: cthiele42 Date: Sun, 15 Feb 2026 13:14:13 +0100 Subject: [PATCH] ISSUE 1810 Add Jackson3JsonpMapper - add Jackson3JsonpMapper and companion classes - add Jackson3 test - add jackson3 dependencies Signed-off-by: Claas Thiele --- java-client/build.gradle.kts | 3 + .../json/jackson/Jackson3JsonProvider.java | 297 +++++++++++++ .../json/jackson/Jackson3JsonValueParser.java | 116 +++++ .../json/jackson/Jackson3JsonpGenerator.java | 386 ++++++++++++++++ .../json/jackson/Jackson3JsonpLocation.java | 69 +++ .../json/jackson/Jackson3JsonpMapper.java | 147 ++++++ .../json/jackson/Jackson3JsonpParser.java | 420 ++++++++++++++++++ .../client/json/jackson/Jackson3Utils.java | 55 +++ .../json/jackson/Jackson3JsonpParserTest.java | 208 +++++++++ 9 files changed, 1701 insertions(+) create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonProvider.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonValueParser.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpGenerator.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpLocation.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpMapper.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpParser.java create mode 100644 java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3Utils.java create mode 100644 java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/Jackson3JsonpParserTest.java diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index 59568ff7a3..c22cd64a7e 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -180,6 +180,7 @@ val opensearchVersion = "3.5.0-SNAPSHOT" dependencies { val jacksonVersion = "2.20.1" + val jackson3Version = "3.0.4" val jacksonDatabindVersion = "2.20.1" // Apache 2.0 @@ -217,6 +218,8 @@ dependencies { // Apache 2.0 implementation("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) implementation("com.fasterxml.jackson.core", "jackson-databind", jacksonDatabindVersion) + implementation("tools.jackson.core", "jackson-core", jackson3Version) + implementation("tools.jackson.core", "jackson-databind", jackson3Version) testImplementation("com.fasterxml.jackson.datatype", "jackson-datatype-jakarta-jsonp", jacksonVersion) // For AwsSdk2Transport diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonProvider.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonProvider.java new file mode 100644 index 0000000000..8421c6ddcc --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonProvider.java @@ -0,0 +1,297 @@ +/* + * 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.jackson; + +import jakarta.json.*; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonGeneratorFactory; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParserFactory; +import tools.jackson.core.JacksonException; +import tools.jackson.core.json.JsonFactory; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Map; + +/** + * A partial implementation of JSONP's SPI on top of Jackson. + */ +public class Jackson3JsonProvider extends JsonProvider { + + private final JsonFactory jsonFactory; + + public Jackson3JsonProvider(JsonFactory jsonFactory) { + this.jsonFactory = jsonFactory; + } + + public Jackson3JsonProvider() { + this(new JsonFactory()); + } + + /** + * Return the underlying Jackson {@link JsonFactory}. + */ + public JsonFactory jacksonJsonFactory() { + return this.jsonFactory; + } + + // --------------------------------------------------------------------------------------------- + // Parser + + private final ParserFactory defaultParserFactory = new ParserFactory(null); + + @Override + public JsonParserFactory createParserFactory(Map config) { + if (config == null || config.isEmpty()) { + return defaultParserFactory; + } else { + // TODO: handle specific configuration + return defaultParserFactory; + } + } + + @Override + public JsonParser createParser(Reader reader) { + return defaultParserFactory.createParser(reader); + } + + @Override + public JsonParser createParser(InputStream in) { + return defaultParserFactory.createParser(in); + } + + private class ParserFactory implements JsonParserFactory { + + private final Map config; + + ParserFactory(Map config) { + this.config = config == null ? Collections.emptyMap() : config; + } + + @Override + public JsonParser createParser(Reader reader) { + try { + return new Jackson3JsonpParser(jsonFactory.createParser(reader)); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + @Override + public JsonParser createParser(InputStream in) { + try { + return new Jackson3JsonpParser(jsonFactory.createParser(in)); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + @Override + public JsonParser createParser(InputStream in, Charset charset) { + try { + return new Jackson3JsonpParser(jsonFactory.createParser(new InputStreamReader(in, charset))); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + /** + * Not implemented. + */ + @Override + public JsonParser createParser(JsonObject obj) { + return JsonProvider.provider().createParserFactory(null).createParser(obj); + } + + /** + * Not implemented. + */ + @Override + public JsonParser createParser(JsonArray array) { + return JsonProvider.provider().createParserFactory(null).createParser(array); + } + + /** + * Not implemented. + */ + @Override + public Map getConfigInUse() { + return config; + } + } + + // --------------------------------------------------------------------------------------------- + // Generator + + private final JsonGeneratorFactory defaultGeneratorFactory = new GeneratorFactory(null); + + @Override + public JsonGeneratorFactory createGeneratorFactory(Map config) { + if (config == null || config.isEmpty()) { + return defaultGeneratorFactory; + } else { + // TODO: handle specific configuration + return defaultGeneratorFactory; + } + } + + @Override + public JsonGenerator createGenerator(Writer writer) { + return defaultGeneratorFactory.createGenerator(writer); + } + + @Override + public JsonGenerator createGenerator(OutputStream out) { + return defaultGeneratorFactory.createGenerator(out); + } + + private class GeneratorFactory implements JsonGeneratorFactory { + + private final Map config; + + GeneratorFactory(Map config) { + this.config = config == null ? Collections.emptyMap() : config; + } + + @Override + public JsonGenerator createGenerator(Writer writer) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(writer)); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + @Override + public JsonGenerator createGenerator(OutputStream out) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(out)); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + @Override + public JsonGenerator createGenerator(OutputStream out, Charset charset) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, charset))); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + + } + + @Override + public Map getConfigInUse() { + return config; + } + } + + // --------------------------------------------------------------------------------------------- + // Unsupported operations + + /** + * Not implemented. + */ + @Override + public JsonReader createReader(Reader reader) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonReader createReader(InputStream in) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonWriter createWriter(Writer writer) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonWriter createWriter(OutputStream out) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonWriterFactory createWriterFactory(Map config) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonReaderFactory createReaderFactory(Map config) { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonObjectBuilder createObjectBuilder() { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonArrayBuilder createArrayBuilder() { + throw new UnsupportedOperationException(); + } + + /** + * Not implemented. + */ + @Override + public JsonBuilderFactory createBuilderFactory(Map config) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonValueParser.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonValueParser.java new file mode 100644 index 0000000000..f3a399ffa1 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonValueParser.java @@ -0,0 +1,116 @@ +/* + * 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.jackson; + +import jakarta.json.*; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +/** + * Reads a Jsonp value/object/array from a Jackson parser. The parser's current token should be the start of the + * object (e.g. START_OBJECT, VALUE_NUMBER, etc). + */ +class Jackson3JsonValueParser { + private static class DefaultJsonProvider { + private static final JsonProvider INSTANCE = JsonProvider.provider(); + } + + public JsonObject parseObject(JsonParser parser) { + + JsonObjectBuilder ob = DefaultJsonProvider.INSTANCE.createObjectBuilder(); + + JsonToken token; + while ((token = parser.nextToken()) != JsonToken.END_OBJECT) { + if (token != JsonToken.PROPERTY_NAME) { + throw new JsonParsingException("Expected a property name", new Jackson3JsonpLocation(parser)); + } + String name = parser.currentName(); + parser.nextToken(); + ob.add(name, parseValue(parser)); + } + return ob.build(); + } + + public JsonArray parseArray(JsonParser parser) { + JsonArrayBuilder ab = DefaultJsonProvider.INSTANCE.createArrayBuilder(); + + while (parser.nextToken() != JsonToken.END_ARRAY) { + ab.add(parseValue(parser)); + } + return ab.build(); + } + + public JsonValue parseValue(JsonParser parser) { + switch (parser.currentToken()) { + case START_OBJECT: + return parseObject(parser); + + case START_ARRAY: + return parseArray(parser); + + case VALUE_TRUE: + return JsonValue.TRUE; + + case VALUE_FALSE: + return JsonValue.FALSE; + + case VALUE_NULL: + return JsonValue.NULL; + + case VALUE_STRING: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getString()); + + case VALUE_NUMBER_FLOAT: + case VALUE_NUMBER_INT: + switch (parser.getNumberType()) { + case INT: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getIntValue()); + case LONG: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getLongValue()); + case FLOAT: + case DOUBLE: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getDoubleValue()); + case BIG_DECIMAL: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getDecimalValue()); + case BIG_INTEGER: + return Jackson3JsonValueParser.DefaultJsonProvider.INSTANCE.createValue(parser.getBigIntegerValue()); + } + + default: + throw new JsonParsingException("Unexpected token '" + parser.currentToken() + "'", new Jackson3JsonpLocation(parser)); + + } + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpGenerator.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpGenerator.java new file mode 100644 index 0000000000..7ccc9bfbbb --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpGenerator.java @@ -0,0 +1,386 @@ +/* + * 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.jackson; + +import jakarta.json.JsonNumber; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerationException; +import jakarta.json.stream.JsonGenerator; +import tools.jackson.core.JacksonException; +import tools.jackson.core.TokenStreamContext; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Map; + +/** + * A JSONP generator implementation on top of Jackson. + */ +public class Jackson3JsonpGenerator implements JsonGenerator { + + private final tools.jackson.core.JsonGenerator generator; + + public Jackson3JsonpGenerator(tools.jackson.core.JsonGenerator generator) { + this.generator = generator; + } + + /** + * Returns the underlying Jackson generator. + */ + public tools.jackson.core.JsonGenerator jacksonGenerator() { + return generator; + } + + @Override + public JsonGenerator writeStartObject() { + try { + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartObject(String name) { + try { + generator.writeName(name); + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartArray() { + try { + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartArray(String name) { + try { + generator.writeName(name); + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeKey(String name) { + try { + generator.writeName(name); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, JsonValue value) { + try { + generator.writeName(name); + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, String value) { + try { + generator.writeName(name); + generator.writeString(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, BigInteger value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, BigDecimal value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, int value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, long value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, double value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, boolean value) { + try { + generator.writeName(name); + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeNull(String name) { + try { + generator.writeName(name); + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeEnd() { + try { + TokenStreamContext ctx = generator.streamWriteContext(); + if (ctx.inObject()) { + generator.writeEndObject(); + } else if (ctx.inArray()) { + generator.writeEndArray(); + } else { + throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'"); + } + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(JsonValue value) { + try { + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String value) { + try { + generator.writeString(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(BigDecimal value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(BigInteger value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(int value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(long value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(double value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(boolean value) { + try { + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeNull() { + try { + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public void close() { + try { + generator.close(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public void flush() { + try { + generator.flush(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + private void writeValue(JsonValue value) throws JacksonException { + switch (value.getValueType()) { + case OBJECT: + generator.writeStartObject(); + for (Map.Entry entry : value.asJsonObject().entrySet()) { + generator.writeName(entry.getKey()); + writeValue(entry.getValue()); + } + generator.writeEndObject(); + break; + + case ARRAY: + generator.writeStartArray(); + for (JsonValue item : value.asJsonArray()) { + writeValue(item); + } + generator.writeEndArray(); + break; + + case STRING: + generator.writeString(((JsonString) value).getString()); + break; + + case FALSE: + generator.writeBoolean(false); + break; + + case TRUE: + generator.writeBoolean(true); + break; + + case NULL: + generator.writeNull(); + break; + + case NUMBER: + JsonNumber n = (JsonNumber) value; + if (n.isIntegral()) { + generator.writeNumber(n.longValue()); + } else { + generator.writeNumber(n.doubleValue()); + } + break; + } + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpLocation.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpLocation.java new file mode 100644 index 0000000000..3df02af613 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpLocation.java @@ -0,0 +1,69 @@ +/* + * 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.jackson; + +import jakarta.json.stream.JsonLocation; +import tools.jackson.core.JsonParser; +import tools.jackson.core.TokenStreamLocation; + +/** + * Translate a Jackson location to a JSONP location. + */ +public class Jackson3JsonpLocation implements JsonLocation { + + private final TokenStreamLocation location; + + Jackson3JsonpLocation(TokenStreamLocation location) { + this.location = location; + } + + Jackson3JsonpLocation(JsonParser parser) { + this(parser.currentTokenLocation()); + } + + @Override + public long getLineNumber() { + return location.getLineNr(); + } + + @Override + public long getColumnNumber() { + return location.getColumnNr(); + } + + @Override + public long getStreamOffset() { + long charOffset = location.getCharOffset(); + return charOffset == -1 ? location.getByteOffset() : charOffset; + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpMapper.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpMapper.java new file mode 100644 index 0000000000..183419608b --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpMapper.java @@ -0,0 +1,147 @@ +/* + * 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.jackson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import org.opensearch.client.json.*; +import tools.jackson.core.JacksonException; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import java.util.EnumSet; + +public class Jackson3JsonpMapper extends JsonpMapperBase { + private final Jackson3JsonProvider provider; + private final ObjectMapper objectMapper; + + public Jackson3JsonpMapper(ObjectMapper objectMapper) { + // Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec() + this(objectMapper, (JsonFactory) objectMapper.tokenStreamFactory()); + } + + public Jackson3JsonpMapper(ObjectMapper objectMapper, JsonFactory jsonFactory) { + this.provider = new Jackson3JsonProvider(jsonFactory); + this.objectMapper = objectMapper; + } + + public Jackson3JsonpMapper() { + this( + JsonMapper.builder().configure(SerializationFeature.INDENT_OUTPUT, false) + .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) + .changeDefaultPropertyInclusion(incl -> incl.withContentInclusion(JsonInclude.Include.NON_NULL)) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build() + ); + } + + private Jackson3JsonpMapper(Jackson3JsonpMapper o) { + super(o); + this.provider = o.provider; + this.objectMapper = o.objectMapper; + } + + @Override + public JsonpMapper withAttribute(String name, T value) { + return new Jackson3JsonpMapper(this).addAttribute(name, value); + } + + /** + * Returns the underlying Jackson mapper. + */ + public ObjectMapper objectMapper() { + return this.objectMapper; + } + + @Override + public JsonProvider jsonProvider() { + return provider; + } + + @Override + protected JsonpDeserializer getDefaultDeserializer(Class clazz) { + return new JacksonValueParser<>(clazz); + } + + @Override + public void serialize(T value, JsonGenerator generator) { + + if (!(generator instanceof Jackson3JsonpGenerator)) { + throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the Jackson3JsonpProvider"); + } + + JsonpSerializer serializer = findSerializer(value); + if (serializer != null) { + serializer.serialize(value, generator, this); + return; + } + + tools.jackson.core.JsonGenerator jkGenerator = ((Jackson3JsonpGenerator) generator).jacksonGenerator(); + try { + objectMapper.writeValue(jkGenerator, value); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + + private class JacksonValueParser extends JsonpDeserializerBase { + + private final Class clazz; + + protected JacksonValueParser(Class clazz) { + super(EnumSet.allOf(JsonParser.Event.class)); + this.clazz = clazz; + } + + @Override + public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) { + + if (!(parser instanceof Jackson3JsonpParser)) { + throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider"); + } + + tools.jackson.core.JsonParser jkParser = ((Jackson3JsonpParser) parser).jacksonParser(); + + try { + return objectMapper.readValue(jkParser, clazz); + } catch (JacksonException je) { + throw Jackson3Utils.convertException(je); + } + } + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpParser.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpParser.java new file mode 100644 index 0000000000..ed9ba71e85 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3JsonpParser.java @@ -0,0 +1,420 @@ +/* + * 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.jackson; + +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 tools.jackson.core.JacksonException; +import tools.jackson.core.JsonToken; +import tools.jackson.core.util.JsonParserSequence; +import tools.jackson.databind.util.TokenBuffer; + +import java.math.BigDecimal; +import java.util.AbstractMap; +import java.util.EnumMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +/** + * A JSONP parser implementation on top of Jackson. + *

+ * Warning: this implementation isn't fully compliant with the JSONP specification: calling {@link #hasNext()} + * moves forward the underlying Jackson parser as Jackson doesn't provide an equivalent method. This means no value + * 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 Jackson3JsonpParser implements LookAheadJsonParser { + + private final tools.jackson.core.JsonParser parser; + + private boolean hasNextWasCalled = false; + + private static final EnumMap tokenToEvent; + + static { + tokenToEvent = new EnumMap<>(JsonToken.class); + tokenToEvent.put(JsonToken.END_ARRAY, Event.END_ARRAY); + tokenToEvent.put(JsonToken.END_OBJECT, Event.END_OBJECT); + tokenToEvent.put(JsonToken.PROPERTY_NAME, Event.KEY_NAME); + tokenToEvent.put(JsonToken.START_ARRAY, Event.START_ARRAY); + tokenToEvent.put(JsonToken.START_OBJECT, Event.START_OBJECT); + tokenToEvent.put(JsonToken.VALUE_FALSE, Event.VALUE_FALSE); + tokenToEvent.put(JsonToken.VALUE_NULL, Event.VALUE_NULL); + tokenToEvent.put(JsonToken.VALUE_NUMBER_FLOAT, Event.VALUE_NUMBER); + tokenToEvent.put(JsonToken.VALUE_NUMBER_INT, Event.VALUE_NUMBER); + tokenToEvent.put(JsonToken.VALUE_STRING, Event.VALUE_STRING); + tokenToEvent.put(JsonToken.VALUE_TRUE, Event.VALUE_TRUE); + + // No equivalent for + // - VALUE_EMBEDDED_OBJECT + // - NOT_AVAILABLE + } + + public Jackson3JsonpParser(tools.jackson.core.JsonParser parser) { + this.parser = parser; + } + + /** + * Returns the underlying Jackson parser. + */ + public tools.jackson.core.JsonParser jacksonParser() { + return this.parser; + } + + private JsonParsingException convertException(JacksonException je) { + return new JsonParsingException("Jackson exception: " + je.getMessage(), je, getLocation()); + } + + private JsonToken fetchNextToken() { + try { + return parser.nextToken(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + private void ensureTokenIsCurrent() { + if (hasNextWasCalled) { + throw new IllegalStateException("Cannot get event data as parser as already been moved to the next event"); + } + } + + @Override + public boolean hasNext() { + if (hasNextWasCalled) { + return parser.currentToken() != null; + } else { + hasNextWasCalled = true; + return fetchNextToken() != null; + } + } + + @Override + public Event next() { + JsonToken token; + if (hasNextWasCalled) { + token = parser.currentToken(); + hasNextWasCalled = false; + } else { + token = fetchNextToken(); + } + + if (token == null) { + throw new NoSuchElementException(); + } + + Event result = tokenToEvent.get(token); + if (result == null) { + throw new JsonParsingException("Unsupported Jackson event type '" + token + "'", getLocation()); + } + + return result; + } + + @Override + public String getString() { + ensureTokenIsCurrent(); + try { + return parser.getValueAsString(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public boolean isIntegralNumber() { + ensureTokenIsCurrent(); + return parser.isExpectedNumberIntToken(); + } + + @Override + public int getInt() { + ensureTokenIsCurrent(); + try { + return parser.getIntValue(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public long getLong() { + ensureTokenIsCurrent(); + try { + return parser.getLongValue(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public BigDecimal getBigDecimal() { + ensureTokenIsCurrent(); + try { + return parser.getDecimalValue(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public JsonLocation getLocation() { + return new Jackson3JsonpLocation(parser.currentLocation()); + } + + @Override + public void close() { + try { + parser.close(); + } catch (JacksonException e) { + throw convertException(e); + } + } + + private Jackson3JsonValueParser valueParser; + + @Override + public JsonObject getObject() { + ensureTokenIsCurrent(); + if (parser.currentToken() != JsonToken.START_OBJECT) { + throw new IllegalStateException("Unexpected event '" + parser.currentToken() + "' at " + parser.currentTokenLocation()); + } + if (valueParser == null) { + valueParser = new Jackson3JsonValueParser(); + } + try { + return valueParser.parseObject(parser); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public JsonArray getArray() { + ensureTokenIsCurrent(); + if (valueParser == null) { + valueParser = new Jackson3JsonValueParser(); + } + if (parser.currentToken() != JsonToken.START_ARRAY) { + throw new IllegalStateException("Unexpected event '" + parser.currentToken() + "' at " + parser.currentTokenLocation()); + } + try { + return valueParser.parseArray(parser); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public JsonValue getValue() { + ensureTokenIsCurrent(); + if (valueParser == null) { + valueParser = new Jackson3JsonValueParser(); + } + try { + return valueParser.parseValue(parser); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public void skipObject() { + ensureTokenIsCurrent(); + if (parser.currentToken() != JsonToken.START_OBJECT) { + return; + } + + try { + int depth = 1; + JsonToken token; + do { + token = parser.nextToken(); + switch (token) { + case START_OBJECT: + depth++; + break; + case END_OBJECT: + depth--; + break; + } + } while (!(token == JsonToken.END_OBJECT && depth == 0)); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public void skipArray() { + ensureTokenIsCurrent(); + if (parser.currentToken() != JsonToken.START_ARRAY) { + return; + } + + try { + int depth = 1; + JsonToken token; + do { + token = parser.nextToken(); + switch (token) { + case START_ARRAY: + depth++; + break; + case END_ARRAY: + depth--; + break; + } + } while (!(token == JsonToken.END_ARRAY && depth == 0)); + } catch (JacksonException e) { + throw convertException(e); + } + } + + @Override + public Stream> getObjectStream() { + return getObject().entrySet().stream(); + } + + @Override + public Stream getArrayStream() { + return getArray().stream(); + } + + /** + * Not implemented. + */ + @Override + public Stream getValueStream() { + return LookAheadJsonParser.super.getValueStream(); + } + + // ----- Look ahead methods + + public Map.Entry lookAheadFieldValue(String name, String defaultValue) { + + TokenBuffer tb = TokenBuffer.forBuffering(parser, parser.objectReadContext()); + + try { + // The resulting parser must contain the full object, including START_EVENT + tb.copyCurrentEvent(parser); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + expectEvent(JsonToken.PROPERTY_NAME); + // Do not copy current event here, each branch will take care of it + + String fieldName = parser.currentName(); + if (fieldName.equals(name)) { + // Found + tb.copyCurrentEvent(parser); + expectNextEvent(JsonToken.VALUE_STRING); + tb.copyCurrentEvent(parser); + + return new AbstractMap.SimpleImmutableEntry<>( + parser.getString(), + new Jackson3JsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + + // Field not found + return new AbstractMap.SimpleImmutableEntry<>( + defaultValue, + new Jackson3JsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } + + @Override + public Map.Entry findVariant(Map variants) { + // We're on a START_OBJECT event + TokenBuffer tb = TokenBuffer.forBuffering(parser, parser.objectReadContext()); + + try { + // The resulting parser must contain the full object, including START_EVENT + tb.copyCurrentEvent(parser); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + expectEvent(JsonToken.PROPERTY_NAME); + String fieldName = parser.currentName(); + + Variant variant = variants.get(fieldName); + if (variant != null) { + tb.copyCurrentEvent(parser); + return new AbstractMap.SimpleImmutableEntry<>( + variant, + new Jackson3JsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + + // No variant found: return the buffered parser and let the caller decide what to do. + return new AbstractMap.SimpleImmutableEntry<>( + null, + new Jackson3JsonpParser(JsonParserSequence.createFlattened(false, tb.asParser(), parser)) + ); + } + + private void expectNextEvent(JsonToken expected) { + 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)); + } + } +} \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3Utils.java b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3Utils.java new file mode 100644 index 0000000000..bc8d8fc95f --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/json/jackson/Jackson3Utils.java @@ -0,0 +1,55 @@ +/* + * 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.jackson; + +import jakarta.json.JsonException; +import jakarta.json.stream.JsonGenerationException; +import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JacksonException; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.core.exc.StreamWriteException; + +class Jackson3Utils { + public static JsonException convertException(JacksonException je) { + if (je instanceof StreamWriteException) { + return new JsonGenerationException(je.getMessage(), je); + + } else if (je instanceof StreamReadException) { + StreamReadException jpe = (StreamReadException)je; + return new JsonParsingException(je.getMessage(), jpe, new Jackson3JsonpLocation(jpe.getLocation())); + + } else { + return new JsonException("Jackson exception", je); + } + } +} \ No newline at end of file diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/Jackson3JsonpParserTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/Jackson3JsonpParserTest.java new file mode 100644 index 0000000000..668281292f --- /dev/null +++ b/java-client/src/test/java/org/opensearch/client/opensearch/json/jackson/Jackson3JsonpParserTest.java @@ -0,0 +1,208 @@ +/* + * 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.opensearch.json.jackson; + +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParser.Event; +import org.junit.Test; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.jackson.Jackson3JsonProvider; +import org.opensearch.client.json.jackson.Jackson3JsonpMapper; +import org.opensearch.client.opensearch.core.MsearchResponse; +import org.opensearch.client.opensearch.model.ModelTestCase; + +import java.io.StringReader; + +public class Jackson3JsonpParserTest extends ModelTestCase { + + private static final String json = "{ 'foo': 'fooValue', 'bar': { 'baz': 1}, 'quux': [true] }".replace('\'', '"'); + + @Test + public void testEventStream() { + + Jackson3JsonProvider provider = new Jackson3JsonProvider(); + JsonParser parser = provider.createParser(new StringReader(json)); + + assertEquals(Event.START_OBJECT, parser.next()); + + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("foo", parser.getString()); + + assertEquals(Event.VALUE_STRING, parser.next()); + assertEquals("fooValue", parser.getString()); + + // test it sometimes, but not always to detect invalid state management + assertTrue(parser.hasNext()); + + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("bar", parser.getString()); + + assertEquals(Event.START_OBJECT, parser.next()); + + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("baz", parser.getString()); + + assertTrue(parser.hasNext()); + assertEquals(Event.VALUE_NUMBER, parser.next()); + assertEquals(1, parser.getInt()); + + assertEquals(Event.END_OBJECT, parser.next()); + + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals("quux", parser.getString()); + + assertEquals(Event.START_ARRAY, parser.next()); + + assertEquals(Event.VALUE_TRUE, parser.next()); + + assertEquals(Event.END_ARRAY, parser.next()); + assertEquals(Event.END_OBJECT, parser.next()); + + assertFalse(parser.hasNext()); + } + + @Test + public void testForbidValueGettersAfterHasNext() { + + Jackson3JsonProvider provider = new Jackson3JsonProvider(); + JsonParser parser = provider.createParser(new StringReader(json)); + + assertEquals(Event.START_OBJECT, parser.next()); + assertEquals(Event.KEY_NAME, parser.next()); + assertEquals(Event.VALUE_STRING, parser.next()); + assertEquals("fooValue", parser.getString()); + + assertTrue(parser.hasNext()); + + try { + assertEquals("fooValue", parser.getString()); + fail(); + } catch (IllegalStateException e) { + // 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 Jackson3JsonpMapper().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().intValue()); + assertEquals((Integer) 200, response.responses().get(1).result().status()); + } + + 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; + } + } + +}