/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ package org.opensearch.dataprepper.model.configuration; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Supplier; /** * Model class for a Plugin in Configuration YAML containing name of the Plugin and its associated settings * * @since 1.2 */ @JsonSerialize(using = PluginModel.PluginModelSerializer.class) @JsonDeserialize(using = PluginModel.PluginModelDeserializer.class) public class PluginModel { private static final ObjectMapper SERIALIZER_OBJECT_MAPPER = new ObjectMapper(); private final String pluginName; private final InternalJsonModel innerModel; /** * This class represents the part of the {@link PluginModel} which sits below the name. * In the following example, this would be everything below "opensearch": *
     *     opensearch:
     *       hosts: ["http://localhost:9200"]
     *       username: admin
     *       password: admin
     * 
*

* Classes that inherit from {@link PluginModel} can create a sublcass of {@link InternalJsonModel} * with any custom values needed. By configuring Jackson databind (ie. Jackson annotations) correctly, * this internal model will serialize and deserialize correctly. */ static class InternalJsonModel { @JsonAnySetter @JsonAnyGetter private final Map pluginSettings; @JsonCreator InternalJsonModel() { this(new HashMap<>()); } InternalJsonModel(final Map pluginSettings) { this.pluginSettings = pluginSettings; } } public PluginModel(final String pluginName, final Map pluginSettings) { this(pluginName, new InternalJsonModel(pluginSettings)); } protected PluginModel(final String pluginName, final InternalJsonModel innerModel) { this.pluginName = pluginName; this.innerModel = Objects.requireNonNull(innerModel); } public String getPluginName() { return pluginName; } public Map getPluginSettings() { return innerModel.pluginSettings; } M getInternalJsonModel() { return (M) innerModel; } /** * Custom Serializer for Plugin Model *

* Sub-classes of {@link PluginModel} can use this class directly. * * @since 1.2 */ static class PluginModelSerializer extends StdSerializer { public PluginModelSerializer() { this(null); } public PluginModelSerializer(final Class valueClass) { super(valueClass); } @Override public void serialize( final PluginModel value, final JsonGenerator gen, final SerializerProvider provider) throws IOException { gen.writeStartObject(); Map serializedInner = SERIALIZER_OBJECT_MAPPER.convertValue(value.innerModel, Map.class); if(serializedInner != null && serializedInner.isEmpty()) serializedInner = null; gen.writeObjectField(value.getPluginName(), serializedInner); gen.writeEndObject(); } } /** * Custom Deserializer for Plugin Model. *

* This deserializer is only intended for {@link PluginModel}. Any * subclasses of {@link PluginModel} should see {@link AbstractPluginModelDeserializer}. * * @since 1.2 */ static final class PluginModelDeserializer extends AbstractPluginModelDeserializer { public PluginModelDeserializer() { super(PluginModel.class, InternalJsonModel.class, PluginModel::new, InternalJsonModel::new); } } /** * Abstract deserializer for {@link PluginModel} objects. Any classes which inherit from {@link PluginModel} * can extend this class. That class must also have a class derived from {@link InternalJsonModel}. It should be configured * for Jackson databind. Please note that this class is only intended for internal use. Subclasses of {@link PluginModel} * will need to create subclass of {@link AbstractPluginModelDeserializer}, but they do not need to override any methods. * The subclass should only need to configure the correct classes in the default constructor. * * @param The type inheriting from {@link PluginModel} that you ultimately need deserialized * @param The type inheriting from {@link InternalJsonModel} that has custom fields. * * @see SinkModel.SinkModelDeserializer */ abstract static class AbstractPluginModelDeserializer extends StdDeserializer { private final Class innerModelClass; private final BiFunction constructorFunction; private final Supplier emptyInnerModelConstructor; protected AbstractPluginModelDeserializer( final Class valueClass, final Class innerModelClass, final BiFunction constructorFunction, final Supplier emptyInnerModelConstructor) { super(valueClass); this.innerModelClass = innerModelClass; this.constructorFunction = constructorFunction; this.emptyInnerModelConstructor = emptyInnerModelConstructor; } @Override public PluginModel deserialize(final JsonParser jsonParser, final DeserializationContext context) throws IOException, JacksonException { final JsonNode node = jsonParser.getCodec().readTree(jsonParser); final Iterator> fields = node.fields(); final Map.Entry onlyField = fields.next(); final String pluginName = onlyField.getKey(); final JsonNode value = onlyField.getValue(); M innerModel = SERIALIZER_OBJECT_MAPPER.convertValue(value, innerModelClass); if(innerModel == null) innerModel = emptyInnerModelConstructor.get(); return constructorFunction.apply(pluginName, innerModel); } } }