Skip to content

Commit

Permalink
Add support framework to populate builders from JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
swallez committed Mar 14, 2022
1 parent 941b0fd commit 1be4c36
Show file tree
Hide file tree
Showing 19 changed files with 706 additions and 54 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ gradle-app.setting
*.iml

.ci/output

# HTML files produced by the docs build
html_docs
1 change: 1 addition & 0 deletions docs/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ include::installation.asciidoc[]
include::connecting.asciidoc[]
include::migrate.asciidoc[]
include::api-conventions.asciidoc[]
include::loading-json.asciidoc[]
include::javadoc-and-source.asciidoc[]

include::{elasticsearch-root}/docs/java-rest/low-level/index.asciidoc[]
68 changes: 68 additions & 0 deletions docs/loading-json.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[[loading-json]]
== Creating API objects from JSON data

A common workflow during application development with Elasticsearch is to use the Kibana Developer Console to interactively prepare and test queries, aggregations, index mappings and other complex API calls. This results in working JSON snippets that you may want to use in your application.

As translating these JSON snippets to Java code can be time-consuming and error-prone, most of the data classes in the {java-client} can be loaded from JSON text: object builders have `withJson()` methods that populate the builder from raw JSON. This also allows you to combine dynamically loaded JSON with programmatic construction of objects.

Under the hood, the `withJson()` methods call the object's deserializer. The JSON text's structure and value types therefore have to be correct for the target data structure. Using `withJson()` keeps the strong typing guarantees of the {java-client}.

=== Examples

==== Loading an index definition from a resource file

Consider a resource file `some-index.json` containing an index definition:

["source", "json"]
--------------------------------------------------
{
"mappings": {
"properties": {
"field1": { "type": "text" }
}
}
}
--------------------------------------------------

You can create an index from that definition as follows:

["source","java"]
--------------------------------------------------
include-tagged::{doc-tests}/LoadingJson.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.

==== Ingesting documents from JSON files

Similarly, you can read documents to be stored in {es} from data files:

["source","java"]
--------------------------------------------------
include-tagged::{doc-tests}/LoadingJson.java[ingest-data]
--------------------------------------------------
<1> When calling `withJson()` on data structures that have generic type parameters, these generic types will be considered to be `JsonData`.

==== Creating a search request combining JSON and programmatic construction

You can combine `withJson()` with regular calls to setter methods. The example below loads the query part of a search request from a `String` and programmatically adds an aggregation.

["source","java"]
--------------------------------------------------
include-tagged::{doc-tests}/LoadingJson.java[query]
--------------------------------------------------
<1> loads the query from the JSON string.
<2> adds the aggregation.
<3> since this is an aggregation we don't care about result documents and set their target class to `Void`, meaning they will just be ignored. Note that setting `size` to zero actually prevents any document from being returned.

==== Creating a search request from multiple JSON snippets

The `withJson()` methods are partial deserializers: the properties loaded from the JSON will set property values or replace the previous ones, but will not reset other properties not found in the JSON input. You can use this to combine multiple JSON snippets to build complex search requests (this uses `queryJson` from the previous example):

["source","java"]
--------------------------------------------------
include-tagged::{doc-tests}/LoadingJson.java[query-and-agg]
--------------------------------------------------
<1> loads the query part of the request.
<2> loads the aggregation part of the request.
<3> additional request properties set programmatically.
13 changes: 13 additions & 0 deletions java-client/src/main/java/co/elastic/clients/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package co.elastic.clients;

import co.elastic.clients.json.JsonpMapper;
import co.elastic.clients.transport.TransportOptions;
import co.elastic.clients.transport.Transport;
import co.elastic.clients.json.JsonpDeserializer;
Expand Down Expand Up @@ -51,11 +52,23 @@ protected <V> JsonpDeserializer<V> getDeserializer(Class<V> clazz) {
*/
public abstract Self withTransportOptions(@Nullable TransportOptions transportOptions);

/**
* Get the transport used by this client to handle communication with the server.
*/
public T _transport() {
return this.transport;
}

public TransportOptions _transportOptions() {
return this.transportOptions;
}

/**
* Get the JSON mapper used to map API objects to/from JSON.
* <p>
* Shortcut for <code>_transport().jsonpMapper()</code>
*/
public JsonpMapper _jsonpMapper() {
return transport.jsonpMapper();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,21 @@

package co.elastic.clients.json;

import jakarta.json.spi.JsonProvider;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;
import javax.annotation.Nullable;

class AttributedJsonpMapper implements JsonpMapper {
class AttributedJsonpMapper extends DelegatingJsonpMapper {

private final JsonpMapper mapper;
private final String name;
private final Object value;

AttributedJsonpMapper(JsonpMapper mapper, String name, Object value) {
this.mapper = mapper;
super(mapper);
this.name = name;
this.value = value;
}

@Override
public JsonProvider jsonProvider() {
return mapper.jsonProvider();
}

@Override
public <T> T deserialize(JsonParser parser, Class<T> clazz) {
return mapper.deserialize(parser, clazz);
}

@Override
public <T> void serialize(T value, JsonGenerator generator) {
mapper.serialize(value, generator);
}

@Override
public boolean ignoreUnknownFields() {
return mapper.ignoreUnknownFields();
}

@Override
@Nullable
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
if (this.name.equals(name)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.spi.JsonProvider;
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;

import javax.annotation.Nullable;

public class DelegatingJsonpMapper implements JsonpMapper {

protected final JsonpMapper mapper;

public DelegatingJsonpMapper(JsonpMapper mapper) {
this.mapper = mapper;
}

@Override
public JsonProvider jsonProvider() {
return mapper.jsonProvider();
}

@Override
public <T> T deserialize(JsonParser parser, Class<T> clazz) {
return mapper.deserialize(parser, clazz);
}

@Override
public <T> void serialize(T value, JsonGenerator generator) {
mapper.serialize(value, generator);
}

@Override
public boolean ignoreUnknownFields() {
return mapper.ignoreUnknownFields();
}

@Override
@Nullable
public <T> T attribute(String name) {
return mapper.attribute(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import jakarta.json.stream.JsonGenerator;
import jakarta.json.stream.JsonParser;

import javax.annotation.Nullable;

/**
* A {@code JsonpMapper} combines a JSON-P provider and object serialization/deserialization based on JSON-P events.
*/
Expand Down Expand Up @@ -61,6 +63,7 @@ default boolean ignoreUnknownFields() {
/**
* Get a named attribute associated to this mapper.
*/
@Nullable
default <T> T attribute(String name) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ public EnumSet<JsonParser.Event> acceptedEvents() {

@Override
public T deserialize(JsonParser parser, JsonpMapper mapper) {
if (mapper.<JsonpDeserializer<T>>attribute(name) == null) {
throw new JsonParsingException("Missing deserializer", parser.getLocation());
JsonpDeserializer<T> deserializer = mapper.attribute(name);
if (deserializer == null) {
throw new JsonParsingException("Missing deserializer for generic type: " + name, parser.getLocation());
}
return mapper.<JsonpDeserializer<T>>attribute(name).deserialize(parser, mapper);
return deserializer.deserialize(parser, mapper);
}

@Override
public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) {
if (mapper.<JsonpDeserializer<T>>attribute(name) == null) {
throw new JsonParsingException("Missing deserializer", parser.getLocation());
JsonpDeserializer<T> deserializer = mapper.attribute(name);
if (deserializer == null) {
throw new JsonParsingException("Missing deserializer for generic type: " + name, parser.getLocation());
}
return mapper.<JsonpDeserializer<T>>attribute(name).deserialize(parser, mapper, event);
return deserializer.deserialize(parser, mapper, event);
}
}
71 changes: 71 additions & 0 deletions java-client/src/main/java/co/elastic/clients/json/WithJson.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.stream.JsonParser;

import java.io.InputStream;
import java.io.Reader;

/**
* An object that can read its state, in whole or part, from JSON.
*/
public interface WithJson<T> {

/**
* Sets additional properties values on this object by reading from a JSON input.
* <p>
* 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
* @return this object
*/
T withJson(JsonParser parser, JsonpMapper mapper);

/**
* Sets additional properties values on this object by reading from a JSON input.
* <p>
* 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
* @return this object
*/
default T withJson(InputStream input, JsonpMapper mapper) {
return withJson(mapper.jsonProvider().createParser(input), mapper);
}

/**
* Sets additional properties values on this object by reading from a JSON input.
* <p>
* 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
* @return this object
*/
default T withJson(Reader input, JsonpMapper mapper) {
return withJson(mapper.jsonProvider().createParser(input), mapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import java.util.Map;
import java.util.Objects;

/**
* Base class for object builders.
*/
public class ObjectBuilderBase {
private boolean _used = false;

Expand Down
Loading

0 comments on commit 1be4c36

Please sign in to comment.