Skip to content

Commit

Permalink
Allow serializing aggregations without typed keys (#316) (#325)
Browse files Browse the repository at this point in the history
Co-authored-by: Sylvain Wallez <sylvain@elastic.co>
  • Loading branch information
github-actions[bot] and swallez authored Jun 22, 2022
1 parent 7735ed4 commit 051da99
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 61 deletions.
2 changes: 2 additions & 0 deletions docs/troubleshooting/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
.Exceptions

* <<missing-required-property>>
* <<serialize-without-typed-keys>>


// [[debugging]]
Expand All @@ -16,3 +17,4 @@
// === Elasticsearch deprecation warnings

include::missing-required-property.asciidoc[]
include::serialize-without-typed-keys.asciidoc[]
24 changes: 24 additions & 0 deletions docs/troubleshooting/serialize-without-typed-keys.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[[serialize-without-typed-keys]]
=== Serializing aggregations and suggestions without typed keys

{es} search requests accept a `typed_key` parameter that allow returning type information along with the name in aggregation and suggestion results (see the {es-docs}/search-aggregations.html#return-agg-type[aggregations documentation] for additional details).

The {java-client} always adds this parameter to search requests, as type information is needed to know the concrete class that should be used to deserialize aggregation and suggestion results.

Symmetrically, the {java-client} also serializes aggregation and suggestion results using this `typed_keys` format, so that it can correctly deserialize the results of its own serialization.

["source","java"]
--------------------------------------------------
ElasticsearchClient esClient = ...
include-tagged::{doc-tests-src}/troubleshooting/TroubleShootingTests.java[aggregation-typed-keys]
--------------------------------------------------

However, in some use cases serializing objects in the `typed_keys` format may not be desirable, for example when the {java-client} is used in an application that acts as a front-end to other services that expect the default format for aggregations and suggestions.

You can disable `typed_keys` serialization by setting the `JsonpMapperFeatures.SERIALIZE_TYPED_KEYS` attribute to `false` on your mapper object:

["source","java"]
--------------------------------------------------
ElasticsearchClient esClient = ...
include-tagged::{doc-tests-src}/troubleshooting/TroubleShootingTests.java[aggregation-no-typed-keys]
--------------------------------------------------
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper,
}

/**
* Serialize an externally tagged union using the typed keys encoding.
* Serialize a map of externally tagged union objects.
* <p>
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
* (<code>type#name</code>) is used.
*/
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeys(
Map<String, T> map, JsonGenerator generator, JsonpMapper mapper
Expand All @@ -163,36 +166,65 @@ public void deserializeEntry(String key, JsonParser parser, JsonpMapper mapper,
generator.writeEnd();
}

/**
* Serialize a map of externally tagged union object arrays.
* <p>
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
* (<code>type#name</code>) is used.
*/
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeysArray(
Map<String, List<T>> map, JsonGenerator generator, JsonpMapper mapper
) {
generator.writeStartObject();
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
List<T> list = entry.getValue();
if (list.isEmpty()) {
continue; // We can't know the kind, skip this entry
}

generator.writeKey(list.get(0)._kind().jsonValue() + "#" + entry.getKey());
generator.writeStartArray();
for (T value: list) {
value.serialize(generator, mapper);
if (mapper.attribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, true)) {
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
List<T> list = entry.getValue();
if (list.isEmpty()) {
continue; // We can't know the kind, skip this entry
}

generator.writeKey(list.get(0)._kind().jsonValue() + "#" + entry.getKey());
generator.writeStartArray();
for (T value: list) {
value.serialize(generator, mapper);
}
generator.writeEnd();
}
} else {
for (Map.Entry<String, List<T>> entry: map.entrySet()) {
generator.writeKey(entry.getKey());
generator.writeStartArray();
for (T value: entry.getValue()) {
value.serialize(generator, mapper);
}
generator.writeEnd();
}
generator.writeEnd();
}

generator.writeEnd();
}

/**
* Serialize an externally tagged union using the typed keys encoding, without the enclosing start/end object.
* Serialize a map of externally tagged union objects, without the enclosing start/end object.
* <p>
* If {@link JsonpMapperFeatures#SERIALIZE_TYPED_KEYS} is <code>true</code> (the default), the typed keys encoding
* (<code>type#name</code>) is used.
*/
static <T extends JsonpSerializable & TaggedUnion<? extends JsonEnum, ?>> void serializeTypedKeysInner(
Map<String, T> map, JsonGenerator generator, JsonpMapper mapper
) {
for (Map.Entry<String, T> entry: map.entrySet()) {
T value = entry.getValue();
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
value.serialize(generator, mapper);
if (mapper.attribute(JsonpMapperFeatures.SERIALIZE_TYPED_KEYS, true)) {
for (Map.Entry<String, T> entry: map.entrySet()) {
T value = entry.getValue();
generator.writeKey(value._kind().jsonValue() + "#" + entry.getKey());
value.serialize(generator, mapper);
}
} else {
for (Map.Entry<String, T> entry: map.entrySet()) {
generator.writeKey(entry.getKey());
entry.getValue().serialize(generator, mapper);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ default <T> T attribute(String name, T defaultValue) {
}

/**
* Create a new mapper with a named attribute that delegates to this one.
* Create a new mapper with an additional attribute.
* <p>
* The {@link JsonpMapperFeatures} class contains the names of attributes that all implementations of
* <code>JsonpMapper</code> must implement.
*
* @see JsonpMapperFeatures
*/
default <T> JsonpMapper withAttribute(String name, T value) {
return new AttributedJsonpMapper(this, name, value);
}
<T> JsonpMapper withAttribute(String name, T value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,41 @@

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public abstract class JsonpMapperBase implements JsonpMapper {

/** Get a serializer when none of the builtin ones are applicable */
protected abstract <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz);

private Map<String, Object> attributes;

@Nullable
@Override
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
return attributes == null ? null : (T)attributes.get(name);
}

/**
* Updates attributes to a copy of the current ones with an additional key/value pair.
*
* Mutates the current mapper, intended to be used in implementations of {@link #withAttribute(String, Object)}
*/
protected JsonpMapperBase addAttribute(String name, Object value) {
if (attributes == null) {
this.attributes = Collections.singletonMap(name, value);
} else {
Map<String, Object> newAttrs = new HashMap<>(attributes.size() + 1);
newAttrs.putAll(attributes);
newAttrs.put(name, value);
this.attributes = newAttrs;
}
return this;
}

@Override
public <T> T deserialize(JsonParser parser, Class<T> clazz) {
JsonpDeserializer<T> deserializer = findDeserializer(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,11 @@

package co.elastic.clients.json;

import javax.annotation.Nullable;

class AttributedJsonpMapper extends DelegatingJsonpMapper {

private final String name;
private final Object value;
/**
* Defines attribute names for {@link JsonpMapper} features.
*/
public class JsonpMapperFeatures {

AttributedJsonpMapper(JsonpMapper mapper, String name, Object value) {
super(mapper);
this.name = name;
this.value = value;
}
public static final String SERIALIZE_TYPED_KEYS = JsonpMapperFeatures.class.getName() + ":SERIALIZE_TYPED_KEYS";

@Override
@Nullable
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
if (this.name.equals(name)) {
return (T)this.value;
} else {
return mapper.attribute(name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public SimpleJsonpMapper() {
this(true);
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new SimpleJsonpMapper(this.ignoreUnknownFields).addAttribute(name, value);
}

@Override
public boolean ignoreUnknownFields() {
return ignoreUnknownFields;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,30 @@ public class JacksonJsonpMapper extends JsonpMapperBase {
private final JacksonJsonProvider provider;
private final ObjectMapper objectMapper;

private JacksonJsonpMapper(ObjectMapper objectMapper, JacksonJsonProvider provider) {
this.objectMapper = objectMapper;
this.provider = provider;
}

public JacksonJsonpMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec()
this.provider = new JacksonJsonProvider(this.objectMapper.getFactory());
this(
objectMapper
.configure(SerializationFeature.INDENT_OUTPUT, false)
.setSerializationInclusion(JsonInclude.Include.NON_NULL),
// Creating the json factory from the mapper ensures it will be returned by JsonParser.getCodec()
new JacksonJsonProvider(objectMapper.getFactory())
);
}

public JacksonJsonpMapper() {
this(new ObjectMapper());
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new JacksonJsonpMapper(this.objectMapper, this.provider).addAttribute(name, value);
}

/**
* Returns the underlying Jackson mapper.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public JsonbJsonpMapper() {
this(JsonpUtils.provider(), JsonbProvider.provider());
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new JsonbJsonpMapper(this.jsonProvider, this.jsonb).addAttribute(name, value);
}

@Override
protected <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz) {
return new Deserializer<>(clazz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,33 @@ public B withJson(JsonParser parser, JsonpMapper mapper) {
}

// Generic parameters are always deserialized to JsonData unless the parent mapper can provide a deserializer
mapper = new DelegatingJsonpMapper(mapper) {
@Override
public <T> T attribute(String name) {
T attr = mapper.attribute(name);
if (attr == null && name.startsWith("co.elastic.clients:Deserializer")) {
@SuppressWarnings("unchecked")
T result = (T)JsonData._DESERIALIZER;
return result;
} else {
return attr;
}
}
};
mapper = new WithJsonMapper(mapper);

@SuppressWarnings("unchecked")
ObjectDeserializer<B> builderDeser = (ObjectDeserializer<B>) DelegatingDeserializer.unwrap(classDeser);
return builderDeser.deserialize(self(), parser, mapper, parser.next());
}

private static class WithJsonMapper extends DelegatingJsonpMapper {
WithJsonMapper(JsonpMapper parent) {
super(parent);
}

@Override
public <T> T attribute(String name) {
T attr = mapper.attribute(name);
if (attr == null && name.startsWith("co.elastic.clients:Deserializer")) {
@SuppressWarnings("unchecked")
T result = (T)JsonData._DESERIALIZER;
return result;
} else {
return attr;
}
}

@Override
public <T> JsonpMapper withAttribute(String name, T value) {
return new WithJsonMapper(this.mapper.withAttribute(name, value));
}
}
}
Loading

0 comments on commit 051da99

Please sign in to comment.