Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default JSONB (de-)serializers #466

Merged
merged 4 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
* The `JsonSuperType` annotation helps to identify a type that should be deserialized instead of a parent type.
* The `type` property specifies the parent type. This annotation needs to go along with a `JsonDeserialize` pointing
* to the own type.
* If the super type that should get replaced is not an interface, than `override` needs to be set to `true`.
* If the super type that should get replaced is already used by another parent, and it should be overridden by another
* type, then `override` needs to be set to `true`.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.vladmihalcea.hibernate.type.util.ObjectMapperSupplier;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import lombok.extern.log4j.Log4j2;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import org.reflections.Reflections;
Expand All @@ -38,6 +39,7 @@
import java.util.HashMap;
import java.util.Map;

@Log4j2
@Configuration
public class JacksonConfig implements ObjectMapperSupplier {

Expand Down Expand Up @@ -105,23 +107,52 @@ private Map<Class<?>, Class<?>> findAnnotatedClasses() {
var annotation = cl.getAnnotation(JsonSuperType.class);
var superType = annotation.type();

if (!annotation.override() && !superType.isInterface()) {
throw new IllegalStateException("The super type " + superType.getName() + " is not an interface. " +
"Set override to true if this is intended.");
if (!implementers.containsKey(superType)) {
implementers.put(superType, cl);
log.debug("(De-)serializing supertype " + superType + " with type " + cl);
continue;
}

if (!implementers.containsKey(superType)) {
var previous = implementers.get(superType);
if (previous.isAssignableFrom(cl)) {
implementers.put(superType, cl);
} else {
var previous = implementers.get(superType);
if (previous.isAssignableFrom(cl)) {
implementers.put(superType, cl);
} else if (!cl.isAssignableFrom(previous)) {
throw new IllegalStateException("Found 2 incompatible types that both want to deserialize to the type "
+ superType.getName() + ". Any existing type should get extended.");
}
log.debug("(De-)serializing supertype " + superType + " with type " + cl);
continue;
}

var currentOverride = annotation.override();
var previousOverride = previous.getAnnotation(JsonSuperType.class).override();

log.warn("Found two conflicting (de-)serialization candidates (" + cl.getName() + ", " +
previous.getName() + ") for supertype " + superType + ". Checking for a conflict resolution " +
"(via the override field)");

if (currentOverride && previousOverride) {
throw new IllegalStateException("Found two types (" + cl.getName() + ", " + previous.getName() + ") " +
"that both want to (de-)serialize to the type " + superType.getName() + " and both have set " +
"override to true. Override must be set for a single type only.");
}

if (!currentOverride && !previousOverride) {
throw new IllegalStateException("Found two types (" + cl.getName() + ", " + previous.getName() + ") " +
"that both want to (de-)serialize to the type " + superType.getName() + ". Any existing type " +
"should get extended.");
}

if (previousOverride) {
log.info("Existing type (" + previous.getName() + ") for (de-)serialization of " + superType.getName() +
" will be used as override is set to true");
continue;
}

if (annotation.override()) {
implementers.remove(previous);
implementers.put(superType, cl);
log.info("Removing the existing type for (de-)serialization of " + superType.getName() + " (" +
previous.getName() + ") in favour of " + cl.getName() + " as override is set to true");
}
}

return implementers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,34 @@
*/
package de.terrestris.shogun.lib.config;

import com.fasterxml.classmate.TypeResolver;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.scanners.Scanners;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.AlternateTypeRules;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.Collections;
import java.util.*;
import java.util.function.Predicate;

@Configuration
@EnableAutoConfiguration
public abstract class SwaggerConfig {

@Autowired
private TypeResolver typeResolver;

protected String title = "SHOGun REST API";
protected String description = "This is the REST API description of SHOGun";
protected String version = "1.0.0";
Expand Down Expand Up @@ -81,18 +86,69 @@ private SecurityScheme basicAuthScheme() {
protected void directModelSubsitutions(Docket docket) {
var reflections = new Reflections(new ConfigurationBuilder()
.setUrls(ClasspathHelper.forJavaClassPath())
.setScanners(new SubTypesScanner(), new TypeAnnotationsScanner()));
.setScanners(
Scanners.SubTypes,
Scanners.TypesAnnotated
)
);

Map<Class<?>, Class<?>> substitutions = new HashMap<>();

for (var cl : reflections.getTypesAnnotatedWith(JsonSuperType.class)) {
var annotation = cl.getAnnotation(JsonSuperType.class);
var superType = annotation.type();

if (!annotation.override() && !superType.isInterface()) {
throw new IllegalStateException("The super type " + superType.getName() + " is not an interface. " +
"Set override to true if this is intended.");
if (!substitutions.containsKey(superType)) {
substitutions.put(superType, cl);
continue;
}

var previous = substitutions.get(superType);

var currentOverride = annotation.override();
var previousOverride = previous.getAnnotation(JsonSuperType.class).override();

if (currentOverride && previousOverride) {
throw new IllegalStateException("Found two types (" + cl.getName() + ", " + previous.getName() + ") " +
"that both want to (de-)serialize to the type " + superType.getName() + " and both have set " +
"override to true. Override must be set for a single type only.");
}

if (!currentOverride && !previousOverride) {
throw new IllegalStateException("Found two types (" + cl.getName() + ", " + previous.getName() + ") " +
"that both want to (de-)serialize to the type " + superType.getName() + ". Any existing type " +
"should get extended.");
}

if (previousOverride) {
continue;
}

if (annotation.override()) {
substitutions.remove(previous);
substitutions.put(superType, cl);
}
}

for (var entry : substitutions.entrySet()) {
Class<?> superType = entry.getKey();
Class<?> cl = entry.getValue();

docket.directModelSubstitute(superType, cl);
docket.alternateTypeRules(
AlternateTypeRules.newRule(
typeResolver.resolve(List.class, superType),
typeResolver.resolve(List.class, cl)
),
AlternateTypeRules.newRule(
typeResolver.resolve(Set.class, superType),
typeResolver.resolve(Set.class, cl)
),
AlternateTypeRules.newRule(
typeResolver.resolve(Collection.class, superType),
typeResolver.resolve(Collection.class, cl)
)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public class Layer extends BaseEntity {
@ToString.Exclude
@Schema(
description = "The configuration of the layer which should be used to define client specific aspects of " +
"the layer. This may include the name, the visible resolution range, search configurations or similiar."
"the layer. This may include the name, the visible resolution range, search configurations or similar."
)
private LayerClientConfig clientConfig;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.terrestris.shogun.lib.model.jsonb.application;
dnlkoch marked this conversation as resolved.
Show resolved Hide resolved

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import de.terrestris.shogun.lib.model.jsonb.ApplicationClientConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Data
@JsonDeserialize(as = DefaultApplicationClientConfig.class)
@JsonSuperType(type = ApplicationClientConfig.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ToString
@EqualsAndHashCode
public class DefaultApplicationClientConfig implements ApplicationClientConfig {
@Schema(
description = "The configuration of the map view.",
required = true
)
private DefaultMapView mapView;

@Schema(
description = "The description of the application."
)
private String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.terrestris.shogun.lib.model.jsonb.application;
dnlkoch marked this conversation as resolved.
Show resolved Hide resolved

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import de.terrestris.shogun.lib.model.jsonb.LayerConfig;
import de.terrestris.shogun.lib.model.jsonb.layer.DefaultLayerClientConfig;
import de.terrestris.shogun.lib.model.jsonb.layer.DefaultLayerSourceConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Data
@JsonDeserialize(as = DefaultApplicationLayerConfig.class)
@JsonSuperType(type = LayerConfig.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ToString
@EqualsAndHashCode
public class DefaultApplicationLayerConfig implements LayerConfig {
@Schema(
description = "The configuration of the layer which should be used to define client specific aspects of " +
"the layer. This may include the name, the visible resolution range, search configurations or similar."
)
private DefaultLayerClientConfig clientConfig;

@Schema(
description = "The configuration of the datasource of the layer, e.g. the URL of the server, the name or " +
"the grid configuration."
)
private DefaultLayerSourceConfig sourceConfig;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.terrestris.shogun.lib.model.jsonb.application;
dnlkoch marked this conversation as resolved.
Show resolved Hide resolved

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import de.terrestris.shogun.lib.model.jsonb.ApplicationToolConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import java.util.HashMap;

@Data
@JsonDeserialize(as = DefaultApplicationToolConfig.class)
@JsonSuperType(type = ApplicationToolConfig.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ToString
@EqualsAndHashCode
public class DefaultApplicationToolConfig implements ApplicationToolConfig {
@Schema(
description = "The name of the tool.",
example = "map-tool"
)
private String name;

@Schema(
description = "The configuration object of the tool.",
example = "{\"visible\": true}"
)
private HashMap<String, Object> config;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package de.terrestris.shogun.lib.model.jsonb.application;
dnlkoch marked this conversation as resolved.
Show resolved Hide resolved

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.terrestris.shogun.lib.annotation.JsonSuperType;
import de.terrestris.shogun.lib.model.jsonb.LayerTree;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import java.util.ArrayList;

@Data
@JsonDeserialize(as = DefaultLayerTree.class)
@JsonSuperType(type = LayerTree.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ToString
@EqualsAndHashCode
public class DefaultLayerTree implements LayerTree {
@Schema(
description = "The title of the node to show.",
example = "Layer A"
)
private String title;

@Schema(
description = "Whether the node should be checked initially or not.",
example = "true"
)
private Boolean checked;

@Schema(
description = "The ID of the layer to associate to the node.",
example = "1909"
)
private Integer layerId;

@Schema(
description = "The children configuration",
example = "[]"
)
private ArrayList<DefaultLayerTree> children;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.terrestris.shogun.lib.model.jsonb.application;
dnlkoch marked this conversation as resolved.
Show resolved Hide resolved

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import java.io.Serializable;
import java.util.ArrayList;

@Data
@ToString
@EqualsAndHashCode
public class DefaultMapView implements Serializable {
@Schema(
description = "The initial zoom level of the map.",
example = "1"
)
private Integer zoom;

@Schema(
description = "The initial center of the map (in WGS84).",
example = "[10.3, 51.08]"
)
private ArrayList<Double> center;

@Schema(
description = "The maximum extent of the map (in WGS84).",
example = "[2.5683045738288137, 45.429089001638076, 19.382621082401887, 57.283993958205926]"
)
private ArrayList<Double> extent;

@Schema(
description = "The projection of the map.",
example = "EPSG:25832"
)
private String projection;

@Schema(
description = "The list of resolutions/zoom levels of the map.",
example = "[2445.9849047851562, 1222.9924523925781, 611.4962261962891, 305.74811309814453, 152.87405654907226, 76.43702827453613, 38.218514137268066, 19.109257068634033, 9.554628534317017, 4.777314267158508, 2.388657133579254, 1.194328566789627, 0.5971642833948135, 0.298582142, 0.149291071, 0.074645535]"
)
private ArrayList<Double> resolutions;
}
Loading