From 91faf3044d33af2f71a430f71f039c76a5e55d1c Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Wed, 1 Oct 2025 13:02:37 +0200 Subject: [PATCH 01/11] add jackson 3 support --- java-client/build.gradle.kts | 6 + .../json/jackson/Jackson3JsonBuffer.java | 113 +++++ .../json/jackson/Jackson3JsonProvider.java | 274 ++++++++++++ .../json/jackson/Jackson3JsonValueParser.java | 205 +++++++++ .../json/jackson/Jackson3JsonpGenerator.java | 306 ++++++++++++++ .../json/jackson/Jackson3JsonpLocation.java | 62 +++ .../json/jackson/Jackson3JsonpMapper.java | 152 +++++++ .../json/jackson/Jackson3JsonpParser.java | 398 ++++++++++++++++++ 8 files changed, 1516 insertions(+) create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonValueParser.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpLocation.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index bc24b71b7..b978d3d4a 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -224,6 +224,12 @@ dependencies { implementation("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) implementation("com.fasterxml.jackson.core", "jackson-databind", jacksonVersion) + // Apache 2.0 + // https://github.com/FasterXML/jackson + implementation("tools.jackson.core", "jackson-databind", "3.0.0-rc10") + implementation("tools.jackson.core", "jackson-core", "3.0.0-rc10") + implementation("com.fasterxml.jackson.core","jackson-annotations","3.0-rc5") + // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ testImplementation("org.eclipse", "yasson", "3.0.4") diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java new file mode 100644 index 000000000..5a76bd914 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java @@ -0,0 +1,113 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.JsonBuffer; +import co.elastic.clients.json.JsonData; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpUtils; + +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import tools.jackson.databind.util.TokenBuffer; + +import java.io.StringWriter; +import java.lang.reflect.Type; + +class Jackson3JsonBuffer implements JsonBuffer, JsonData { + private final TokenBuffer buffer; + private final Jackson3JsonpMapper mapper; + + Jackson3JsonBuffer(TokenBuffer buffer, Jackson3JsonpMapper mapper) { + this.buffer = buffer; + this.mapper = mapper; + } + + @Override + public JsonParser asParser() { + return new Jackson3JsonpParser(buffer.asParser(), mapper); + } + + @Override + public JsonValue toJson() { + try (JsonParser parser = asParser()) { + parser.next(); // move to first event + return parser.getValue(); + } + } + + @Override + public JsonValue toJson(JsonpMapper mapper) { + // We don't need the mapper + return toJson(); + } + + @Override + public T to(Type type) { + return to(type, this.mapper); + } + + @Override + public T to(Type type, JsonpMapper mapper) { + try (JsonParser parser = asParser()) { + return mapper.deserialize(parser, type); + } + } + + @Override + public T deserialize(JsonpDeserializer deserializer) { + return deserialize(deserializer, this.mapper); + } + + @Override + public T deserialize(JsonpDeserializer deserializer, JsonpMapper mapper) { + try (JsonParser parser = asParser()) { + return deserializer.deserialize(parser, mapper); + } + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + if (generator instanceof Jackson3JsonpGenerator) { + Jackson3JsonpGenerator jkGenerator = (Jackson3JsonpGenerator) generator; + buffer.serialize(jkGenerator.jacksonGenerator()); + } else { + try (JsonParser parser = asParser()) { + JsonpUtils.copy(parser, generator); + } + } + } + + /** + * Renders this buffer as a JSON string for debugger and logging convenience. + */ + @Override + public String toString() { + StringWriter writer = new StringWriter(); + try (Jackson3JsonpGenerator generator = + new Jackson3JsonpGenerator(mapper.objectMapper().createGenerator(writer))) { + serialize(generator, mapper); + generator.close(); + return writer.toString(); + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java new file mode 100644 index 000000000..b8c7f28cf --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java @@ -0,0 +1,274 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.JsonpUtils; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonReader; +import jakarta.json.JsonReaderFactory; +import jakarta.json.JsonWriter; +import jakarta.json.JsonWriterFactory; +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.json.JsonFactory; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +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 Jackson3JsonpMapper mapper; + private final JsonFactory jsonFactory; + + public Jackson3JsonProvider(Jackson3JsonpMapper mapper) { + this.mapper = mapper; + this.jsonFactory = mapper.objectMapper().tokenStreamFactory(); + } + + public Jackson3JsonProvider() { + this(new Jackson3JsonpMapper()); + } + + public Jackson3JsonpMapper mapper() { + return this.mapper; + } + + //--------------------------------------------------------------------------------------------- + // 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) { + return new Jackson3JsonpParser(jsonFactory.createParser(reader), mapper); + } + + @Override + public JsonParser createParser(InputStream in) { + return new Jackson3JsonpParser(jsonFactory.createParser(in), mapper); + } + + @Override + public JsonParser createParser(InputStream in, Charset charset) { + return new Jackson3JsonpParser(jsonFactory.createParser(new InputStreamReader(in, charset)), + mapper); + } + + /** + * Not implemented. + */ + @Override + public JsonParser createParser(JsonObject obj) { + return JsonpUtils.systemProvider().createParserFactory(null).createParser(obj); + } + + /** + * Not implemented. + */ + @Override + public JsonParser createParser(JsonArray array) { + return JsonpUtils.systemProvider().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) { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(writer)); + } + + @Override + public JsonGenerator createGenerator(OutputStream out) { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(out)); + } + + @Override + public JsonGenerator createGenerator(OutputStream out, Charset charset) { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, + charset))); + + } + + @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(); + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonValueParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonValueParser.java new file mode 100644 index 000000000..685c81c64 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonValueParser.java @@ -0,0 +1,205 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.JsonpUtils; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * 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 final JsonProvider systemProvider = JsonpUtils.systemProvider(); + + public JsonObject parseObject(JsonParser parser) { + + JsonObjectBuilder ob = systemProvider.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 = systemProvider.createArrayBuilder(); + + while (parser.nextToken() != JsonToken.END_ARRAY) { + ab.add(parseValue(parser)); + } + return ab.build(); + } + + public JsonValue parseValue(JsonParser parser) { + JsonToken jsonToken = parser.currentToken(); + 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 systemProvider.createValue(parser.getText()); + + case VALUE_NUMBER_FLOAT: + case VALUE_NUMBER_INT: + switch (parser.getNumberType()) { + case INT: + return systemProvider.createValue(parser.getIntValue()); + case LONG: + return systemProvider.createValue(parser.getLongValue()); + case FLOAT: + case DOUBLE: + // Use double also for floats, as JSON-P has no support for float + return new DoubleNumber(parser.getDoubleValue()); + case BIG_DECIMAL: + return systemProvider.createValue(parser.getDecimalValue()); + case BIG_INTEGER: + return systemProvider.createValue(parser.getBigIntegerValue()); + } + + default: + throw new JsonParsingException("Unexpected token '" + parser.currentToken() + "'", + new Jackson3JsonpLocation(parser)); + + } + } + + private static class DoubleNumber implements JsonNumber { + + private final double value; + + DoubleNumber(double value) { + this.value = value; + } + + @Override + public boolean isIntegral() { + return false; + } + + @Override + public int intValue() { + return (int) value; + } + + @Override + public int intValueExact() { + int result = (int) value; + + if ((double) result == value) { + return result; + } else { + throw new ArithmeticException(); + } + } + + @Override + public long longValue() { + return (long) value; + } + + @Override + public long longValueExact() { + long result = (long) value; + + if ((double) result == value) { + return result; + } else { + throw new ArithmeticException(); + } + } + + @Override + public BigInteger bigIntegerValue() { + return bigDecimalValue().toBigInteger(); + } + + @Override + public BigInteger bigIntegerValueExact() { + return bigDecimalValue().toBigIntegerExact(); + } + + @Override + public double doubleValue() { + return value; + } + + @Override + public BigDecimal bigDecimalValue() { + return new BigDecimal(value); + } + + @Override + public ValueType getValueType() { + return ValueType.NUMBER; + } + + @Override + public Number numberValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public int hashCode() { + return Double.hashCode(value); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DoubleNumber && ((DoubleNumber) obj).value == value; + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java new file mode 100644 index 000000000..aef916e08 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java @@ -0,0 +1,306 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.BufferingJsonGenerator; +import co.elastic.clients.json.JsonData; +import jakarta.json.JsonNumber; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerationException; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import tools.jackson.core.TokenStreamContext; +import tools.jackson.databind.util.TokenBuffer; + +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 tools.jackson.core.JsonGenerator generator; + + public Jackson3JsonpGenerator(tools.jackson.core.JsonGenerator generator) { + this.generator = generator; + } + + public static class Buffering extends Jackson3JsonpGenerator implements BufferingJsonGenerator { + + private final Jackson3JsonpMapper mapper; + + public Buffering(Jackson3JsonpMapper mapper) { + super(new TokenBuffer(mapper.objectMapper()._serializationContext(), false)); + this.mapper = mapper; + } + + @Override + public JsonData getJsonData() { + this.close(); + return new Jackson3JsonBuffer((TokenBuffer) jacksonGenerator(), mapper); + } + + @Override + public JsonParser getParser() { + this.close(); + TokenBuffer tokenBuffer = (TokenBuffer) jacksonGenerator(); + return new Jackson3JsonpParser(tokenBuffer.asParser(), mapper); + } + + @Override + public void copyValue(JsonParser parser) { + if (!(parser instanceof Jackson3JsonpGenerator)) { + throw new IllegalArgumentException("Can only be used with a JacksonJsonpGenerator"); + } + + tools.jackson.core.JsonParser jkParser = ((Jackson3JsonpParser) parser).jacksonParser(); + jacksonGenerator().copyCurrentStructure(jkParser); + } + } + + /** + * Returns the underlying Jackson generator. + */ + public tools.jackson.core.JsonGenerator jacksonGenerator() { + return generator; + } + + @Override + public JsonGenerator writeStartObject() { + generator.writeStartObject(); + return this; + } + + @Override + public JsonGenerator writeStartObject(String name) { + generator.writeName(name); + generator.writeStartObject(); + return this; + } + + @Override + public JsonGenerator writeStartArray() { + generator.writeStartArray(); + return this; + } + + @Override + public JsonGenerator writeStartArray(String name) { + generator.writeName(name); + generator.writeStartArray(); + return this; + } + + @Override + public JsonGenerator writeKey(String name) { + generator.writeName(name); + return this; + } + + @Override + public JsonGenerator write(String name, JsonValue value) { + generator.writeName(name); + writeValue(value); + return this; + } + + @Override + public JsonGenerator write(String name, String value) { + generator.writeName(name); + generator.writeString(value); + return this; + } + + @Override + public JsonGenerator write(String name, BigInteger value) { + generator.writeName(name); + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(String name, BigDecimal value) { + generator.writeName(name); + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(String name, int value) { + generator.writeName(name); + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(String name, long value) { + generator.writeName(name); + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(String name, double value) { + generator.writeName(name); + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(String name, boolean value) { + generator.writeName(name); + generator.writeBoolean(value); + + return this; + } + + @Override + public JsonGenerator writeNull(String name) { + generator.writeName(name); + generator.writeNull(); + return this; + } + + @Override + public JsonGenerator writeEnd() { + TokenStreamContext ctx = generator.streamWriteContext(); + if (ctx.inObject()) { + generator.writeEndObject(); + } else if (ctx.inArray()) { + generator.writeEndArray(); + } else { + throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'"); + } + return this; + } + + @Override + public JsonGenerator write(JsonValue value) { + writeValue(value); + return this; + } + + @Override + public JsonGenerator write(String value) { + generator.writeString(value); + return this; + } + + @Override + public JsonGenerator write(BigDecimal value) { + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(BigInteger value) { + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(int value) { + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(long value) { + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(double value) { + generator.writeNumber(value); + return this; + } + + @Override + public JsonGenerator write(boolean value) { + generator.writeBoolean(value); + return this; + } + + @Override + public JsonGenerator writeNull() { + generator.writeNull(); + return this; + } + + @Override + public void close() { + generator.close(); + } + + @Override + public void flush() { + generator.flush(); + } + + private void writeValue(JsonValue value) { + 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; + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpLocation.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpLocation.java new file mode 100644 index 000000000..ada5be693 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpLocation.java @@ -0,0 +1,62 @@ +/* + * 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. + */ + +package co.elastic.clients.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.currentLocation()); + } + + @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; + } + + @Override + public String toString() { + return "(line no=" + location.getLineNr() + ", column no=" + location.getColumnNr() + ", offset=" + location.getCharOffset() + ")"; + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java new file mode 100644 index 000000000..1f17543cb --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java @@ -0,0 +1,152 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.BufferingJsonGenerator; +import co.elastic.clients.json.BufferingJsonpMapper; +import co.elastic.clients.json.DelegatingJsonGenerator; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpDeserializerBase; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpMapperBase; +import co.elastic.clients.json.JsonpSerializer; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import java.lang.reflect.Type; +import java.util.EnumSet; + +public class Jackson3JsonpMapper extends JsonpMapperBase implements BufferingJsonpMapper { + + private final Jackson3JsonProvider provider; + private final JsonMapper objectMapper; + + private Jackson3JsonpMapper(JsonMapper objectMapper, Jackson3JsonProvider provider) { + // No need to configure here, as this constructor is only called with the objectMapper + // of an existing JacksonJsonpMapper, and has therefore alredy been configured. + this.objectMapper = objectMapper; + this.provider = provider; + } + + public Jackson3JsonpMapper(JsonMapper objectMapper) { + this.objectMapper = configure(objectMapper); + // Order is important as JacksonJsonProvider(this) will get ObjectMapper + this.provider = new Jackson3JsonProvider(this); + } + + public Jackson3JsonpMapper() { + + this(JsonMapper.builder() + .enable(SerializationFeature.INDENT_OUTPUT) + .changeDefaultPropertyInclusion(p -> p + .withValueInclusion(JsonInclude.Include.NON_NULL)) + .build() + ); + } + + private static JsonMapper configure(JsonMapper builder) { + // Accept single objects as collections. This is useful in the context of Elasticsearch since + // Lucene has no concept of multivalued field and fields with a single value will be returned + // as a single object even if other instances of the same field have multiple values. + return builder.rebuild().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY).build(); + } + + @Override + public JsonpMapper withAttribute(String name, T value) { + return new Jackson3JsonpMapper(this.objectMapper, this.provider).addAttribute(name, value); + } + + /** + * Returns the underlying Jackson mapper. + */ + public JsonMapper objectMapper() { + return this.objectMapper; + } + + @Override + public JsonProvider jsonProvider() { + return provider; + } + + @Override + protected JsonpDeserializer getDefaultDeserializer(Type type) { + return new JacksonValueParser<>(type); + } + + @Override + public void serialize(T value, JsonGenerator generator) { + + JsonpSerializer serializer = findSerializer(value); + if (serializer != null) { + serializer.serialize(value, generator, this); + return; + } + + // Delegating generators are used in higher levels of serialization (e.g. filter empty top-level + // objects). + // At this point the object is not a JsonpSerializable and we can assume we're in a nested property + // holding + // a user-provided type and can unwrap to find the underlying non-delegating generator. + while (generator instanceof DelegatingJsonGenerator) { + generator = ((DelegatingJsonGenerator) generator).unwrap(); + } + + if (!(generator instanceof Jackson3JsonpGenerator)) { + throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the " + + "JacksonJsonpProvider"); + } + + tools.jackson.core.JsonGenerator jkGenerator = ((Jackson3JsonpGenerator) generator).jacksonGenerator(); + objectMapper.writeValue(jkGenerator, value); + } + + @Override + public BufferingJsonGenerator createBufferingGenerator() { + return new Jackson3JsonpGenerator.Buffering(this); + } + + private class JacksonValueParser extends JsonpDeserializerBase { + + private final Type type; + + protected JacksonValueParser(Type type) { + super(EnumSet.allOf(JsonParser.Event.class)); + this.type = type; + } + + @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(); + + return objectMapper.readValue(jkParser, objectMapper().constructType(type)); + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java new file mode 100644 index 000000000..91876b477 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java @@ -0,0 +1,398 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.BufferingJsonParser; +import co.elastic.clients.json.JsonData; +import co.elastic.clients.json.LookAheadJsonParser; +import co.elastic.clients.json.UnexpectedJsonEventException; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonLocation; +import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.core.util.JsonParserSequence; +import tools.jackson.databind.util.TokenBuffer; + +import java.io.IOException; +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, BufferingJsonParser { + + private final JsonParser parser; + private final Jackson3JsonpMapper mapper; + + 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(JsonParser parser, Jackson3JsonpMapper mapper) { + this.parser = parser; + this.mapper = mapper; + } + + /** + * Returns the underlying Jackson parser. + */ + public JsonParser jacksonParser() { + return this.parser; + } + + 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 parser.nextToken() != null; + } + } + + @Override + public Event next() { + JsonToken token; + if (hasNextWasCalled) { + token = parser.currentToken(); + hasNextWasCalled = false; + } else token = parser.nextToken(); + + 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(); + return parser.getValueAsString(); + } + + @Override + public boolean isIntegralNumber() { + ensureTokenIsCurrent(); + return parser.isExpectedNumberIntToken(); + } + + @Override + public int getInt() { + ensureTokenIsCurrent(); + return parser.getIntValue(); + } + + @Override + public long getLong() { + ensureTokenIsCurrent(); + return parser.getLongValue(); + } + + @Override + public BigDecimal getBigDecimal() { + ensureTokenIsCurrent(); + return parser.getDecimalValue(); + } + + @Override + public JsonLocation getLocation() { + return new Jackson3JsonpLocation(parser.currentLocation()); + } + + @Override + public void close() { + parser.close(); + } + + 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(); + } + return valueParser.parseObject(parser); + } + + @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()); + } + return valueParser.parseArray(parser); + } + + @Override + public JsonValue getValue() { + ensureTokenIsCurrent(); + if (valueParser == null) { + valueParser = new Jackson3JsonValueParser(); + } + return valueParser.parseValue(parser); + } + + @Override + public void skipObject() { + ensureTokenIsCurrent(); + if (parser.currentToken() != JsonToken.START_OBJECT) { + return; + } + + 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)); + } + + @Override + public void skipArray() { + ensureTokenIsCurrent(); + if (parser.currentToken() != JsonToken.START_ARRAY) { + return; + } + + 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)); + } + + @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(this.parser, this.parser.objectReadContext()); + + // 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); + + String result = null; + switch (parser.nextToken()) { + case VALUE_STRING: + result = parser.getText(); + break; + // Handle booleans promoted to enums + case VALUE_TRUE: + result = "true"; + break; + case VALUE_FALSE: + result = "false"; + break; + default: + expectEvent(JsonToken.VALUE_STRING); + } + + tb.copyCurrentEvent(parser); + + return new AbstractMap.SimpleImmutableEntry<>( + result, + new Jackson3JsonpParser( + JsonParserSequence.createFlattened(false, tb.asParser(), parser), + mapper + ) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + + // Field not found + return new AbstractMap.SimpleImmutableEntry<>( + defaultValue, + new Jackson3JsonpParser( + JsonParserSequence.createFlattened(false, tb.asParser(), parser), + mapper + ) + ); + } + + @Override + public Map.Entry findVariant(Map variants) { + // We're on a START_OBJECT event + TokenBuffer tb = TokenBuffer.forBuffering(parser, parser.objectReadContext()); + + if (parser.currentToken() != JsonToken.START_OBJECT) { + // Primitive value or array + tb.copyCurrentStructure(parser); + } else { + // 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), + mapper + ) + ); + } else { + tb.copyCurrentStructure(parser); + } + } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } + + // 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), + mapper + ) + ); + } + + 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)); + } + } + + //----- Buffering methods + + @Override + public JsonData getJsonData() { + TokenBuffer buffer = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext()); + buffer.copyCurrentStructure(parser); + return new Jackson3JsonBuffer(buffer, mapper); + } +} + From fc6c28d2e32ba418fc76ca09dbccf2097094ae7c Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Wed, 1 Oct 2025 18:07:09 +0200 Subject: [PATCH 02/11] updated unit tests --- .../json/jackson/Jackson3JsonProvider.java | 7 +- .../json/jackson/Jackson3JsonpGenerator.java | 2 +- .../json/jackson/Jackson3JsonpMapper.java | 5 +- .../json/jackson/Jackson3JsonBufferTest.java | 70 +++++++ .../jackson/Jackson3JsonValueParserTest.java | 77 +++++++ .../jackson/Jackson3JsonpGeneratorTest.java | 90 ++++++++ .../json/jackson/Jackson3JsonpParserTest.java | 193 ++++++++++++++++++ .../json/jackson/Jackson3MapperTest.java | 102 +++++++++ .../json/jackson/JsonValueParserTest.java | 12 +- .../clients/testkit/ModelTestCase.java | 7 +- 10 files changed, 552 insertions(+), 13 deletions(-) create mode 100644 java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonBufferTest.java create mode 100644 java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonValueParserTest.java create mode 100644 java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpGeneratorTest.java create mode 100644 java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpParserTest.java create mode 100644 java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3MapperTest.java diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java index b8c7f28cf..2d3c8bf4b 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java @@ -34,6 +34,7 @@ import jakarta.json.stream.JsonGeneratorFactory; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParserFactory; +import tools.jackson.core.ObjectReadContext; import tools.jackson.core.json.JsonFactory; import java.io.InputStream; @@ -102,17 +103,17 @@ private class ParserFactory implements JsonParserFactory { @Override public JsonParser createParser(Reader reader) { - return new Jackson3JsonpParser(jsonFactory.createParser(reader), mapper); + return new Jackson3JsonpParser(mapper.objectMapper().createParser(reader), mapper); } @Override public JsonParser createParser(InputStream in) { - return new Jackson3JsonpParser(jsonFactory.createParser(in), mapper); + return new Jackson3JsonpParser(mapper.objectMapper().createParser(in), mapper); } @Override public JsonParser createParser(InputStream in, Charset charset) { - return new Jackson3JsonpParser(jsonFactory.createParser(new InputStreamReader(in, charset)), + return new Jackson3JsonpParser(mapper.objectMapper().createParser(new InputStreamReader(in, charset)), mapper); } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java index aef916e08..e813ec7e3 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java @@ -50,7 +50,7 @@ public static class Buffering extends Jackson3JsonpGenerator implements Bufferin private final Jackson3JsonpMapper mapper; public Buffering(Jackson3JsonpMapper mapper) { - super(new TokenBuffer(mapper.objectMapper()._serializationContext(), false)); + super(TokenBuffer.forGeneration()); // TODO probably not this.mapper = mapper; } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java index 1f17543cb..0f1160ac5 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java @@ -70,7 +70,10 @@ private static JsonMapper configure(JsonMapper builder) { // Accept single objects as collections. This is useful in the context of Elasticsearch since // Lucene has no concept of multivalued field and fields with a single value will be returned // as a single object even if other instances of the same field have multiple values. - return builder.rebuild().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY).build(); + return builder.rebuild() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .build(); } @Override diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonBufferTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonBufferTest.java new file mode 100644 index 000000000..179184f7c --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonBufferTest.java @@ -0,0 +1,70 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.util.TokenBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class Jackson3JsonBufferTest { + + private Jackson3JsonpMapper mapper() { + return new Jackson3JsonpMapper(new JsonMapper()); + } + + @Test + public void testToString_rendersJson() { + TokenBuffer buffer = TokenBuffer.forGeneration(); + buffer.writeStartObject(); + buffer.writeStringProperty("foo", "bar"); + buffer.writeNumberProperty("baz", 42); + buffer.writeEndObject(); + + Jackson3JsonBuffer jsonBuffer = new Jackson3JsonBuffer(buffer, mapper()); + String json = jsonBuffer.toString(); + assertTrue(json.contains("\"foo\":\"bar\"")); + assertTrue(json.contains("\"baz\":42")); + assertTrue(json.startsWith("{") && json.endsWith("}")); + } + + @Test + public void testToString_emptyBuffer() { + TokenBuffer buffer = TokenBuffer.forGeneration(); + Jackson3JsonBuffer jsonBuffer = new Jackson3JsonBuffer(buffer, mapper()); + String json = jsonBuffer.toString(); + assertEquals(StringUtils.EMPTY, json.trim()); + } + + @Test + public void testToString_invalidBuffer() { + TokenBuffer buffer = TokenBuffer.forGeneration(); + + // Write some invalid JSON (start object but don't close) + buffer.writeStartObject(); + Jackson3JsonBuffer jsonBuffer = new Jackson3JsonBuffer(buffer, mapper()); + String json = jsonBuffer.toString(); + // Should not throw, but may contain error or partial output + assertTrue(json.startsWith("{")); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonValueParserTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonValueParserTest.java new file mode 100644 index 000000000..4e9c67799 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonValueParserTest.java @@ -0,0 +1,77 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpUtils; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; +import java.util.Map; + +public class Jackson3JsonValueParserTest extends Assertions { + + public static class Data { + public Map data; + } + + @Test + public void testFloatsShouldDeserializeAsFloats() throws Exception { + // When using Jackson to target a map of objects, values with a decimal separator + // should deserialize as a double even if they fit in an int or long. + // See https://github.com/elastic/elasticsearch-java/issues/156 + + String json = "{\"data\": {\"value\": 1.4778125E7, \"value2\": 1.4778125E7 }}"; + JsonpMapper mapper = new Jackson3JsonpMapper(); + + { + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + Data data = mapper.deserialize(parser, Data.class); + + Double d = (Double) data.data.get("value"); + assertEquals(1.4778125E7, d, 0.001); + } + + { + // Test with buffering used in union resolution + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + parser.next(); + JsonObject object = parser.getObject(); + + // Test equals/hashcode + JsonValue v = object.getJsonObject("data").get("value"); + JsonValue v2 = object.getJsonObject("data").get("value2"); + + assertEquals(v.hashCode(), v2.hashCode()); + assertEquals(v, v2); + + parser = JsonpUtils.jsonValueParser(object, mapper); + Data data = mapper.deserialize(parser, Data.class); + + Double d = (Double) data.data.get("value"); + assertEquals(1.4778125E7, d, 0.001); + } + + } +} diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpGeneratorTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpGeneratorTest.java new file mode 100644 index 000000000..af0e34928 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpGeneratorTest.java @@ -0,0 +1,90 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import jakarta.json.Json; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonGenerator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +public class Jackson3JsonpGeneratorTest extends Assertions { + + @Test + public void testWrite(){ + StringWriter sw = new StringWriter(); + JsonGenerator generator = new Jackson3JsonpMapper().jsonProvider().createGenerator(sw); + + generator.writeStartObject(); + + // Boolean + generator.write("bool1", true); + generator.writeKey("bool2"); + generator.write(false); + + // String + generator.write("str1", "foo"); + generator.writeKey("str2"); + generator.write("bar"); + + // Integer + generator.write("int1", 42); + generator.writeKey("int2"); + generator.write(1337); + + // Long + generator.write("long1", 123456789012345L); + generator.writeKey("long2"); + generator.write(123456789012345L); + + generator.write("double1", 0.001); + generator.writeKey("double2"); + generator.write(12345.6789); + + // JsonValue + JsonValue jsonValue = Json.createObjectBuilder() + .add("bool", true) + .add("str", "foo") + .add("int", 42) + .add("long", 123456789012345L) + .add("double", 12345.6789) + .build(); + + generator.write("value", jsonValue); + + generator.close(); + + assertEquals("{" + + "\"bool1\":true," + + "\"bool2\":false," + + "\"str1\":\"foo\"," + + "\"str2\":\"bar\"," + + "\"int1\":42," + + "\"int2\":1337," + + "\"long1\":123456789012345," + + "\"long2\":123456789012345," + + "\"double1\":0.001," + + "\"double2\":12345.6789," + + "\"value\":{\"bool\":true,\"str\":\"foo\",\"int\":42,\"long\":123456789012345,\"double\":12345.6789}" + + "}", sw.toString()); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpParserTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpParserTest.java new file mode 100644 index 000000000..93f6edc16 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3JsonpParserTest.java @@ -0,0 +1,193 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.elasticsearch.core.MsearchResponse; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.testkit.ModelTestCase; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParser.Event; +import org.junit.jupiter.api.Test; + +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 + 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(); + mapper = mapper.withAttribute("co.elastic.clients:Deserializer:_global.msearch.Response.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 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/co/elastic/clients/json/jackson/Jackson3MapperTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3MapperTest.java new file mode 100644 index 000000000..b7642e78f --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/Jackson3MapperTest.java @@ -0,0 +1,102 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import co.elastic.clients.elasticsearch.core.search.Hit; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.testkit.ModelTestCase; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonNaming; + +import java.util.Collections; +import java.util.List; + +public class Jackson3MapperTest extends ModelTestCase { + + @Test + public void testCustomDeserializer() { + // See https://github.com/elastic/elasticsearch-java/issues/120 + JsonpMapper jsonpMapper = new Jackson3JsonpMapper(); + + String json = "{\"_index\":\"foo\",\"_id\":\"1\",\"_source\":{\"model\":\"Foo\",\"age\":42}}"; + + Hit testDataHit = fromJson(json, + Hit.createHitDeserializer(JsonpDeserializer.of(TestData.class)), + jsonpMapper + ); + TestData data = testDataHit.source(); + assertEquals("Foo", data.theModel); + assertEquals(42, data.theAge); + } + + @JsonDeserialize(using = TestData.TestDeserializer.class) + public static class TestData { + public String theModel; + public int theAge; + + public static class TestDeserializer extends ValueDeserializer { + + @Override + public TestData deserialize(JsonParser jp, DeserializationContext ctx) { + JsonNode node = jp.readValueAsTree(); + + TestData res = new TestData(); + if (node.has("age")) { + res.theAge = node.get("age").asInt(); + } + if (node.has("model")) { + res.theModel = node.get("model").asText(); + } + return res; + } + } + } + + @Test + public void testSingleValueAsList() { + JsonpMapper jsonpMapper = new Jackson3JsonpMapper(); + + String json = "{\"_index\":\"foo\",\"_id\":\"1\",\"_source\":{\"emp_no\":42," + + "\"job_positions\":\"SWE\"}}"; + + Hit testDataHit = fromJson(json, + Hit.createHitDeserializer(JsonpDeserializer.of(EmpData.class)), + jsonpMapper + ); + EmpData data = testDataHit.source(); + assertEquals(42, data.empNo); + assertEquals(Collections.singletonList("SWE"), data.jobPositions); + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class EmpData { + public int empNo; + public List jobPositions; + } +} diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/JsonValueParserTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/JsonValueParserTest.java index 0f49a6191..624631941 100644 --- a/java-client/src/test/java/co/elastic/clients/json/jackson/JsonValueParserTest.java +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/JsonValueParserTest.java @@ -21,17 +21,15 @@ import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.JsonpUtils; -import co.elastic.clients.json.jackson.JacksonJsonpMapper; import jakarta.json.JsonObject; import jakarta.json.JsonValue; import jakarta.json.stream.JsonParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.io.StringReader; import java.util.Map; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - public class JsonValueParserTest extends Assertions { public static class Data { @@ -51,7 +49,7 @@ public void testFloatsShouldDeserializeAsFloats() throws Exception { JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); Data data = mapper.deserialize(parser, Data.class); - Double d = (Double)data.data.get("value"); + Double d = (Double) data.data.get("value"); assertEquals(1.4778125E7, d, 0.001); } @@ -67,11 +65,11 @@ public void testFloatsShouldDeserializeAsFloats() throws Exception { assertEquals(v.hashCode(), v2.hashCode()); assertEquals(v, v2); - + parser = JsonpUtils.jsonValueParser(object, mapper); Data data = mapper.deserialize(parser, Data.class); - Double d = (Double)data.data.get("value"); + Double d = (Double) data.data.get("value"); assertEquals(1.4778125E7, d, 0.001); } diff --git a/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java b/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java index f935c9d82..c6a83f36b 100644 --- a/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java +++ b/java-client/src/test/java/co/elastic/clients/testkit/ModelTestCase.java @@ -22,6 +22,7 @@ import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.SimpleJsonpMapper; +import co.elastic.clients.json.jackson.Jackson3JsonpMapper; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import jakarta.json.spi.JsonProvider; @@ -41,7 +42,7 @@ */ public abstract class ModelTestCase extends Assertions { - protected enum JsonImpl { Jsonb, Jackson, Simple }; + protected enum JsonImpl { Jsonb, Jackson, Jackson3, Simple }; // Same value for all tests in a test run private static final int RAND = new Random().nextInt(100); @@ -64,6 +65,10 @@ private static JsonpMapper createMapper(JsonImpl jsonImpl, int rand) { System.out.println("Using a Jackson mapper (rand = " + rand + ")."); return new JacksonJsonpMapper(); + case Jackson3: + System.out.println("Using a Jackson 3 mapper (rand = " + rand + ")."); + return new Jackson3JsonpMapper(); + default: System.out.println("Using a simple mapper (rand = " + rand + ")."); return SimpleJsonpMapper.INSTANCE; From b74e81ae4781c3a01e3c092f92ec298fc22eef8f Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Wed, 1 Oct 2025 18:24:34 +0200 Subject: [PATCH 03/11] fix test --- .../src/test/java/co/elastic/clients/json/JsonDataTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java-client/src/test/java/co/elastic/clients/json/JsonDataTest.java b/java-client/src/test/java/co/elastic/clients/json/JsonDataTest.java index d752a8558..cb6c5981b 100644 --- a/java-client/src/test/java/co/elastic/clients/json/JsonDataTest.java +++ b/java-client/src/test/java/co/elastic/clients/json/JsonDataTest.java @@ -160,6 +160,9 @@ private void checkClass(JsonData data) { case Jackson: assertEquals(name, "co.elastic.clients.json.jackson.JacksonJsonBuffer"); break; + case Jackson3: + assertEquals(name, "co.elastic.clients.json.jackson.Jackson3JsonBuffer"); + break; case Jsonb: case Simple: From 489d47e5f6d3ca2404a0ab9335fd03cd85a73f73 Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Wed, 1 Oct 2025 18:28:34 +0200 Subject: [PATCH 04/11] removed comment --- .../co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java index e813ec7e3..e2bec286f 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java @@ -50,7 +50,7 @@ public static class Buffering extends Jackson3JsonpGenerator implements Bufferin private final Jackson3JsonpMapper mapper; public Buffering(Jackson3JsonpMapper mapper) { - super(TokenBuffer.forGeneration()); // TODO probably not + super(TokenBuffer.forGeneration()); this.mapper = mapper; } From 7d0d2b468980a23c27d26413d9cb388aaa3cdc21 Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Wed, 1 Oct 2025 18:35:10 +0200 Subject: [PATCH 05/11] deprecate jackson 2 classes --- .../co/elastic/clients/json/jackson/JacksonJsonBuffer.java | 4 ++++ .../co/elastic/clients/json/jackson/JacksonJsonProvider.java | 2 ++ .../elastic/clients/json/jackson/JacksonJsonpGenerator.java | 2 ++ .../co/elastic/clients/json/jackson/JacksonJsonpLocation.java | 2 ++ .../co/elastic/clients/json/jackson/JacksonJsonpMapper.java | 4 ++++ .../co/elastic/clients/json/jackson/JacksonJsonpParser.java | 2 ++ .../java/co/elastic/clients/json/jackson/JacksonUtils.java | 1 + .../java/co/elastic/clients/json/jackson/JsonValueParser.java | 2 ++ 8 files changed, 19 insertions(+) diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonBuffer.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonBuffer.java index 333040c5e..9c4143365 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonBuffer.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonBuffer.java @@ -33,6 +33,10 @@ import java.io.StringWriter; import java.lang.reflect.Type; +/** + * @deprecated Use {@link Jackson3JsonBuffer} + */ +@Deprecated class JacksonJsonBuffer implements JsonBuffer, JsonData { private final TokenBuffer buffer; private final JacksonJsonpMapper mapper; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java index d42159b14..485bd1ec7 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonProvider.java @@ -49,7 +49,9 @@ /** * A partial implementation of JSONP's SPI on top of Jackson. + * @deprecated Use {@link Jackson3JsonProvider} */ +@Deprecated public class JacksonJsonProvider extends JsonProvider { private final JacksonJsonpMapper mapper; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java index 664d3ae9a..743862016 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java @@ -37,7 +37,9 @@ /** * A JSONP generator implementation on top of Jackson. + * @deprecated Use {@link Jackson3JsonpGenerator} */ +@Deprecated public class JacksonJsonpGenerator implements JsonGenerator { private final com.fasterxml.jackson.core.JsonGenerator generator; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpLocation.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpLocation.java index b9d69fb25..842f46090 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpLocation.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpLocation.java @@ -23,7 +23,9 @@ /** * Translate a Jackson location to a JSONP location. + * @deprecated Use {@link Jackson3JsonpLocation} */ +@Deprecated public class JacksonJsonpLocation implements JsonLocation { private final com.fasterxml.jackson.core.JsonLocation location; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java index 441b5990c..7ad4c04dd 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java @@ -39,6 +39,10 @@ import java.lang.reflect.Type; import java.util.EnumSet; +/** + * @deprecated Use {@link Jackson3JsonpMapper} + */ +@Deprecated public class JacksonJsonpMapper extends JsonpMapperBase implements BufferingJsonpMapper { private final JacksonJsonProvider provider; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpParser.java index b1f526725..491d46249 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpParser.java @@ -48,7 +48,9 @@ * 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}. + * @deprecated Use {@link Jackson3JsonpParser} */ +@Deprecated public class JacksonJsonpParser implements LookAheadJsonParser, BufferingJsonParser { private final com.fasterxml.jackson.core.JsonParser parser; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonUtils.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonUtils.java index 70157cd87..0b0e6d898 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonUtils.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonUtils.java @@ -26,6 +26,7 @@ import java.io.IOException; +@Deprecated class JacksonUtils { public static JsonException convertException(IOException ioe) { if (ioe instanceof com.fasterxml.jackson.core.JsonGenerationException) { diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java index 6f9572388..da257c9f0 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JsonValueParser.java @@ -38,7 +38,9 @@ /** * 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). + * @deprecated Use {@link Jackson3JsonValueParser} */ +@Deprecated class JsonValueParser { private final JsonProvider systemProvider = JsonpUtils.systemProvider(); From bf813eaa92e1bd72e74ca94a3218e9efb310ed6e Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Mon, 6 Oct 2025 15:34:26 +0200 Subject: [PATCH 06/11] Catch parsing exceptions to convert them to JsonP's JsonParsingException --- .../json/jackson/Jackson3JsonpParser.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java index 91876b477..f96525cc7 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java @@ -28,8 +28,10 @@ import jakarta.json.JsonValue; import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; +import tools.jackson.core.exc.StreamReadException; import tools.jackson.core.util.JsonParserSequence; import tools.jackson.databind.util.TokenBuffer; @@ -92,6 +94,18 @@ public JsonParser jacksonParser() { return this.parser; } + private JsonParsingException convertException(StreamReadException ex) { + return new JsonParsingException("Jackson exception: " + ex.getMessage(), ex, getLocation()); + } + + private JsonToken fetchNextToken() { + try { + return parser.nextToken(); + } catch(StreamReadException e) { + throw convertException(e); + } + } + private void ensureTokenIsCurrent() { if (hasNextWasCalled) { throw new IllegalStateException("Cannot get event data as parser as already been moved to the " + @@ -105,7 +119,7 @@ public boolean hasNext() { return parser.currentToken() != null; } else { hasNextWasCalled = true; - return parser.nextToken() != null; + return fetchNextToken() != null; } } @@ -115,7 +129,7 @@ public Event next() { if (hasNextWasCalled) { token = parser.currentToken(); hasNextWasCalled = false; - } else token = parser.nextToken(); + } else token = fetchNextToken(); if (token == null) { throw new NoSuchElementException(); From 9f58dc88f459d6d3f09479d244b64abf65f8f76a Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Mon, 6 Oct 2025 15:36:23 +0200 Subject: [PATCH 07/11] Jackson 3 does work :-) --- .../elastic/clients/elasticsearch/model/SerializationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java index cb3b5d950..a18b76553 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java @@ -26,6 +26,7 @@ import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapperBase; import co.elastic.clients.json.JsonpUtils; +import co.elastic.clients.json.jackson.Jackson3JsonpMapper; import co.elastic.clients.testkit.ModelTestCase; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; From 5fbb7a1049cd3e1b82fe4fc7ad632e427af72791 Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Mon, 20 Oct 2025 18:07:31 +0200 Subject: [PATCH 08/11] cleanup, using stable dependencies --- java-client/build.gradle.kts | 5 ++--- .../elastic/clients/json/jackson/Jackson3JsonProvider.java | 1 - .../co/elastic/clients/json/jackson/Jackson3JsonpParser.java | 1 - .../clients/elasticsearch/model/SerializationTest.java | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index b978d3d4a..132cf006a 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -226,9 +226,8 @@ dependencies { // Apache 2.0 // https://github.com/FasterXML/jackson - implementation("tools.jackson.core", "jackson-databind", "3.0.0-rc10") - implementation("tools.jackson.core", "jackson-core", "3.0.0-rc10") - implementation("com.fasterxml.jackson.core","jackson-annotations","3.0-rc5") + implementation("tools.jackson.core", "jackson-databind", "3.0.0") + implementation("tools.jackson.core", "jackson-core", "3.0.0") // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java index 2d3c8bf4b..4301846c9 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java @@ -34,7 +34,6 @@ import jakarta.json.stream.JsonGeneratorFactory; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParserFactory; -import tools.jackson.core.ObjectReadContext; import tools.jackson.core.json.JsonFactory; import java.io.InputStream; diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java index f96525cc7..e2b118bc1 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java @@ -28,7 +28,6 @@ import jakarta.json.JsonValue; import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParsingException; -import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; import tools.jackson.core.exc.StreamReadException; diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java index a18b76553..cb3b5d950 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java @@ -26,7 +26,6 @@ import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapperBase; import co.elastic.clients.json.JsonpUtils; -import co.elastic.clients.json.jackson.Jackson3JsonpMapper; import co.elastic.clients.testkit.ModelTestCase; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; From defadc9c9cd1481aa188ed7e2d5f8e41d18a09ea Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Mon, 20 Oct 2025 18:17:38 +0200 Subject: [PATCH 09/11] refactor gradle --- java-client/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index 132cf006a..2ae8596de 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -177,6 +177,7 @@ signing { dependencies { val elasticsearchVersion = "9.0.0" val jacksonVersion = "2.18.3" + val jackson3Version = "3.0.0" val openTelemetryVersion = "1.32.0" api(project(":rest5-client")) @@ -226,8 +227,8 @@ dependencies { // Apache 2.0 // https://github.com/FasterXML/jackson - implementation("tools.jackson.core", "jackson-databind", "3.0.0") - implementation("tools.jackson.core", "jackson-core", "3.0.0") + implementation("tools.jackson.core", "jackson-databind", jackson3Version) + implementation("tools.jackson.core", "jackson-core", jackson3Version) // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ From 5bc5cb96839130e7eeb016758aebaf1fe2b876db Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Mon, 20 Oct 2025 18:26:02 +0200 Subject: [PATCH 10/11] add new jackson bom url --- java-client/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index 2ae8596de..47f612e33 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -312,6 +312,7 @@ class SpdxReporter(val dest: File) : ReportRenderer { "org.apache.httpcomponents.client5" -> "https://hc.apache.org/" "org.apache.httpcomponents.core5" -> "https://hc.apache.org/" "com.fasterxml.jackson" -> "https://github.com/FasterXML/jackson" + "tools.jackson" -> " https://github.com/FasterXML/jackson-bom " else -> if (info.moduleUrls.isEmpty()) { throw RuntimeException("No URL found for module '$depName'") } else { From 90b6a98274d0dcdf63e5c64d31ba1780a3cfa739 Mon Sep 17 00:00:00 2001 From: Laura Trotta Date: Tue, 21 Oct 2025 12:47:23 +0200 Subject: [PATCH 11/11] restore exception wrap behavior --- .../json/jackson/Jackson3JsonBuffer.java | 7 +- .../json/jackson/Jackson3JsonProvider.java | 43 +++- .../json/jackson/Jackson3JsonpGenerator.java | 198 ++++++++++++++---- .../json/jackson/Jackson3JsonpMapper.java | 16 +- .../json/jackson/Jackson3JsonpParser.java | 146 +++++++------ .../clients/json/jackson/Jackson3Utils.java | 29 +++ 6 files changed, 315 insertions(+), 124 deletions(-) create mode 100644 java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3Utils.java diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java index 5a76bd914..42f6de501 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java @@ -28,6 +28,7 @@ import jakarta.json.JsonValue; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; +import tools.jackson.core.JacksonException; import tools.jackson.databind.util.TokenBuffer; import java.io.StringWriter; @@ -89,7 +90,11 @@ public T deserialize(JsonpDeserializer deserializer, JsonpMapper mapper) public void serialize(JsonGenerator generator, JsonpMapper mapper) { if (generator instanceof Jackson3JsonpGenerator) { Jackson3JsonpGenerator jkGenerator = (Jackson3JsonpGenerator) generator; - buffer.serialize(jkGenerator.jacksonGenerator()); + try { + buffer.serialize(jkGenerator.jacksonGenerator()); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } else { try (JsonParser parser = asParser()) { JsonpUtils.copy(parser, generator); diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java index 4301846c9..55561da00 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java @@ -34,6 +34,7 @@ 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.InputStream; @@ -102,18 +103,31 @@ private class ParserFactory implements JsonParserFactory { @Override public JsonParser createParser(Reader reader) { - return new Jackson3JsonpParser(mapper.objectMapper().createParser(reader), mapper); + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(reader), mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override public JsonParser createParser(InputStream in) { - return new Jackson3JsonpParser(mapper.objectMapper().createParser(in), mapper); + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(in), mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override public JsonParser createParser(InputStream in, Charset charset) { - return new Jackson3JsonpParser(mapper.objectMapper().createParser(new InputStreamReader(in, charset)), - mapper); + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(new InputStreamReader(in, + charset)), + mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } /** @@ -176,19 +190,30 @@ private class GeneratorFactory implements JsonGeneratorFactory { @Override public JsonGenerator createGenerator(Writer writer) { - return new Jackson3JsonpGenerator(jsonFactory.createGenerator(writer)); + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(writer)); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override public JsonGenerator createGenerator(OutputStream out) { - return new Jackson3JsonpGenerator(jsonFactory.createGenerator(out)); + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(out)); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override public JsonGenerator createGenerator(OutputStream out, Charset charset) { - return new Jackson3JsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, - charset))); - + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, + charset))); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java index e2bec286f..e23dcccfc 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java @@ -27,6 +27,7 @@ import jakarta.json.stream.JsonGenerationException; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; +import tools.jackson.core.JacksonException; import tools.jackson.core.TokenStreamContext; import tools.jackson.databind.util.TokenBuffer; @@ -74,7 +75,11 @@ public void copyValue(JsonParser parser) { } tools.jackson.core.JsonParser jkParser = ((Jackson3JsonpParser) parser).jacksonParser(); - jacksonGenerator().copyCurrentStructure(jkParser); + try { + jacksonGenerator().copyCurrentStructure(jkParser); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } } @@ -87,175 +92,278 @@ public tools.jackson.core.JsonGenerator jacksonGenerator() { @Override public JsonGenerator writeStartObject() { - generator.writeStartObject(); + try { + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeStartObject(String name) { - generator.writeName(name); - generator.writeStartObject(); + try { + generator.writeName(name); + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeStartArray() { - generator.writeStartArray(); + try { + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeStartArray(String name) { - generator.writeName(name); - generator.writeStartArray(); + try { + generator.writeName(name); + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeKey(String name) { - generator.writeName(name); + try { + generator.writeName(name); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(String name, JsonValue value) { - generator.writeName(name); - writeValue(value); + try { + generator.writeName(name); + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(String name, String value) { - generator.writeName(name); - generator.writeString(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) { - generator.writeName(name); - generator.writeNumber(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) { - generator.writeName(name); - generator.writeNumber(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) { - generator.writeName(name); - generator.writeNumber(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) { - generator.writeName(name); - generator.writeNumber(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) { - generator.writeName(name); - generator.writeNumber(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) { - generator.writeName(name); - generator.writeBoolean(value); - + try { + generator.writeName(name); + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeNull(String name) { - generator.writeName(name); - generator.writeNull(); + try { + generator.writeName(name); + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeEnd() { - TokenStreamContext ctx = generator.streamWriteContext(); - if (ctx.inObject()) { - generator.writeEndObject(); - } else if (ctx.inArray()) { - generator.writeEndArray(); - } else { - throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'"); + 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) { - writeValue(value); + try { + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(String value) { - generator.writeString(value); + try { + generator.writeString(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(BigDecimal value) { - generator.writeNumber(value); + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(BigInteger value) { - generator.writeNumber(value); + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(int value) { - generator.writeNumber(value); + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(long value) { - generator.writeNumber(value); + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(double value) { - generator.writeNumber(value); + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator write(boolean value) { - generator.writeBoolean(value); + try { + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public JsonGenerator writeNull() { - generator.writeNull(); + try { + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } return this; } @Override public void close() { - generator.close(); + try { + generator.close(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override public void flush() { - generator.flush(); + try { + generator.flush(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } private void writeValue(JsonValue value) { diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java index 0f1160ac5..03b0df5bf 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java @@ -31,6 +31,7 @@ import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; +import tools.jackson.core.JacksonException; import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.SerializationFeature; import tools.jackson.databind.json.JsonMapper; @@ -121,8 +122,13 @@ public void serialize(T value, JsonGenerator generator) { "JacksonJsonpProvider"); } - tools.jackson.core.JsonGenerator jkGenerator = ((Jackson3JsonpGenerator) generator).jacksonGenerator(); - objectMapper.writeValue(jkGenerator, value); + tools.jackson.core.JsonGenerator jkGenerator = + ((Jackson3JsonpGenerator) generator).jacksonGenerator(); + try { + objectMapper.writeValue(jkGenerator, value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } @Override @@ -149,7 +155,11 @@ public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event eve tools.jackson.core.JsonParser jkParser = ((Jackson3JsonpParser) parser).jacksonParser(); - return objectMapper.readValue(jkParser, objectMapper().constructType(type)); + try { + return objectMapper.readValue(jkParser, objectMapper().constructType(type)); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } } } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java index e2b118bc1..3c448acfa 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java @@ -28,6 +28,7 @@ import jakarta.json.JsonValue; import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParsingException; +import tools.jackson.core.JacksonException; import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; import tools.jackson.core.exc.StreamReadException; @@ -100,7 +101,7 @@ private JsonParsingException convertException(StreamReadException ex) { private JsonToken fetchNextToken() { try { return parser.nextToken(); - } catch(StreamReadException e) { + } catch (StreamReadException e) { throw convertException(e); } } @@ -283,53 +284,58 @@ public Stream getValueStream() { //----- Look ahead methods - public Map.Entry lookAheadFieldValue(String name, String defaultValue) { + public Map.Entry lookAheadFieldValue(String name, + String defaultValue) { TokenBuffer tb = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext()); - // The resulting parser must contain the full object, including START_EVENT - tb.copyCurrentEvent(parser); - while (parser.nextToken() != JsonToken.END_OBJECT) { + 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 + 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); + String fieldName = parser.currentName(); + if (fieldName.equals(name)) { + // Found + tb.copyCurrentEvent(parser); - String result = null; - switch (parser.nextToken()) { - case VALUE_STRING: - result = parser.getText(); - break; - // Handle booleans promoted to enums - case VALUE_TRUE: - result = "true"; - break; - case VALUE_FALSE: - result = "false"; - break; - default: - expectEvent(JsonToken.VALUE_STRING); - } + String result = null; + switch (parser.nextToken()) { + case VALUE_STRING: + result = parser.getText(); + break; + // Handle booleans promoted to enums + case VALUE_TRUE: + result = "true"; + break; + case VALUE_FALSE: + result = "false"; + break; + default: + expectEvent(JsonToken.VALUE_STRING); + } - tb.copyCurrentEvent(parser); + tb.copyCurrentEvent(parser); - return new AbstractMap.SimpleImmutableEntry<>( - result, - new Jackson3JsonpParser( - JsonParserSequence.createFlattened(false, tb.asParser(), parser), - mapper - ) - ); - } else { - tb.copyCurrentStructure(parser); + return new AbstractMap.SimpleImmutableEntry<>( + result, + new Jackson3JsonpParser( + JsonParserSequence.createFlattened(false, tb.asParser(), parser), + mapper + ) + ); + } else { + tb.copyCurrentStructure(parser); + } } + // Copy ending END_OBJECT + tb.copyCurrentEvent(parser); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); } - // Copy ending END_OBJECT - tb.copyCurrentEvent(parser); // Field not found return new AbstractMap.SimpleImmutableEntry<>( @@ -346,33 +352,37 @@ public Map.Entry findVariant( // We're on a START_OBJECT event TokenBuffer tb = TokenBuffer.forBuffering(parser, parser.objectReadContext()); - if (parser.currentToken() != JsonToken.START_OBJECT) { - // Primitive value or array - tb.copyCurrentStructure(parser); - } else { - // 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), - mapper - ) - ); - } else { - tb.copyCurrentStructure(parser); + try { + if (parser.currentToken() != JsonToken.START_OBJECT) { + // Primitive value or array + tb.copyCurrentStructure(parser); + } else { + // 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), + mapper + ) + ); + } else { + tb.copyCurrentStructure(parser); + } } + // Copy ending END_OBJECT + tb.copyCurrentEvent(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. @@ -403,9 +413,13 @@ private void expectEvent(JsonToken expected) { @Override public JsonData getJsonData() { - TokenBuffer buffer = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext()); - buffer.copyCurrentStructure(parser); - return new Jackson3JsonBuffer(buffer, mapper); + try { + TokenBuffer buffer = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext()); + buffer.copyCurrentStructure(parser); + return new Jackson3JsonBuffer(buffer, mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } } } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3Utils.java b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3Utils.java new file mode 100644 index 000000000..fc9e02f8f --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3Utils.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package co.elastic.clients.json.jackson; + +import jakarta.json.JsonException; +import tools.jackson.core.JacksonException; + +class Jackson3Utils { + public static JsonException convertException(JacksonException je) { + return new JsonException("Jackson exception", je); + } +}