Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 7.17] Allow serializing aggregations without typed keys #325

Merged
merged 1 commit into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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