diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/migrations/V20191203120602_MigrateSavedSearchesToViewsSupport/savedsearch/Query.java b/graylog2-server/src/main/java/org/graylog/plugins/views/migrations/V20191203120602_MigrateSavedSearchesToViewsSupport/savedsearch/Query.java index ba67cc9e6590..580c34039f33 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/migrations/V20191203120602_MigrateSavedSearchesToViewsSupport/savedsearch/Query.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/migrations/V20191203120602_MigrateSavedSearchesToViewsSupport/savedsearch/Query.java @@ -16,6 +16,7 @@ */ package org.graylog.plugins.views.migrations.V20191203120602_MigrateSavedSearchesToViewsSupport.savedsearch; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.Splitter; @@ -38,6 +39,7 @@ public abstract class Query { private final String TIMESTAMP_FIELD = "timestamp"; private final List DEFAULT_FIELDS = ImmutableList.of(TIMESTAMP_FIELD, "source", "message"); + @JsonProperty("rangeType") abstract String rangeType(); abstract Optional fields(); public abstract String query(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/export/ExportJob.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/export/ExportJob.java index 81965615bf3e..4f6a9f429ca1 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/export/ExportJob.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/export/ExportJob.java @@ -39,6 +39,9 @@ public interface ExportJob { @JsonProperty(FIELD_ID) String id(); - @JsonProperty + @JsonProperty(FIELD_TYPE) + String type(); + + @JsonProperty(FIELD_CREATED_AT) DateTime createdAt(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java index 8ece3cf871ee..5cd5eedd4b74 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java @@ -19,8 +19,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; @@ -30,15 +28,12 @@ import org.graylog.plugins.views.search.rest.SearchTypeExecutionState; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; -import org.graylog.plugins.views.search.timeranges.OffsetRange; import org.graylog2.contentpacks.EntityDescriptorIds; import org.graylog2.contentpacks.model.entities.MessageListEntity; import org.graylog2.contentpacks.model.entities.SearchTypeEntity; import org.graylog2.decorators.Decorator; import org.graylog2.decorators.DecoratorImpl; import org.graylog2.plugin.indexer.searches.timeranges.AbsoluteRange; -import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; -import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import org.graylog2.rest.models.messages.responses.DecorationStats; import org.graylog2.rest.models.messages.responses.ResultMessageSummary; @@ -169,13 +164,6 @@ public static Builder createDefault() { public abstract Builder fields(List fields); @JsonProperty - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = false) - @JsonSubTypes({ - @JsonSubTypes.Type(name = AbsoluteRange.ABSOLUTE, value = AbsoluteRange.class), - @JsonSubTypes.Type(name = RelativeRange.RELATIVE, value = RelativeRange.class), - @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class), - @JsonSubTypes.Type(name = OffsetRange.OFFSET, value = OffsetRange.class) - }) public Builder timerange(@Nullable TimeRange timerange) { return timerange(timerange == null ? null : DerivedTimeRange.of(timerange)); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java index c1e87de7d2ae..11c880d3b300 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java @@ -17,10 +17,7 @@ package org.graylog.plugins.views.search.searchtypes.pivot; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; @@ -29,22 +26,16 @@ import org.graylog.plugins.views.search.engine.BackendQuery; import org.graylog.plugins.views.search.rest.SearchTypeExecutionState; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; -import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; -import org.graylog.plugins.views.search.timeranges.OffsetRange; import org.graylog2.contentpacks.EntityDescriptorIds; import org.graylog2.contentpacks.model.entities.PivotEntity; import org.graylog2.contentpacks.model.entities.SearchTypeEntity; -import org.graylog2.plugin.indexer.searches.timeranges.AbsoluteRange; -import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; -import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import javax.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.OptionalInt; import java.util.Set; import java.util.UUID; @@ -182,13 +173,6 @@ public Builder sort(SortSpec... sort) { public abstract Builder filters(List filters); @JsonProperty - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = false) - @JsonSubTypes({ - @JsonSubTypes.Type(name = AbsoluteRange.ABSOLUTE, value = AbsoluteRange.class), - @JsonSubTypes.Type(name = RelativeRange.RELATIVE, value = RelativeRange.class), - @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class), - @JsonSubTypes.Type(name = OffsetRange.OFFSET, value = OffsetRange.class) - }) public Builder timerange(@Nullable TimeRange timerange) { return timerange(timerange == null ? null : DerivedTimeRange.of(timerange)); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpec.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpec.java index 54e00ea35c8a..f6956932ad4d 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpec.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpec.java @@ -27,7 +27,7 @@ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, property = SortSpec.TYPE_FIELD, visible = true) public interface SortSpec { diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/views/formatting/highlighting/HighlightingColor.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/views/formatting/highlighting/HighlightingColor.java index 2e37b1dfcef4..57cc35dec6db 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/views/formatting/highlighting/HighlightingColor.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/views/formatting/highlighting/HighlightingColor.java @@ -16,6 +16,7 @@ */ package org.graylog.plugins.views.search.views.formatting.highlighting; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -25,5 +26,6 @@ }) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") public interface HighlightingColor { + @JsonProperty("type") String type(); } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/MessageListEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/MessageListEntity.java index 94804cf7123d..29bd16666df2 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/MessageListEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/MessageListEntity.java @@ -18,8 +18,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; @@ -30,13 +28,9 @@ import org.graylog.plugins.views.search.searchtypes.MessageList; import org.graylog.plugins.views.search.searchtypes.Sort; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; -import org.graylog.plugins.views.search.timeranges.OffsetRange; import org.graylog2.contentpacks.model.entities.references.ValueReference; import org.graylog2.decorators.Decorator; import org.graylog2.decorators.DecoratorImpl; -import org.graylog2.plugin.indexer.searches.timeranges.AbsoluteRange; -import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; -import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import javax.annotation.Nullable; @@ -128,13 +122,6 @@ public static Builder createDefault() { public abstract Builder filters(List filters); @JsonProperty - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = false) - @JsonSubTypes({ - @JsonSubTypes.Type(name = AbsoluteRange.ABSOLUTE, value = AbsoluteRange.class), - @JsonSubTypes.Type(name = RelativeRange.RELATIVE, value = RelativeRange.class), - @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class), - @JsonSubTypes.Type(name = OffsetRange.OFFSET, value = OffsetRange.class) - }) public Builder timerange(@Nullable TimeRange timerange) { return timerange(timerange == null ? null : DerivedTimeRange.of(timerange)); } diff --git a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/PivotEntity.java b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/PivotEntity.java index ad7a2bd82b0b..1de38e830314 100644 --- a/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/PivotEntity.java +++ b/graylog2-server/src/main/java/org/graylog2/contentpacks/model/entities/PivotEntity.java @@ -18,8 +18,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; @@ -33,11 +31,7 @@ import org.graylog.plugins.views.search.searchtypes.pivot.SortSpec; import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; -import org.graylog.plugins.views.search.timeranges.OffsetRange; import org.graylog2.contentpacks.model.entities.references.ValueReference; -import org.graylog2.plugin.indexer.searches.timeranges.AbsoluteRange; -import org.graylog2.plugin.indexer.searches.timeranges.KeywordRange; -import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; import javax.annotation.Nullable; @@ -160,13 +154,6 @@ public static Builder createDefault() { public abstract Builder filters(List filters); @JsonProperty - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = false) - @JsonSubTypes({ - @JsonSubTypes.Type(name = AbsoluteRange.ABSOLUTE, value = AbsoluteRange.class), - @JsonSubTypes.Type(name = RelativeRange.RELATIVE, value = RelativeRange.class), - @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class), - @JsonSubTypes.Type(name = OffsetRange.OFFSET, value = OffsetRange.class) - }) public Builder timerange(@Nullable TimeRange timerange) { return timerange(timerange == null ? null : DerivedTimeRange.of(timerange)); } diff --git a/graylog2-server/src/main/java/org/graylog2/jackson/JacksonModelValidator.java b/graylog2-server/src/main/java/org/graylog2/jackson/JacksonModelValidator.java new file mode 100644 index 000000000000..328e8bb44ad8 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog2/jackson/JacksonModelValidator.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.jackson; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.graylog2.shared.utilities.StringUtils.f; + +/** + * Validates model or DTO objects for correct annotation usage. It mainly checks if the configured annotations + * for subtypes use the correct settings. + */ +public class JacksonModelValidator { + private static final Logger LOG = LoggerFactory.getLogger(JacksonModelValidator.class); + + /** + * Validates a class by inspecting its Jackson specific annotations. Can be used to validate classes on + * application startup. The bean serializer modifier only runs when the first object serialization happens. + * + * @param collectionName the collection name that the given class is stored in + * @param objectMapper the Jackson object mapper + * @param clazz the class that should be validated + */ + public static void check(String collectionName, ObjectMapper objectMapper, Class clazz) { + if (LOG.isDebugEnabled()) { + LOG.debug("CHECK [{}] {}", collectionName, clazz.getCanonicalName()); + } + + final var config = objectMapper.getSerializationConfig(); + final var ai = config.getAnnotationIntrospector(); + final var beanDesc = config.introspect(objectMapper.constructType(clazz)); + + try { + objectMapper.getSerializerProviderInstance().findTypedValueSerializer(clazz, true, null); + } catch (JsonMappingException e) { + throw new UncheckedIOException(e); + } + + // AnnotationIntrospector#findSubtypes finds all subtypes going back up the parent class chain. That can lead + // to recursion, so we only try to find subtypes for classes that are annotated with JsonSubTypes. + if (beanDesc.getBeanClass().isAnnotationPresent(JsonSubTypes.class)) { + if (LOG.isDebugEnabled()) { + LOG.debug("ITERATE SUBTYPES [{}] {}", collectionName, clazz.getCanonicalName()); + } + final List subtypes = ai.findSubtypes(beanDesc.getClassInfo()); + if (subtypes != null) { + for (NamedType subtype : subtypes) { + if (LOG.isDebugEnabled()) { + LOG.debug("CHECK SUBTYPE [{}] {} -> {}", collectionName, clazz.getCanonicalName(), subtype.getType().getCanonicalName()); + } + check(collectionName, objectMapper, subtype.getType()); + } + } + } + } + + public static BeanSerializerModifier getBeanSerializerModifier() { + return new ModelValidationBeanSerializerModifier(); + } + + private static class ModelValidationBeanSerializerModifier extends BeanSerializerModifier { + @Override + public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { + final var annotatedClass = beanDesc.getClassInfo(); + + if (annotatedClass.hasAnnotation(JsonTypeInfo.class)) { + final var fieldNames = beanDesc.findProperties().stream() + .map(BeanPropertyDefinition::getName) + .collect(Collectors.toSet()); + + final var jsonTypeInfo = annotatedClass.getAnnotation(JsonTypeInfo.class); + switch (jsonTypeInfo.include()) { + case PROPERTY -> { + if (fieldNames.contains(jsonTypeInfo.property())) { + // When the property for the subtype conflicts with an existing field in the class, + // Jackson will generate the field twice. + // + // Example: {"type": "foo", "type": "foo"} + // + // This is not an issue if both fields have the same value, but it can become + // problematic when the values differ. Specifically, when using abstract classes + // (auto-value) for the JsonSubType.Type values, Jackson might generate two different + // values. + // + // Example: {"type": "AutoValue_Foo", "type": "foo"} + // + // (see below where we check for existing type name annotations and abstract classes) + throw new RuntimeException(f("JsonTypeInfo#property value conflicts with existing property: %s (class %s)", jsonTypeInfo.property(), annotatedClass.getName())); + } + if (jsonTypeInfo.use() == JsonTypeInfo.Id.NAME + && annotatedClass.hasAnnotation(JsonSubTypes.class) + && !annotatedClass.hasAnnotation(JsonTypeIdResolver.class)) { + // When using abstract classes that don't have a @JsonTypeName annotation as the value + // for @JsonSubTypes.Type annotations, Jackson cannot look up the "name" value for the + // subtype and will use the class name as a fallback. (e.g., {"type": "AutoValue_ClassName"}) + // This is not an issue when a custom @JsonTypeIdResolver is present on the superclass. + final var invalidClasses = Arrays.stream(annotatedClass.getAnnotation(JsonSubTypes.class).value()) + .map(JsonSubTypes.Type::value) + .map(config::constructType) + .filter(JavaType::isAbstract) + .map(JavaType::getRawClass) + .filter(clazz -> !clazz.isAnnotationPresent(JsonTypeName.class)) + .map(Class::getCanonicalName) + .toList(); + + if (!invalidClasses.isEmpty()) { + throw new RuntimeException(f("@JsonSubTypes.Type values that are abstract classes (e.g., auto-value) must have a @JsonTypeName annotation or a custom @JsonTypeIdResolver. Affected classes: %s", invalidClasses)); + } + } + } + case EXISTING_PROPERTY -> { + if (!fieldNames.contains(jsonTypeInfo.property())) { + // Jackson cannot deserialize values where the existing property is not present. + // This check helps to detect this on serialization already. + throw new RuntimeException(f("JsonTypeInfo#property value doesn't exist as property: %s (class %s)", jsonTypeInfo.property(), annotatedClass.getName())); + } + } + default -> { + // Nothing to do + } + } + } + return beanProperties; + } + } +} diff --git a/graylog2-server/src/main/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRange.java b/graylog2-server/src/main/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRange.java index 7a53d7d900f2..c7ed4483609a 100644 --- a/graylog2-server/src/main/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRange.java +++ b/graylog2-server/src/main/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRange.java @@ -20,13 +20,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.graylog.plugins.views.search.timeranges.OffsetRange; import org.joda.time.DateTime; -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = false) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(name = AbsoluteRange.ABSOLUTE, value = AbsoluteRange.class), @JsonSubTypes.Type(name = RelativeRange.RELATIVE, value = RelativeRange.class), - @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class) + @JsonSubTypes.Type(name = KeywordRange.KEYWORD, value = KeywordRange.class), + @JsonSubTypes.Type(name = OffsetRange.OFFSET, value = OffsetRange.class) }) public abstract class TimeRange { diff --git a/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupCacheConfiguration.java b/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupCacheConfiguration.java index a8d9f8c4d6c9..a7a20dd2b017 100644 --- a/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupCacheConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupCacheConfiguration.java @@ -16,17 +16,16 @@ */ package org.graylog2.plugin.lookup; -import com.google.common.collect.Multimap; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.google.common.collect.Multimap; import java.util.Optional; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, property = LookupCacheConfiguration.TYPE_FIELD, visible = true, defaultImpl = FallbackCacheConfig.class) diff --git a/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupDataAdapterConfiguration.java b/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupDataAdapterConfiguration.java index e713b31772f8..5697e1a739f1 100644 --- a/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupDataAdapterConfiguration.java +++ b/graylog2-server/src/main/java/org/graylog2/plugin/lookup/LookupDataAdapterConfiguration.java @@ -26,7 +26,7 @@ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, property = LookupDataAdapterConfiguration.TYPE_FIELD, visible = true, defaultImpl = FallbackAdapterConfig.class) diff --git a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/ObjectMapperProvider.java b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/ObjectMapperProvider.java index af9ac0672187..69cf1800421c 100644 --- a/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/ObjectMapperProvider.java +++ b/graylog2-server/src/main/java/org/graylog2/shared/bindings/providers/ObjectMapperProvider.java @@ -37,6 +37,9 @@ import com.google.common.cache.LoadingCache; import com.vdurmont.semver4j.Requirement; import com.vdurmont.semver4j.Semver; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; import org.graylog.grn.GRN; import org.graylog.grn.GRNDeserializer; import org.graylog.grn.GRNKeyDeserializer; @@ -45,6 +48,7 @@ import org.graylog2.jackson.AutoValueSubtypeResolver; import org.graylog2.jackson.DeserializationProblemHandlerModule; import org.graylog2.jackson.InputConfigurationBeanDeserializerModifier; +import org.graylog2.jackson.JacksonModelValidator; import org.graylog2.jackson.JodaDurationCompatSerializer; import org.graylog2.jackson.JodaTimePeriodKeyDeserializer; import org.graylog2.jackson.SemverDeserializer; @@ -68,11 +72,6 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; - -import jakarta.inject.Inject; -import jakarta.inject.Provider; -import jakarta.inject.Singleton; - import java.util.Collections; import java.util.Set; import java.util.UUID; @@ -159,6 +158,7 @@ public ObjectMapperProvider(@GraylogClassLoader final ClassLoader classLoader, .addDeserializer(GRN.class, new GRNDeserializer(grnRegistry)) .addDeserializer(EncryptedValue.class, new EncryptedValueDeserializer(encryptedValueService)) .setDeserializerModifier(inputConfigurationBeanDeserializerModifier) + .setSerializerModifier(JacksonModelValidator.getBeanSerializerModifier()) ); if (subtypes != null) { diff --git a/graylog2-server/src/test/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpecTest.java b/graylog2-server/src/test/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpecTest.java index cbc7fff09465..923c4086e31d 100644 --- a/graylog2-server/src/test/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpecTest.java +++ b/graylog2-server/src/test/java/org/graylog/plugins/views/search/searchtypes/pivot/SortSpecTest.java @@ -16,10 +16,13 @@ */ package org.graylog.plugins.views.search.searchtypes.pivot; +import com.fasterxml.jackson.databind.jsontype.NamedType; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import java.util.List; + +import static org.graylog.testing.jackson.JacksonSubtypesAssertions.assertThatDto; class SortSpecTest { @@ -30,8 +33,8 @@ void directionDeserialize() { Assertions.assertThat(SortSpec.Direction.deserialize(" ")).isNull(); Assertions.assertThatThrownBy(() -> SortSpec.Direction.deserialize("blah")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Failed to parse sort direction"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Failed to parse sort direction"); Assertions.assertThat(SortSpec.Direction.deserialize("asc")).isEqualTo(SortSpec.Direction.Ascending); Assertions.assertThat(SortSpec.Direction.deserialize("ascending")).isEqualTo(SortSpec.Direction.Ascending); @@ -41,4 +44,17 @@ void directionDeserialize() { Assertions.assertThat(SortSpec.Direction.deserialize("desc")).isEqualTo(SortSpec.Direction.Descending); Assertions.assertThat(SortSpec.Direction.deserialize("descending")).isEqualTo(SortSpec.Direction.Descending); } + + @Test + void subtypes() { + assertThatDto(PivotSort.create("field", SortSpec.Direction.Ascending)) + .withRegisteredSubtypes(List.of(new NamedType(PivotSort.class, PivotSort.Type))) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(SortSpec.class); + + assertThatDto(SeriesSort.create("field", SortSpec.Direction.Ascending)) + .withRegisteredSubtypes(List.of(new NamedType(SeriesSort.class, SeriesSort.Type))) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(SortSpec.class); + } } diff --git a/graylog2-server/src/test/java/org/graylog/testing/jackson/JacksonSubtypesAssertions.java b/graylog2-server/src/test/java/org/graylog/testing/jackson/JacksonSubtypesAssertions.java new file mode 100644 index 000000000000..058d8c015d48 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/testing/jackson/JacksonSubtypesAssertions.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.testing.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; + +import java.util.List; + +public class JacksonSubtypesAssertions extends AbstractAssert, T> { + + private ObjectMapper objectMapper; + private List subtypes; + + /** + * Create an assertion to check that Jackson subtype resolving is working as expected. + * + * @param actual The object to assert + */ + public static JacksonSubtypesAssertions assertThatDto(T actual) { + return new JacksonSubtypesAssertions<>(actual); + } + + protected JacksonSubtypesAssertions(T actual) { + super(actual, JacksonSubtypesAssertions.class); + this.objectMapper = new ObjectMapperProvider().get(); + this.subtypes = List.of(); + } + + public JacksonSubtypesAssertions withObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + objectMapper.registerSubtypes(subtypes.toArray(new NamedType[]{})); + return this; + } + + public JacksonSubtypesAssertions withRegisteredSubtypes(List subtypes) { + this.subtypes = subtypes; + objectMapper.registerSubtypes(subtypes.toArray(new NamedType[]{})); + return this; + } + + public JacksonSubtypesAssertions doesNotSerializeWithDuplicateFields() { + + final JsonNode jsonNode = objectMapper.valueToTree(actual); + + Assertions.assertThat(serializeToJson(jsonNode)) + .as("Serializing directly to JSON yields different results from serializing with an " + + "intermediate JsonNode step. This is an indicator for duplicate fields.") + .isEqualTo(serializeToJson(actual)); + + return this; + } + + + public JacksonSubtypesAssertions deserializesWhenGivenSupertype(Class superType) { + Object deserializedObject = null; + + try { + deserializedObject = objectMapper.readValue(serializeToJson(actual), superType); + } catch (Exception e) { + failWithMessage("Deserializing from JSON failed with:\n" + ExceptionUtils.getStackTrace(e)); + } + + Assertions.assertThat(deserializedObject).isInstanceOf(actual.getClass()); + + return this; + } + + private String serializeToJson(Object o) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); + } catch (JsonProcessingException e) { + failWithMessage("Serializing to JSON failed with:\n" + ExceptionUtils.getStackTrace(e)); + } + return null; + } + +} diff --git a/graylog2-server/src/test/java/org/graylog2/jackson/JacksonModelValidatorTest.java b/graylog2-server/src/test/java/org/graylog2/jackson/JacksonModelValidatorTest.java new file mode 100644 index 000000000000..8b4479c6fb25 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/jackson/JacksonModelValidatorTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.auto.value.AutoValue; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JacksonModelValidatorTest { + private final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new SimpleModule("Test") + .setSerializerModifier(JacksonModelValidator.getBeanSerializerModifier())); + + // Well configured subtypes don't throw an error. + @Nested + class WithExistingProperty { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(name = "employee", value = Employee.class), + @JsonSubTypes.Type(name = "customer", value = Customer.class), + }) + private interface Person { + @JsonProperty("type") + String type(); + } + + record Employee(@JsonProperty("type") String type, + @JsonProperty("employee_number") String employeeNumber) implements Person { + } + + record Customer(@JsonProperty("type") String type, + @JsonProperty("customer_number") String customerNumber) implements Person { + } + + @Test + void serialize() throws Exception { + assertThat(objectMapper.readValue(objectMapper.writeValueAsString(new Employee("employee", "emp-123")), Person.class)) + .isInstanceOf(Employee.class); + assertThat(objectMapper.readValue(objectMapper.writeValueAsString(new Customer("customer", "cus-123")), Person.class)) + .isInstanceOf(Customer.class); + } + + @Test + void check() { + assertThatNoException().isThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Employee.class); + }); + assertThatNoException().isThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Customer.class); + }); + } + } + + // JsonTypeInfo.As.EXISTING_PROPERTY but without the existing field. + @Nested + class WithExistingPropertyWithoutPropertyDefined { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(name = "employee", value = Employee.class), + @JsonSubTypes.Type(name = "customer", value = Customer.class), + }) + private interface Person { + } + + record Employee(@JsonProperty("employee_number") String employeeNumber) implements Person { + } + + record Customer(@JsonProperty("customer_number") String customerNumber) implements Person { + } + + @Test + void serialize() throws Exception { + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(new Employee("emp-123")), Person.class); + }).hasMessageContaining("doesn't exist as property: type"); + + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(new Customer("cus-123")), Person.class); + }).hasMessageContaining("doesn't exist as property: type"); + } + + @Test + void check() { + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Employee.class); + }).hasMessageContaining("doesn't exist as property: type"); + + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Customer.class); + }).hasMessageContaining("doesn't exist as property: type"); + } + } + + // JsonTypeInfo.As.Property and a conflicting field name in the objects. + @Nested + class WithPropertyAndConflictingField { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(name = "employee", value = Employee.class), + @JsonSubTypes.Type(name = "customer", value = Customer.class), + }) + private interface Person { + @JsonProperty("type") + String type(); + } + + record Employee(@JsonProperty("type") String type, + @JsonProperty("employee_number") String employeeNumber) implements Person { + } + + record Customer(@JsonProperty("type") String type, + @JsonProperty("customer_number") String customerNumber) implements Person { + } + + @Test + void serialize() { + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(new Employee("employee", "emp-123")), Person.class); + }).hasMessageContaining("conflicts with existing property: type"); + + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(new Customer("customer", "cus-123")), Person.class); + }).hasMessageContaining("conflicts with existing property: type"); + } + + @Test + void check() { + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Employee.class); + }).hasMessageContaining("conflicts with existing property: type"); + + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Customer.class); + }).hasMessageContaining("conflicts with existing property: type"); + } + } + + // JsonTypeInfo.As.Property and missing JsonTypeName annotations with abstract classes. + @Nested + class WithPropertyAndMissingTypeNames { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(name = "employee", value = Employee.class), + @JsonSubTypes.Type(name = "customer", value = Customer.class), + }) + private interface Person { + } + + @AutoValue + static abstract class Employee implements Person { + @JsonProperty("employee_number") + public abstract String employeeNumber(); + + @JsonCreator + public static Employee create(@JsonProperty("employee_number") String employeeNumber) { + return new AutoValue_JacksonModelValidatorTest_WithPropertyAndMissingTypeNames_Employee(employeeNumber); + } + } + + @AutoValue + static abstract class Customer implements Person { + @JsonProperty("customer_number") + public abstract String customerNumber(); + + @JsonCreator + public static Customer create(@JsonProperty("customer_number") String customerNumber) { + return new AutoValue_JacksonModelValidatorTest_WithPropertyAndMissingTypeNames_Customer(customerNumber); + } + } + + @Test + void serialize() { + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(Employee.create("emp-123")), Person.class); + }).hasMessageContaining("must have a @JsonTypeName annotation"); + + assertThatThrownBy(() -> { + objectMapper.readValue(objectMapper.writeValueAsString(Customer.create("cus-123")), Person.class); + }).hasMessageContaining("must have a @JsonTypeName annotation"); + } + + @Test + void check() { + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Employee.class); + }).hasMessageContaining("must have a @JsonTypeName annotation"); + + assertThatThrownBy(() -> { + JacksonModelValidator.check("test", objectMapper, Customer.class); + }).hasMessageContaining("must have a @JsonTypeName annotation"); + } + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRangeTest.java b/graylog2-server/src/test/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRangeTest.java new file mode 100644 index 000000000000..1be6124b1658 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/plugin/indexer/searches/timeranges/TimeRangeTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.plugin.indexer.searches.timeranges; + +import org.graylog.plugins.views.search.timeranges.OffsetRange; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.jupiter.api.Test; + +import static org.graylog.testing.jackson.JacksonSubtypesAssertions.assertThatDto; + +class TimeRangeTest { + @Test + void subtypes() { + final var now = DateTime.now(DateTimeZone.UTC); + + final var absoluteRange = AbsoluteRange.create(now, now); + final var relativeRange = RelativeRange.create(500); + final var keywordRange = KeywordRange.create("yesterday", "UTC"); + final var offsetRange = OffsetRange.Builder.builder() + .offset(1) + .source("foo") + .build(); + + assertThatDto(absoluteRange) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(TimeRange.class); + assertThatDto(relativeRange) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(TimeRange.class); + assertThatDto(keywordRange) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(TimeRange.class); + assertThatDto(offsetRange) + .doesNotSerializeWithDuplicateFields() + .deserializesWhenGivenSupertype(TimeRange.class); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupCacheConfigurationTest.java b/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupCacheConfigurationTest.java new file mode 100644 index 000000000000..90ffeadfdcfc --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupCacheConfigurationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.plugin.lookup; + +import com.fasterxml.jackson.databind.jsontype.NamedType; +import org.graylog2.lookup.caches.CaffeineLookupCache; +import org.graylog2.lookup.caches.NullCache; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; +import org.junit.jupiter.api.Test; + +import static org.graylog.testing.jackson.JacksonSubtypesAssertions.assertThatDto; + +class LookupCacheConfigurationTest { + @Test + void subtypes() { + + final var objectMapper = new ObjectMapperProvider().get(); + objectMapper.registerSubtypes( + new NamedType(CaffeineLookupCache.Config.class, CaffeineLookupCache.NAME), + new NamedType(NullCache.Config.class, NullCache.NAME) + ); + + final var caffeineCacheConfig = CaffeineLookupCache.Config.builder() + .type(CaffeineLookupCache.NAME) + .maxSize(1) + .expireAfterAccess(1) + .expireAfterWrite(1) + .build(); + + final var nullCacheConfig = NullCache.Config.builder() + .type(NullCache.NAME) + .build(); + + assertThatDto(caffeineCacheConfig) + .withObjectMapper(objectMapper) + .deserializesWhenGivenSupertype(LookupCacheConfiguration.class) + .doesNotSerializeWithDuplicateFields(); + + assertThatDto(nullCacheConfig) + .withObjectMapper(objectMapper) + .deserializesWhenGivenSupertype(LookupCacheConfiguration.class) + .doesNotSerializeWithDuplicateFields(); + } +} diff --git a/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupDataAdapterConfigurationTest.java b/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupDataAdapterConfigurationTest.java new file mode 100644 index 000000000000..6517f88dda40 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog2/plugin/lookup/LookupDataAdapterConfigurationTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog2.plugin.lookup; + +import com.fasterxml.jackson.databind.jsontype.NamedType; +import org.graylog.plugins.threatintel.whois.ip.WhoisDataAdapter; +import org.graylog2.lookup.adapters.HTTPJSONPathDataAdapter; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; +import org.junit.jupiter.api.Test; + +import static org.graylog.testing.jackson.JacksonSubtypesAssertions.assertThatDto; + +class LookupDataAdapterConfigurationTest { + @Test + void subtypes() { + final var objectMapper = new ObjectMapperProvider().get(); + objectMapper.registerSubtypes( + new NamedType(WhoisDataAdapter.Config.class, WhoisDataAdapter.NAME), + new NamedType(HTTPJSONPathDataAdapter.Config.class, HTTPJSONPathDataAdapter.NAME) + ); + + final var httpConfig = HTTPJSONPathDataAdapter.Config.builder() + .type(HTTPJSONPathDataAdapter.NAME) + .url("http://graylog.local") + .singleValueJSONPath(".") + .userAgent("test") + .build(); + + final var whoisConfig = WhoisDataAdapter.Config.builder() + .type(WhoisDataAdapter.NAME) + .connectTimeout(1) + .readTimeout(1) + .build(); + + assertThatDto(httpConfig) + .withObjectMapper(objectMapper) + .deserializesWhenGivenSupertype(LookupDataAdapterConfiguration.class) + .doesNotSerializeWithDuplicateFields(); + assertThatDto(whoisConfig) + .withObjectMapper(objectMapper) + .deserializesWhenGivenSupertype(LookupDataAdapterConfiguration.class) + .doesNotSerializeWithDuplicateFields(); + + } +}