co.elastic.clients.json.UnionDeserializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch-java Show documentation
Show all versions of elasticsearch-java Show documentation
Elasticsearch Java API Client
/*
* 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 co.elastic.clients.util.ObjectBuilder;
import jakarta.json.JsonObject;
import jakarta.json.stream.JsonParser;
import jakarta.json.stream.JsonParser.Event;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
public class UnionDeserializer implements JsonpDeserializer {
public static class AmbiguousUnionException extends RuntimeException {
public AmbiguousUnionException(String message) {
super(message);
}
}
private abstract static class EventHandler {
abstract Union deserialize(JsonParser parser, JsonpMapper mapper, Event event, BiFunction buildFn);
abstract EnumSet nativeEvents();
}
private static class SingleMemberHandler extends EventHandler {
private final JsonpDeserializer extends Member> deserializer;
private final Kind tag;
// ObjectDeserializers provide the list of fields they know about
private final Set fields;
SingleMemberHandler(Kind tag, JsonpDeserializer extends Member> deserializer) {
this(tag, deserializer, null);
}
SingleMemberHandler(Kind tag, JsonpDeserializer extends Member> deserializer, Set fields) {
this.deserializer = deserializer;
this.tag = tag;
this.fields = fields;
}
@Override
EnumSet nativeEvents() {
return deserializer.nativeEvents();
}
@Override
Union deserialize(JsonParser parser, JsonpMapper mapper, Event event, BiFunction buildFn) {
return buildFn.apply(tag, deserializer.deserialize(parser, mapper, event));
}
}
/**
* An event handler for value events (string, number, etc) that can try multiple handlers, which are ordered
* from most specific (e.g. enum) to least specific (e.g. string)
*/
private static class MultiMemberHandler extends EventHandler {
private List> handlers;
@Override
EnumSet nativeEvents() {
EnumSet result = EnumSet.noneOf(Event.class);
for (SingleMemberHandler smh: handlers) {
result.addAll(smh.deserializer.nativeEvents());
}
return result;
}
@Override
Union deserialize(JsonParser parser, JsonpMapper mapper, Event event, BiFunction buildFn) {
RuntimeException exception = null;
for (EventHandler d: handlers) {
try {
return d.deserialize(parser, mapper, event, buildFn);
} catch(RuntimeException ex) {
exception = ex;
}
}
throw JsonpMappingException.from(exception, null, null, parser);
}
}
public static class Builder implements ObjectBuilder> {
private final BiFunction buildFn;
private final List> objectMembers = new ArrayList<>();
private final Map> otherMembers = new HashMap<>();
private final boolean allowAmbiguousPrimitive;
public Builder(BiFunction buildFn, boolean allowAmbiguities) {
// If we allow ambiguities, multiple handlers for a given JSON value event will be allowed
this.allowAmbiguousPrimitive = allowAmbiguities;
this.buildFn = buildFn;
}
private void addAmbiguousDeserializer(Event e, Kind tag, JsonpDeserializer extends Member> deserializer) {
EventHandler m = otherMembers.get(e);
MultiMemberHandler mmh;
if (m instanceof MultiMemberHandler, ?, ?>) {
mmh = (MultiMemberHandler) m;
} else {
mmh = new MultiMemberHandler<>();
mmh.handlers = new ArrayList<>(2);
mmh.handlers.add((SingleMemberHandler) m);
otherMembers.put(e, mmh);
}
mmh.handlers.add(new SingleMemberHandler<>(tag, deserializer));
// Sort handlers by number of accepted events, which gives their specificity
mmh.handlers.sort(Comparator.comparingInt(a -> a.deserializer.acceptedEvents().size()));
}
private void addMember(Event e, Kind tag, UnionDeserializer.SingleMemberHandler member) {
if (otherMembers.containsKey(e)) {
if (!allowAmbiguousPrimitive || e == Event.START_OBJECT || e == Event.START_ARRAY) {
throw new AmbiguousUnionException("Union member '" + tag + "' conflicts with other members");
} else {
// Allow ambiguities on value event
addAmbiguousDeserializer(e, tag, member.deserializer);
}
} else {
// Note: we accept START_OBJECT here. It can be a user-provided type, and will be used
// as a fallback if no element of objectMembers matches.
otherMembers.put(e, member);
}
}
public Builder addMember(Kind tag, JsonpDeserializer extends Member> deserializer) {
JsonpDeserializer> unwrapped = DelegatingDeserializer.unwrap(deserializer);
if (unwrapped instanceof ObjectDeserializer) {
ObjectDeserializer> od = (ObjectDeserializer>) unwrapped;
Set allFields = od.fieldNames();
Set fields = new HashSet<>(allFields); // copy to update
for (UnionDeserializer.SingleMemberHandler member: objectMembers) {
// Remove respective fields on both sides to keep specific ones
fields.removeAll(member.fields);
member.fields.removeAll(allFields);
}
UnionDeserializer.SingleMemberHandler member = new SingleMemberHandler<>(tag, deserializer, fields);
objectMembers.add(member);
if (od.shortcutProperty() != null) {
// also add it as a string
addMember(Event.VALUE_STRING, tag, member);
}
} else {
UnionDeserializer.SingleMemberHandler member = new SingleMemberHandler<>(tag, deserializer);
for (Event e: deserializer.nativeEvents()) {
addMember(e, tag, member);
}
}
return this;
}
@Override
public JsonpDeserializer build() {
// Check that no object member had all its fields removed
for (UnionDeserializer.SingleMemberHandler member: objectMembers) {
if (member.fields.isEmpty()) {
throw new AmbiguousUnionException("All properties of '" + member.tag + "' also exist in other object members");
}
}
if (objectMembers.size() == 1 && !otherMembers.containsKey(Event.START_OBJECT)) {
// A single deserializer handles objects: promote it to otherMembers as we don't need property-based disambiguation
otherMembers.put(Event.START_OBJECT, objectMembers.remove(0));
}
// if (objectMembers.size() > 1) {
// System.out.println("multiple objects in " + buildFn);
// }
return new UnionDeserializer<>(objectMembers, otherMembers, buildFn);
}
}
private final BiFunction buildFn;
private final EnumSet nativeEvents;
private final Map> objectMembers;
private final Map> otherMembers;
private final EventHandler fallbackObjectMember;
public UnionDeserializer(
List> objectMembers,
Map> otherMembers,
BiFunction buildFn
) {
this.buildFn = buildFn;
// Build a map of (field name -> member) for all fields to speed up lookup
if (objectMembers.isEmpty()) {
this.objectMembers = Collections.emptyMap();
} else {
this.objectMembers = new HashMap<>();
for (SingleMemberHandler member: objectMembers) {
for (String field: member.fields) {
this.objectMembers.put(field, member);
}
}
}
this.otherMembers = otherMembers;
this.nativeEvents = EnumSet.noneOf(Event.class);
for (EventHandler member: otherMembers.values()) {
this.nativeEvents.addAll(member.nativeEvents());
}
if (objectMembers.isEmpty()) {
fallbackObjectMember = null;
} else {
fallbackObjectMember = this.otherMembers.remove(Event.START_OBJECT);
this.nativeEvents.add(Event.START_OBJECT);
}
}
@Override
public EnumSet nativeEvents() {
return nativeEvents;
}
@Override
public EnumSet acceptedEvents() {
// In a union we want the real thing
return nativeEvents;
}
@Override
public Union deserialize(JsonParser parser, JsonpMapper mapper) {
Event event = parser.next();
JsonpUtils.ensureAccepts(this, parser, event);
return deserialize(parser, mapper, event);
}
@Override
public Union deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
EventHandler member = otherMembers.get(event);
if (member == null && event == Event.START_OBJECT && !objectMembers.isEmpty()) {
// Parse as an object to find matching field names
JsonObject object = parser.getObject();
for (String field: object.keySet()) {
member = objectMembers.get(field);
if (member != null) {
break;
}
}
if (member == null) {
member = fallbackObjectMember;
}
if (member != null) {
// Traverse the object we have inspected
parser = JsonpUtils.objectParser(object, mapper);
event = parser.next();
}
}
if (member == null) {
throw new JsonpMappingException("Cannot determine what union member to deserialize", parser.getLocation());
}
return member.deserialize(parser, mapper, event, buildFn);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy