diff --git a/java-client/build.gradle.kts b/java-client/build.gradle.kts index bc24b71b7..47f612e33 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")) @@ -224,6 +225,11 @@ 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", jackson3Version) + implementation("tools.jackson.core", "jackson-core", jackson3Version) + // EPL-2.0 OR BSD-3-Clause // https://eclipse-ee4j.github.io/yasson/ testImplementation("org.eclipse", "yasson", "3.0.4") @@ -306,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 { 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..42f6de501 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonBuffer.java @@ -0,0 +1,118 @@ +/* + * 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.core.JacksonException; +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; + try { + buffer.serialize(jkGenerator.jacksonGenerator()); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } 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..55561da00 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonProvider.java @@ -0,0 +1,299 @@ +/* + * 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.JacksonException; +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) { + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(reader), mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public JsonParser createParser(InputStream in) { + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(in), mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public JsonParser createParser(InputStream in, Charset charset) { + try { + return new Jackson3JsonpParser(mapper.objectMapper().createParser(new InputStreamReader(in, + charset)), + mapper); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + /** + * 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) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(writer)); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public JsonGenerator createGenerator(OutputStream out) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(out)); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public JsonGenerator createGenerator(OutputStream out, Charset charset) { + try { + return new Jackson3JsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, + charset))); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @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..e23dcccfc --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpGenerator.java @@ -0,0 +1,414 @@ +/* + * 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.JacksonException; +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(TokenBuffer.forGeneration()); + 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(); + try { + jacksonGenerator().copyCurrentStructure(jkParser); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + } + + /** + * Returns the underlying Jackson generator. + */ + public tools.jackson.core.JsonGenerator jacksonGenerator() { + return generator; + } + + @Override + public JsonGenerator writeStartObject() { + try { + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartObject(String name) { + try { + generator.writeName(name); + generator.writeStartObject(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartArray() { + try { + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeStartArray(String name) { + try { + generator.writeName(name); + generator.writeStartArray(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeKey(String name) { + try { + generator.writeName(name); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, JsonValue value) { + try { + generator.writeName(name); + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, String value) { + try { + generator.writeName(name); + generator.writeString(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, BigInteger value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, BigDecimal value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, int value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, long value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, double value) { + try { + generator.writeName(name); + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String name, boolean value) { + try { + generator.writeName(name); + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeNull(String name) { + try { + generator.writeName(name); + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeEnd() { + try { + TokenStreamContext ctx = generator.streamWriteContext(); + if (ctx.inObject()) { + generator.writeEndObject(); + } else if (ctx.inArray()) { + generator.writeEndArray(); + } else { + throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'"); + } + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(JsonValue value) { + try { + writeValue(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(String value) { + try { + generator.writeString(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(BigDecimal value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(BigInteger value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(int value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(long value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(double value) { + try { + generator.writeNumber(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator write(boolean value) { + try { + generator.writeBoolean(value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public JsonGenerator writeNull() { + try { + generator.writeNull(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + return this; + } + + @Override + public void close() { + try { + generator.close(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @Override + public void flush() { + try { + generator.flush(); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + private void writeValue(JsonValue value) { + 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..03b0df5bf --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpMapper.java @@ -0,0 +1,165 @@ +/* + * 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.core.JacksonException; +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) + .disable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .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(); + try { + objectMapper.writeValue(jkGenerator, value); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + } + + @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(); + + 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 new file mode 100644 index 000000000..3c448acfa --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/Jackson3JsonpParser.java @@ -0,0 +1,425 @@ +/* + * 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.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; + +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 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 " + + "next event"); + } + } + + @Override + public boolean hasNext() { + if (hasNextWasCalled) { + return parser.currentToken() != null; + } else { + hasNextWasCalled = true; + return fetchNextToken() != null; + } + } + + @Override + public Event next() { + JsonToken token; + if (hasNextWasCalled) { + token = parser.currentToken(); + hasNextWasCalled = false; + } else token = fetchNextToken(); + + if (token == null) { + throw new NoSuchElementException(); + } + + Event result = tokenToEvent.get(token); + if (result == null) { + throw new JsonParsingException("Unsupported Jackson event type '" + token + "'", getLocation()); + } + + return result; + } + + @Override + public String getString() { + ensureTokenIsCurrent(); + 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()); + + try { + // The resulting parser must contain the full object, including START_EVENT + tb.copyCurrentEvent(parser); + while (parser.nextToken() != JsonToken.END_OBJECT) { + + expectEvent(JsonToken.PROPERTY_NAME); + // Do not copy current event here, each branch will take care of it + + String fieldName = parser.currentName(); + if (fieldName.equals(name)) { + // Found + tb.copyCurrentEvent(parser); + + 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); + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + + // 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()); + + 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); + } + } catch (JacksonException e) { + throw Jackson3Utils.convertException(e); + } + + // No variant found: return the buffered parser and let the caller decide what to do. + return new AbstractMap.SimpleImmutableEntry<>( + null, + new Jackson3JsonpParser( + JsonParserSequence.createFlattened(false, tb.asParser(), parser), + 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() { + 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); + } +} 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(); 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: 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;