/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.client.json; import org.opensearch.client.util.ObjectBuilder; import jakarta.json.JsonObject; import jakarta.json.stream.JsonLocation; import jakarta.json.stream.JsonParser; import jakarta.json.stream.JsonParser.Event; import jakarta.json.stream.JsonParsingException; 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; import java.util.function.Function; import java.util.stream.Collectors; 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 new JsonParsingException("Couldn't find a suitable union member deserializer", exception, parser.getLocation()); } } 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; UnionDeserializer.SingleMemberHandler member = new SingleMemberHandler<>(tag, deserializer, new HashSet<>(od.fieldNames())); 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() { Map fieldFrequencies = objectMembers.stream().flatMap(m -> m.fields.stream()) .collect( Collectors.groupingBy(Function.identity(), Collectors.counting())); Set duplicateFields = fieldFrequencies.entrySet().stream() .filter(entry -> entry.getValue() > 1) .map(Map.Entry::getKey) .collect(Collectors.toSet()); for (UnionDeserializer.SingleMemberHandler member: objectMembers) { member.fields.removeAll(duplicateFields); } // 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> nonObjectMembers; private final EventHandler fallbackObjectMember; public UnionDeserializer( List> objectMembers, Map> nonObjectMembers, 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.nonObjectMembers = nonObjectMembers; this.nativeEvents = EnumSet.noneOf(Event.class); for (EventHandler member: nonObjectMembers.values()) { this.nativeEvents.addAll(member.nativeEvents()); } if (objectMembers.isEmpty()) { fallbackObjectMember = null; } else { fallbackObjectMember = this.nonObjectMembers.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 = nonObjectMembers.get(event); JsonLocation location = parser.getLocation(); if (member == null && event == Event.START_OBJECT && !objectMembers.isEmpty()) { if (parser instanceof LookAheadJsonParser) { Map.Entry, JsonParser> memberAndParser = ((LookAheadJsonParser) parser).findVariant(objectMembers); member = memberAndParser.getKey(); // Parse the buffered parser parser = memberAndParser.getValue(); } else { // 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; } } // Traverse the object we have inspected parser = JsonpUtils.objectParser(object, mapper); } if (member == null) { member = fallbackObjectMember; } if (member != null) { event = parser.next(); } } if (member == null) { throw new JsonParsingException("Cannot determine what union member to deserialize", location); } return member.deserialize(parser, mapper, event, buildFn); } }