From 78e8ee2ff16133fb53ab8b9841f3f6e4a343b76f Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Tue, 15 Mar 2022 16:43:52 +0100 Subject: [PATCH] Use a default mapper in withJson methods --- docs/loading-json.asciidoc | 14 +- .../clients/json/SimpleJsonpMapper.java | 123 ++++++++++++++++++ .../co/elastic/clients/json/WithJson.java | 23 ++-- ...{LoadingJson.java => LoadingJsonTest.java} | 102 ++++++++++----- .../elasticsearch/model/ModelTestCase.java | 39 +++--- .../co/elastic/clients/json/WithJsonTest.java | 14 +- 6 files changed, 242 insertions(+), 73 deletions(-) create mode 100644 java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java rename java-client/src/test/java/co/elastic/clients/documentation/{LoadingJson.java => LoadingJsonTest.java} (62%) diff --git a/docs/loading-json.asciidoc b/docs/loading-json.asciidoc index 2292b83f3..cecb7b2a8 100644 --- a/docs/loading-json.asciidoc +++ b/docs/loading-json.asciidoc @@ -30,10 +30,10 @@ You can create an index from that definition as follows: ["source","java"] -------------------------------------------------- -include-tagged::{doc-tests}/LoadingJson.java[load-index] +include-tagged::{doc-tests}/LoadingJsonTest.java[load-index] -------------------------------------------------- -<1> the input stream for the resource file. -<2> a {java-client} JSON mapper, used to create a JSON parser and find object deserializers. This will generally be the client's mapper. +<1> open an input stream for the JSON resource file. +<2> populate the index creation request with the resource file contents. [discrete] ==== Ingesting documents from JSON files @@ -42,9 +42,9 @@ Similarly, you can read documents to be stored in {es} from data files: ["source","java"] -------------------------------------------------- -include-tagged::{doc-tests}/LoadingJson.java[ingest-data] +include-tagged::{doc-tests}/LoadingJsonTest.java[ingest-data] -------------------------------------------------- -<1> When calling `withJson()` on data structures that have generic type parameters, these generic types will be considered to be `JsonData`. +<1> when calling `withJson()` on data structures that have generic type parameters, these generic types will be considered to be `JsonData`. [discrete] ==== Creating a search request combining JSON and programmatic construction @@ -53,7 +53,7 @@ You can combine `withJson()` with regular calls to setter methods. The example b ["source","java"] -------------------------------------------------- -include-tagged::{doc-tests}/LoadingJson.java[query] +include-tagged::{doc-tests}/LoadingJsonTest.java[query] -------------------------------------------------- <1> loads the query from the JSON string. <2> adds the aggregation. @@ -66,7 +66,7 @@ The `withJson()` methods are partial deserializers: the properties loaded from t ["source","java"] -------------------------------------------------- -include-tagged::{doc-tests}/LoadingJson.java[query-and-agg] +include-tagged::{doc-tests}/LoadingJsonTest.java[query-and-agg] -------------------------------------------------- <1> loads the query part of the request. <2> loads the aggregation part of the request. diff --git a/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java new file mode 100644 index 000000000..c1cd35efd --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java @@ -0,0 +1,123 @@ +/* + * 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; + +import jakarta.json.JsonException; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonGenerator; + +import java.util.HashMap; +import java.util.Map; + +/** + * A simple implementation of JsonpMapper that only handles classes of the Java API client. + *

+ * To handle application classes serialization and deserialization, consider using JacksonJsonpMapper or + * JsonbJsonpMapper. + */ +public class SimpleJsonpMapper extends JsonpMapperBase { + + public static SimpleJsonpMapper INSTANCE = new SimpleJsonpMapper(true); + public static SimpleJsonpMapper INSTANCE_REJECT_UNKNOWN_FIELDS = new SimpleJsonpMapper(false); + + private static final Map, JsonpSerializer> serializers = new HashMap<>(); + private static final Map, JsonpDeserializer> deserializers = new HashMap<>(); + + static { + serializers.put(String.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Boolean.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Boolean.TYPE, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Integer.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Integer.TYPE, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Long.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Long.TYPE, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Float.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Float.TYPE, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Double.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + serializers.put(Double.TYPE, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); + + deserializers.put(String.class, JsonpDeserializer.stringDeserializer()); + deserializers.put(Boolean.class, JsonpDeserializer.booleanDeserializer()); + deserializers.put(Boolean.TYPE, JsonpDeserializer.booleanDeserializer()); + deserializers.put(Integer.class, JsonpDeserializer.integerDeserializer()); + deserializers.put(Integer.TYPE, JsonpDeserializer.integerDeserializer()); + deserializers.put(Long.class, JsonpDeserializer.longDeserializer()); + deserializers.put(Long.TYPE, JsonpDeserializer.longDeserializer()); + deserializers.put(Float.class, JsonpDeserializer.floatDeserializer()); + deserializers.put(Float.TYPE, JsonpDeserializer.floatDeserializer()); + deserializers.put(Double.class, JsonpDeserializer.doubleDeserializer()); + deserializers.put(Double.TYPE, JsonpDeserializer.doubleDeserializer()); + } + + private final boolean ignoreUnknownFields; + + public SimpleJsonpMapper(boolean ignoreUnknownFields) { + this.ignoreUnknownFields = ignoreUnknownFields; + } + + public SimpleJsonpMapper() { + // Lenient by default + this(true); + } + + @Override + public boolean ignoreUnknownFields() { + return ignoreUnknownFields; + } + + @Override + public JsonProvider jsonProvider() { + return JsonpUtils.provider(); + } + + @Override + public void serialize(T value, JsonGenerator generator) { + JsonpSerializer serializer = findSerializer(value); + + if (serializer == null) { + @SuppressWarnings("unchecked") + JsonpSerializer serializer_ = (JsonpSerializer)serializers.get(value.getClass()); + serializer = serializer_; + } + + if (serializer != null) { + serializer.serialize(value, generator, this); + } else { + throw new JsonException( + "Cannot find a serializer for type " + value.getClass().getName() + + ". Consider using a full-featured JsonpMapper" + ); + } + } + + @Override + protected JsonpDeserializer getDefaultDeserializer(Class clazz) { + @SuppressWarnings("unchecked") + JsonpDeserializer deserializer = (JsonpDeserializer) deserializers.get(clazz); + if (deserializer != null) { + return deserializer; + } else { + throw new JsonException( + "Cannot find a deserializer for type " + clazz.getName() + + ". Consider using a full-featured JsonpMapper" + ); + } + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/WithJson.java b/java-client/src/main/java/co/elastic/clients/json/WithJson.java index baccbdafe..c699c8bfd 100644 --- a/java-client/src/main/java/co/elastic/clients/json/WithJson.java +++ b/java-client/src/main/java/co/elastic/clients/json/WithJson.java @@ -35,11 +35,13 @@ public interface WithJson { * This is a "partial deserialization": properties that were already set keep their value if they're not present in the JSON input, * and properties can also be set after having called this method, including overriding those read from the JSON input. * - * @param parser the JSONP parser - * @param mapper the JSONP mapper used to deserialize values and nested objects + * @param input the stream to read from * @return this object */ - T withJson(JsonParser parser, JsonpMapper mapper); + default T withJson(InputStream input) { + JsonpMapper mapper = SimpleJsonpMapper.INSTANCE_REJECT_UNKNOWN_FIELDS; + return withJson(mapper.jsonProvider().createParser(input), mapper); + } /** * Sets additional properties values on this object by reading from a JSON input. @@ -47,11 +49,11 @@ public interface WithJson { * This is a "partial deserialization": properties that were already set keep their value if they're not present in the JSON input, * and properties can also be set after having called this method, including overriding those read from the JSON input. * - * @param input the stream to read from - * @param mapper the JSONP mapper used to deserialize values and nested objects + * @param input the reader to read from * @return this object */ - default T withJson(InputStream input, JsonpMapper mapper) { + default T withJson(Reader input) { + JsonpMapper mapper = SimpleJsonpMapper.INSTANCE_REJECT_UNKNOWN_FIELDS; return withJson(mapper.jsonProvider().createParser(input), mapper); } @@ -60,12 +62,13 @@ default T withJson(InputStream input, JsonpMapper mapper) { *

* This is a "partial deserialization": properties that were already set keep their value if they're not present in the JSON input, * and properties can also be set after having called this method, including overriding those read from the JSON input. + *

+ * This low level variant of withJson gives full control on the json parser and object mapper. Most of the time + * using {@link #withJson(Reader)} and {@link #withJson(InputStream)} will be more convenient. * - * @param input the stream to read from + * @param parser the JSONP parser * @param mapper the JSONP mapper used to deserialize values and nested objects * @return this object */ - default T withJson(Reader input, JsonpMapper mapper) { - return withJson(mapper.jsonProvider().createParser(input), mapper); - } + T withJson(JsonParser parser, JsonpMapper mapper); } diff --git a/java-client/src/test/java/co/elastic/clients/documentation/LoadingJson.java b/java-client/src/test/java/co/elastic/clients/documentation/LoadingJsonTest.java similarity index 62% rename from java-client/src/test/java/co/elastic/clients/documentation/LoadingJson.java rename to java-client/src/test/java/co/elastic/clients/documentation/LoadingJsonTest.java index 78089943d..423074eb6 100644 --- a/java-client/src/test/java/co/elastic/clients/documentation/LoadingJson.java +++ b/java-client/src/test/java/co/elastic/clients/documentation/LoadingJsonTest.java @@ -24,6 +24,8 @@ import co.elastic.clients.elasticsearch._types.aggregations.CalendarInterval; import co.elastic.clients.elasticsearch.core.IndexRequest; import co.elastic.clients.elasticsearch.core.SearchRequest; +import co.elastic.clients.elasticsearch.core.SearchResponse; +import co.elastic.clients.elasticsearch.core.search.TotalHitsRelation; import co.elastic.clients.elasticsearch.indices.CreateIndexRequest; import co.elastic.clients.elasticsearch.indices.CreateIndexResponse; import co.elastic.clients.elasticsearch.model.ModelTestCase; @@ -37,13 +39,30 @@ import java.io.InputStream; import java.io.Reader; import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; import java.util.Map; -public class LoadingJson extends ModelTestCase { +public class LoadingJsonTest extends ModelTestCase { private DocTestsTransport transport = new DocTestsTransport(); private ElasticsearchClient client = new ElasticsearchClient(transport); + private static SearchResponse searchResponse = SearchResponse.searchResponseOf(b -> b + .aggregations(new HashMap<>()) + .took(0) + .timedOut(false) + .hits(h -> h + .total(t -> t.value(0).relation(TotalHitsRelation.Eq)) + .hits(new ArrayList<>()) + ) + .shards(s -> s + .total(1) + .failed(0) + .successful(1) + ) + ); + @Test public void loadIndexDefinition() throws IOException { @@ -54,15 +73,12 @@ public void loadIndexDefinition() throws IOException { )); //tag::load-index - InputStream jsonStream = this.getClass() - .getResourceAsStream("some-index.json"); + InputStream input = this.getClass() + .getResourceAsStream("some-index.json"); //<1> CreateIndexRequest req = CreateIndexRequest.of(b -> b .index("some-index") - .withJson( - jsonStream, //<1> - client._jsonpMapper() //<2> - ) + .withJson(input) //<2> ); boolean created = client.indices().create(req).acknowledged(); @@ -82,15 +98,17 @@ public void ingestDocument() throws IOException { req = IndexRequest.of(b -> b .index("some-index") - .withJson(file, client._jsonpMapper()) + .withJson(file) ); client.index(req); //end::ingest-data } - + @Test public void query1() throws IOException { + transport.setResult(searchResponse); + //tag::query Reader queryJson = new StringReader( "{" + @@ -102,9 +120,9 @@ public void query1() throws IOException { " }" + " }" + "}"); - - SearchRequest agg1 = SearchRequest.of(b -> b - .withJson(queryJson, client._jsonpMapper()) //<1> + + SearchRequest aggRequest = SearchRequest.of(b -> b + .withJson(queryJson) //<1> .aggregations("max-cpu", a1 -> a1 //<2> .dateHistogram(h -> h .field("@timestamp") @@ -117,38 +135,56 @@ public void query1() throws IOException { .size(0) ); - Map aggs1 = client - .search(agg1, Void.class) //<3> + Map aggs = client + .search(aggRequest, Void.class) //<3> .aggregations(); //end::query - + } + + @Test + public void query2() throws IOException { + transport.setResult(searchResponse); + //tag::query-and-agg - Reader aggJson = new StringReader( - "\"size\": 0, " + - "\"aggs\": {" + - " \"hours\": {" + - " \"date_histogram\": {" + - " \"field\": \"@timestamp\"," + - " \"interval\": \"hour\"" + - " }," + - " \"aggs\": {" + - " \"max-cpu\": {" + - " \"max\": {" + - " \"field\": \"host.cpu.usage\"" + + Reader queryJson = new StringReader( + "{" + + " \"query\": {" + + " \"range\": {" + + " \"@timestamp\": {" + + " \"gt\": \"now-1w\"" + + " }" + + " }" + + " }" + + "}"); + + Reader aggregationJson = new StringReader( + "{" + + " \"size\": 0, " + + " \"aggregations\": {" + + " \"hours\": {" + + " \"date_histogram\": {" + + " \"field\": \"@timestamp\"," + + " \"interval\": \"hour\"" + + " }," + + " \"aggregations\": {" + + " \"max-cpu\": {" + + " \"max\": {" + + " \"field\": \"host.cpu.usage\"" + + " }" + " }" + " }" + " }" + " }" + "}"); - - SearchRequest agg2 = SearchRequest.of(b -> b - .withJson(queryJson, client._jsonpMapper()) //<1> - .withJson(aggJson, client._jsonpMapper()) //<2> + + SearchRequest aggRequest = SearchRequest.of(b -> b + .withJson(queryJson) //<1> + .withJson(aggregationJson) //<2> .ignoreUnavailable(true) //<3> ); - Map aggs2 = client - .search(agg2, Void.class) + Map aggs = client + .search(aggRequest, Void.class) .aggregations(); //end::query-and-agg } diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java index a7287309e..39e5a7cf9 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java @@ -21,6 +21,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.JacksonJsonpMapper; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import jakarta.json.spi.JsonProvider; @@ -45,22 +46,28 @@ public abstract class ModelTestCase extends Assert { private JsonpMapper setupMapper(int rand) { // Randomly choose json-b or jackson - if (rand % 2 == 0) { - System.out.println("Using a JsonB mapper (rand = " + rand + ")."); - return new JsonbJsonpMapper() { - @Override - public boolean ignoreUnknownFields() { - return false; - } - }; - } else { - System.out.println("Using a Jackson mapper (rand = " + rand + ")."); - return new JacksonJsonpMapper() { - @Override - public boolean ignoreUnknownFields() { - return false; - } - }; + switch(rand % 3) { + case 0: + System.out.println("Using a JsonB mapper (rand = " + rand + ")."); + return new JsonbJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + + case 1: + System.out.println("Using a Jackson mapper (rand = " + rand + ")."); + return new JacksonJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + + default: + System.out.println("Using a simple mapper (rand = " + rand + ")."); + return SimpleJsonpMapper.INSTANCE_REJECT_UNKNOWN_FIELDS; } } diff --git a/java-client/src/test/java/co/elastic/clients/json/WithJsonTest.java b/java-client/src/test/java/co/elastic/clients/json/WithJsonTest.java index e4b8836ec..5d1770b4b 100644 --- a/java-client/src/test/java/co/elastic/clients/json/WithJsonTest.java +++ b/java-client/src/test/java/co/elastic/clients/json/WithJsonTest.java @@ -41,7 +41,7 @@ public void testRequestWithGenericValueBody() { IndexRequest req = IndexRequest.of(b -> b .index("index") // required path parameter (cannot be expressed in json) - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); assertEquals("index", req.index()); @@ -56,7 +56,7 @@ public void testRequestWithValueBody() { "}"; PutIndicesSettingsRequest req = PutIndicesSettingsRequest.of(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); assertEquals(12, req.settings().analyzeMaxTokenCount().intValue()); @@ -67,7 +67,7 @@ public void testRegularObject() { String json = "{\"field\": \"foo\", \"id\": 12}"; SlicedScroll s = SlicedScroll.of(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) .max(34) // required property not present in the json ); @@ -109,7 +109,7 @@ public void testObjectWithGenericParam() { // withJson() will read values of the generic parameter type as JsonData SearchResponse r = SearchResponse.searchResponseOf(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); assertEquals(1, r.hits().total().value()); @@ -122,7 +122,7 @@ public void testTypeWithParent() { String json = "{\"source\": \"return doc;\"}"; InlineScript is = InlineScript.of(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); assertEquals("return doc;", is.source()); @@ -140,7 +140,7 @@ public void testContainerTaggedUnion() { " }"; Query q = Query.of(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); TermQuery tq = q.term(); @@ -162,7 +162,7 @@ public void testInternallyTaggedUnion() { " }"; Property p = Property.of(b -> b - .withJson(new StringReader(json), mapper) + .withJson(new StringReader(json)) ); TextProperty tp = p.text();