All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.elastic.clients.json.UnionDeserializer Maven / Gradle / Ivy

There is a newer version: 8.17.0
Show newest version
/*
 * 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 deserializer;
        private final Kind tag;
        // ObjectDeserializers provide the list of fields they know about
        private final Set fields;

        SingleMemberHandler(Kind tag, JsonpDeserializer deserializer) {
            this(tag, deserializer, null);
        }

        SingleMemberHandler(Kind tag, JsonpDeserializer 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 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 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