diff --git a/src/docs/resources/org/springframework/hateoas/docs/mediatype/hal/forms/hal-forms-sample.json b/src/docs/resources/org/springframework/hateoas/docs/mediatype/hal/forms/hal-forms-sample.json index 1b149d60..cb5b7d9e 100644 --- a/src/docs/resources/org/springframework/hateoas/docs/mediatype/hal/forms/hal-forms-sample.json +++ b/src/docs/resources/org/springframework/hateoas/docs/mediatype/hal/forms/hal-forms-sample.json @@ -10,7 +10,6 @@ "_templates" : { "default" : { "method" : "put", - "contentType" : "", "properties" : [ { "name" : "firstName", "required" : true @@ -24,7 +23,6 @@ }, "partiallyUpdateEmployee" : { "method" : "patch", - "contentType" : "", "properties" : [ { "name" : "firstName", "required" : false diff --git a/src/main/java/org/springframework/hateoas/mediatype/ConfigurableHandlerInstantiator.java b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableHandlerInstantiator.java new file mode 100644 index 00000000..3874b35f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableHandlerInstantiator.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.hateoas.mediatype; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter; + +/** + * A {@link HandlerInstantiator} that will use instances explicitly registered with it but fall back to lookup or even + * create a fresh instance via the {@link AutowireCapableBeanFactory} provided on construction. + * + * @author Oliver Drotbohm + */ +@SuppressWarnings("null") +public class ConfigurableHandlerInstantiator extends HandlerInstantiator { + + private final Map, Object> instances = new HashMap<>(); + private final AutowireCapableBeanFactory beanFactory; + + /** + * Creates a new {@link ConfigurableHandlerInstantiator} for the given {@link AutowireCapableBeanFactory}. + * + * @param beanFactory must not be {@literal null}. + */ + protected ConfigurableHandlerInstantiator(AutowireCapableBeanFactory beanFactory) { + + Assert.notNull(beanFactory, "BeanFactory must not be null!"); + + this.beanFactory = beanFactory; + } + + protected void registerInstance(Object instance) { + this.instances.put(instance.getClass(), instance); + } + + @Nullable + @SuppressWarnings("unchecked") + protected T findInstance(Class type) { + return (T) this.instances.get(type); + } + + @SuppressWarnings("unchecked") + protected T findOrCreateInstance(Class type) { + + Object object = findInstance(type); + + return object != null + ? (T) object + : beanFactory.getBeanProvider(type) + .getIfAvailable(() -> beanFactory.createBean(type)); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class deserClass) { + return (JsonDeserializer) findOrCreateInstance(deserClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) findOrCreateInstance(keyDeserClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (JsonSerializer) findOrCreateInstance(serClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class builderClass) { + return (TypeResolverBuilder) findOrCreateInstance(builderClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return (TypeIdResolver) findOrCreateInstance(resolverClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#virtualPropertyWriterInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, java.lang.Class) + */ + @Override + public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig config, Class implClass) { + return (VirtualBeanPropertyWriter) findOrCreateInstance(implClass); + } +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/CollectionModelMixin.java b/src/main/java/org/springframework/hateoas/mediatype/hal/CollectionModelMixin.java index 8526d242..a6b3bf2c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/CollectionModelMixin.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/CollectionModelMixin.java @@ -34,7 +34,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; * @author Greg Turnquist */ @JsonPropertyOrder({ "content", "links" }) -abstract class CollectionModelMixin extends CollectionModel { +public abstract class CollectionModelMixin extends CollectionModel { @Override @JsonProperty("_embedded") diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index c3666552..029880fe 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -26,23 +26,24 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.Links; import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.mediatype.ConfigurableHandlerInstantiator; import org.springframework.hateoas.mediatype.MessageResolver; import org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks; import org.springframework.hateoas.server.LinkRelationProvider; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; @@ -51,14 +52,9 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase; -import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; -import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase; -import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; -import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; -import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.ContainerSerializer; @@ -711,32 +707,32 @@ public class Jackson2HalModule extends SimpleModule { * * @author Oliver Gierke */ - public static class HalHandlerInstantiator extends HandlerInstantiator { - - private final Map, Object> serializers = new HashMap<>(); - private final @Nullable AutowireCapableBeanFactory delegate; + public static class HalHandlerInstantiator extends ConfigurableHandlerInstantiator { + /** + * Convenience constructor for testing purposes. Prefer + * {@link #HalHandlerInstantiator(LinkRelationProvider, CurieProvider, MessageResolver, HalConfiguration, AutowireCapableBeanFactory)} + * + * @param provider must not be {@literal null}. + * @param curieProvider must not be {@literal null}. + * @param resolver must not be {@literal null}. + */ public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver) { - this(provider, curieProvider, resolver, new HalConfiguration()); + this(provider, curieProvider, resolver, new HalConfiguration(), new DefaultListableBeanFactory()); } /** - * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} - * and {@link MessageResolver}. Registers a prepared {@link HalResourcesSerializer} and - * {@link HalLinkListSerializer} falling back to instantiation expecting a default constructor. - * * @param provider must not be {@literal null}. - * @param curieProvider can be {@literal null}. + * @param curieProvider must not be {@literal null}. * @param resolver must not be {@literal null}. + * @param halConfiguration must not be {@literal null}. + * @param delegate must not be {@literal null}. */ public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver, - HalConfiguration halConfiguration) { - this(provider, curieProvider, resolver, halConfiguration, null); - } + HalConfiguration halConfiguration, AutowireCapableBeanFactory delegate) { - public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver, - HalConfiguration halConfiguration, @Nullable AutowireCapableBeanFactory delegate) { + super(delegate); Assert.notNull(provider, "RelProvider must not be null!"); Assert.notNull(curieProvider, "CurieProvider must not be null!"); @@ -744,73 +740,8 @@ public class Jackson2HalModule extends SimpleModule { EmbeddedMapper mapper = new EmbeddedMapper(provider, curieProvider, halConfiguration.isEnforceEmbeddedCollections()); - this.delegate = delegate; - - this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper, halConfiguration)); - this.serializers.put(HalLinkListSerializer.class, - new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration)); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - @SuppressWarnings("null") - public JsonDeserializer deserializerInstance(@NonNull DeserializationConfig config, @NonNull Annotated annotated, - @NonNull Class deserClass) { - return (JsonDeserializer) findInstance(deserClass); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - @SuppressWarnings("null") - public KeyDeserializer keyDeserializerInstance(@NonNull DeserializationConfig config, @NonNull Annotated annotated, - @NonNull Class keyDeserClass) { - return (KeyDeserializer) findInstance(keyDeserClass); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - @SuppressWarnings("null") - public JsonSerializer serializerInstance(@NonNull SerializationConfig config, @NonNull Annotated annotated, - @NonNull Class serClass) { - return (JsonSerializer) findInstance(serClass); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - @SuppressWarnings("null") - public TypeResolverBuilder typeResolverBuilderInstance(@NonNull MapperConfig config, - @NonNull Annotated annotated, @NonNull Class builderClass) { - return (TypeResolverBuilder) findInstance(builderClass); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - @SuppressWarnings("null") - public TypeIdResolver typeIdResolverInstance(@NonNull MapperConfig config, @NonNull Annotated annotated, - @NonNull Class resolverClass) { - return (TypeIdResolver) findInstance(resolverClass); - } - - private Object findInstance(Class type) { - - Object result = serializers.get(type); - - return result != null ? result : delegate != null ? delegate.createBean(type) : BeanUtils.instantiateClass(type); + registerInstance(new HalResourcesSerializer(mapper, halConfiguration)); + registerInstance(new HalLinkListSerializer(curieProvider, mapper, resolver, halConfiguration)); } } @@ -988,6 +919,7 @@ public class Jackson2HalModule extends SimpleModule { @Nullable @JsonInclude(Include.NON_EMPTY) + @JsonProperty public String getTitle() { return title; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java deleted file mode 100644 index 1ea48845..00000000 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright 2017-2021 the original author or authors. - * - * Licensed 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 - * - * https://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 org.springframework.hateoas.mediatype.hal.forms; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Links; -import org.springframework.hateoas.PagedModel; -import org.springframework.hateoas.PagedModel.PageMetadata; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.mediatype.PropertyUtils; -import org.springframework.hateoas.mediatype.hal.HalLinkRelation; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; -import org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsModule.HalFormsLinksDeserializer; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -import com.fasterxml.jackson.annotation.JsonAnyGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -/** - * Representation of a HAL-FORMS document. - * - * @author Dietrich Schulten - * @author Greg Turnquist - * @author Oliver Gierke - */ -@JsonPropertyOrder({ "attributes", "entity", "entities", "embedded", "links", "templates", "metadata" }) -final class HalFormsDocument { - - @Nullable // - @JsonInclude(Include.NON_EMPTY) // - private final Map attributes; - - @Nullable // - @JsonUnwrapped // - @JsonInclude(Include.NON_NULL) // - private final T entity; - - @Nullable // - @JsonInclude(Include.NON_EMPTY) // - @JsonIgnore // - private final Collection entities; - - @JsonProperty("_embedded") // - @JsonInclude(Include.NON_EMPTY) // - private final Map embedded; - - @Nullable // - @JsonProperty("page") // - @JsonInclude(Include.NON_NULL) // - private final PagedModel.PageMetadata pageMetadata; - - @JsonProperty("_links") // - @JsonInclude(Include.NON_EMPTY) // - @JsonSerialize(using = HalLinkListSerializer.class) // - @JsonDeserialize(using = HalFormsLinksDeserializer.class) // - private final Links links; - - @JsonProperty("_templates") // - @JsonInclude(Include.NON_EMPTY) // - private final Map templates; - - HalFormsDocument(Map attributes, T entity, Collection entities, - Map embedded, PageMetadata pageMetadata, Links links, - Map templates) { - - this.attributes = attributes; - this.entity = entity; - this.entities = entities; - this.embedded = embedded; - this.pageMetadata = pageMetadata; - this.links = links; - this.templates = templates; - } - - private HalFormsDocument() { - this(null, null, null, Collections.emptyMap(), null, Links.NONE, Collections.emptyMap()); - } - - /** - * Creates a new {@link HalFormsDocument} for the given resource support. - * - * @param model can be {@literal null} - * @return - */ - static HalFormsDocument forRepresentationModel(RepresentationModel model) { - - Map attributes = PropertyUtils.extractPropertyValues(model); - attributes.remove("links"); - - return new HalFormsDocument<>().withAttributes(attributes); - } - - /** - * Creates a new {@link HalFormsDocument} for the given resource. - * - * @param resource can be {@literal null}. - * @return - */ - static HalFormsDocument forEntity(@Nullable T resource) { - return new HalFormsDocument().withEntity(resource); - } - - /** - * returns a new {@link HalFormsDocument} for the given resources. - * - * @param entities must not be {@literal null}. - * @return - */ - static HalFormsDocument forEntities(Collection entities) { - - Assert.notNull(entities, "Resources must not be null!"); - - return new HalFormsDocument().withEntities(entities); - } - - /** - * Creates a new empty {@link HalFormsDocument}. - * - * @return - */ - static HalFormsDocument empty() { - return new HalFormsDocument<>(); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal attributes}. - * - * @param attributes - * @return - */ - private HalFormsDocument withAttributes(@Nullable Map attributes) { - - return this.attributes == attributes ? this - : new HalFormsDocument(attributes, this.entity, this.entities, this.embedded, this.pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing {@literal entity}. - * - * @param entity - * @return - */ - private HalFormsDocument withEntity(@Nullable T entity) { - - return this.entity == entity ? this - : new HalFormsDocument(this.attributes, entity, this.entities, this.embedded, this.pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal entities}. - * - * @param entities - * @return - */ - private HalFormsDocument withEntities(@Nullable Collection entities) { - - return this.entities == entities ? this - : new HalFormsDocument(this.attributes, this.entity, entities, this.embedded, this.pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying the attributes and adding a new embedded value. - * - * @param key must not be {@literal null} or empty. - * @param value must not be {@literal null}. - * @return - */ - HalFormsDocument andEmbedded(HalLinkRelation key, Object value) { - - Assert.notNull(key, "Embedded key must not be null!"); - Assert.notNull(value, "Embedded value must not be null!"); - - Map embedded = new HashMap<>(this.embedded); - embedded.put(key, value); - - return new HalFormsDocument<>(this.attributes, this.entity, this.entities, embedded, this.pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing all {@literal embedded}s. - * - * @param embedded - * @return - */ - HalFormsDocument withEmbedded(Map embedded) { - - return this.embedded == embedded ? this - : new HalFormsDocument(this.attributes, this.entity, this.entities, embedded, this.pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal pageMetadata}. - * - * @param pageMetadata - * @return - */ - HalFormsDocument withPageMetadata(@Nullable PageMetadata pageMetadata) { - - return this.pageMetadata == pageMetadata ? this - : new HalFormsDocument(this.attributes, this.entity, this.entities, this.embedded, pageMetadata, this.links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying the attributes and adding a new {@link Link}. - * - * @param link must not be {@literal null}. - * @return - */ - HalFormsDocument andLink(Link link) { - - Assert.notNull(link, "Link must not be null!"); - - return new HalFormsDocument<>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, - this.links.and(link), this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal links}. - * - * @param links - * @return - */ - HalFormsDocument withLinks(Links links) { - - return this.links == links ? this - : new HalFormsDocument(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, links, - this.templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying the attributes and adding a new {@link HalFormsTemplate}. - * - * @param name must not be {@literal null} or empty. - * @param template must not be {@literal null}. - * @return - */ - HalFormsDocument andTemplate(String name, HalFormsTemplate template) { - - Assert.hasText(name, "Template name must not be null or empty!"); - Assert.notNull(template, "Template must not be null!"); - - Map templates = new HashMap<>(this.templates); - templates.put(name, template); - - return new HalFormsDocument<>(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, - this.links, templates); - } - - /** - * Create a new {@link HalFormsDocument} by copying attributes and replacing the {@literal templates}. - * - * @param templates - * @return - */ - HalFormsDocument withTemplates(Map templates) { - - return this.templates == templates ? this - : new HalFormsDocument(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, - this.links, templates); - } - - @Nullable - @JsonAnyGetter - Map getAttributes() { - return this.attributes; - } - - @Nullable - T getEntity() { - return this.entity; - } - - @Nullable - Collection getEntities() { - return this.entities; - } - - Map getEmbedded() { - return this.embedded; - } - - @Nullable - PageMetadata getPageMetadata() { - return this.pageMetadata; - } - - Links getLinks() { - return this.links; - } - - Map getTemplates() { - return this.templates; - } - - /** - * Returns the template with the given name. - * - * @param key must not be {@literal null}. - * @return - */ - @JsonIgnore - HalFormsTemplate getTemplate(String key) { - - Assert.notNull(key, "Template key must not be null!"); - - return this.templates.get(key); - } - - /** - * Returns the default template of the document. - * - * @return - */ - @JsonIgnore - HalFormsTemplate getDefaultTemplate() { - return getTemplate(HalFormsTemplate.DEFAULT_KEY); - } - - @Override - public boolean equals(Object o) { - - if (this == o) - return true; - if (!(o instanceof HalFormsDocument)) - return false; - HalFormsDocument that = (HalFormsDocument) o; - return Objects.equals(this.attributes, that.attributes) && Objects.equals(this.entity, that.entity) - && Objects.equals(this.entities, that.entities) && Objects.equals(this.embedded, that.embedded) - && Objects.equals(this.pageMetadata, that.pageMetadata) && Objects.equals(this.links, that.links) - && Objects.equals(this.templates, that.templates); - } - - @Override - public int hashCode() { - - return Objects.hash(this.attributes, this.entity, this.entities, this.embedded, this.pageMetadata, this.links, - this.templates); - } - - public String toString() { - - return "HalFormsDocument(attributes=" + this.attributes + ", entity=" + this.entity + ", entities=" + this.entities - + ", embedded=" + this.embedded + ", pageMetadata=" + this.pageMetadata + ", links=" + this.links - + ", templates=" + this.templates + ")"; - } -} diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java index 1d1d47a9..4dd73f13 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.hateoas.mediatype.hal.forms; import java.util.List; +import java.util.function.Supplier; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; @@ -27,6 +28,7 @@ import org.springframework.hateoas.config.HypermediaMappingInformation; import org.springframework.hateoas.mediatype.MessageResolver; import org.springframework.hateoas.mediatype.hal.CurieProvider; import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.hateoas.mediatype.hal.Jackson2HalModule; import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider; import org.springframework.http.MediaType; @@ -39,7 +41,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; * @author Greg Turnquist * @author Oliver Drotbohm */ -@Configuration +@Configuration(proxyBeanMethods = false) class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { private final DelegatingLinkRelationProvider relProvider; @@ -49,6 +51,8 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { private final MessageResolver resolver; private final AbstractAutowireCapableBeanFactory beanFactory; + private HalFormsConfiguration resolvedConfiguration; + public HalFormsMediaTypeConfiguration(DelegatingLinkRelationProvider relProvider, ObjectProvider curieProvider, ObjectProvider halFormsConfiguration, ObjectProvider halConfiguration, MessageResolver resolver, @@ -67,6 +71,15 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { return new HalFormsLinkDiscoverer(); } + @Bean + HalFormsTemplatePropertyWriter halFormsTemplatePropertyWriter() { + + HalFormsConfiguration configuration = getResolvedConfiguration(); + HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, resolver); + + return new HalFormsTemplatePropertyWriter(builder); + } + /* * (non-Javadoc) * @see org.springframework.hateoas.config.HypermediaMappingInformation#configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper) @@ -74,15 +87,15 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { @Override public ObjectMapper configureObjectMapper(ObjectMapper mapper) { - HalFormsConfiguration configuration = halFormsConfiguration - .getIfAvailable(() -> new HalFormsConfiguration(halConfiguration.getIfAvailable(HalConfiguration::new))); + HalFormsConfiguration halFormsConfig = getResolvedConfiguration(); + CurieProvider provider = curieProvider.getIfAvailable(() -> CurieProvider.NONE); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.registerModule(new Jackson2HalFormsModule()); - mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, - curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, configuration, beanFactory)); + mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, provider, + resolver, halFormsConfig.getHalConfiguration(), beanFactory)); - configuration.customize(mapper); + halFormsConfig.customize(mapper); return mapper; } @@ -95,4 +108,16 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { public List getMediaTypes() { return HypermediaType.HAL_FORMS.getMediaTypes(); } + + HalFormsConfiguration getResolvedConfiguration() { + + Supplier defaultConfig = () -> new HalFormsConfiguration( + halConfiguration.getIfAvailable(HalConfiguration::new)); + + if (resolvedConfiguration == null) { + this.resolvedConfiguration = halFormsConfiguration.getIfAvailable(defaultConfig); + } + + return resolvedConfiguration; + } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsPropertyFactory.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsPropertyFactory.java index 76add488..f59a807d 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsPropertyFactory.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsPropertyFactory.java @@ -43,7 +43,7 @@ import org.springframework.util.StringUtils; * @since 1.3 * @soundtrack The Chicks - March March (Gaslighter) */ -public class HalFormsPropertyFactory { +class HalFormsPropertyFactory { private static final Set ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH); diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java deleted file mode 100644 index 4e58d020..00000000 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright 2017-2021 the original author or authors. - * - * Licensed 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 - * - * https://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 org.springframework.hateoas.mediatype.hal.forms; - -import java.io.IOException; -import java.util.Map; - -import org.springframework.hateoas.CollectionModel; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.PagedModel; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.mediatype.hal.HalConfiguration; -import org.springframework.hateoas.mediatype.hal.HalLinkRelation; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper; -import org.springframework.lang.Nullable; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.BeanProperty; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.jsontype.TypeSerializer; -import com.fasterxml.jackson.databind.ser.ContainerSerializer; -import com.fasterxml.jackson.databind.ser.ContextualSerializer; - -/** - * Collection of components needed to serialize a HAL-FORMS document. - * - * @author Greg Turnquist - */ -class HalFormsSerializers { - - static class HalFormsRepresentationModelSerializer extends ContainerSerializer> - implements ContextualSerializer { - - private static final long serialVersionUID = -4583146321934407153L; - - private final HalFormsTemplateBuilder builder; - private final BeanProperty property; - - HalFormsRepresentationModelSerializer(HalFormsTemplateBuilder builder, @Nullable BeanProperty property) { - - super(RepresentationModel.class, false); - - this.builder = builder; - this.property = property; - } - - HalFormsRepresentationModelSerializer(HalFormsTemplateBuilder customizations) { - this(customizations, null); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) - */ - @Override - @SuppressWarnings("null") - public void serialize(RepresentationModel value, JsonGenerator gen, SerializerProvider provider) - throws IOException { - - HalFormsDocument doc = HalFormsDocument.forRepresentationModel(value) // - .withLinks(value.getLinks()) // - .withTemplates(builder.findTemplates(value)); - - provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider); - } - - @Override - @Nullable - @SuppressWarnings("null") - public JavaType getContentType() { - return null; - } - - @Override - @Nullable - @SuppressWarnings("null") - public JsonSerializer getContentSerializer() { - return null; - } - - @Override - @SuppressWarnings("null") - public boolean hasSingleElement(RepresentationModel resource) { - return false; - } - - @Override - @Nullable - @SuppressWarnings("null") - protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) { - return null; - } - - @Override - @SuppressWarnings("null") - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) { - return new HalFormsRepresentationModelSerializer(builder, property); - } - } - - /** - * Serializer for {@link CollectionModel}. - */ - static class HalFormsEntityModelSerializer extends ContainerSerializer> - implements ContextualSerializer { - - private static final long serialVersionUID = -7912243216469101379L; - - private final HalFormsTemplateBuilder builder; - private final BeanProperty property; - - HalFormsEntityModelSerializer(HalFormsTemplateBuilder builder, @Nullable BeanProperty property) { - - super(EntityModel.class, false); - - this.builder = builder; - this.property = property; - } - - HalFormsEntityModelSerializer(HalFormsTemplateBuilder builder) { - this(builder, null); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) - */ - @Override - @SuppressWarnings("null") - public void serialize(EntityModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { - - HalFormsDocument doc = HalFormsDocument.forEntity(value.getContent()) // - .withLinks(value.getLinks()) // - .withTemplates(builder.findTemplates(value)); - - provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() - */ - @Override - @Nullable - public JavaType getContentType() { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() - */ - @Override - @Nullable - public JsonSerializer getContentSerializer() { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) - */ - @Override - @SuppressWarnings("null") - public boolean hasSingleElement(EntityModel resource) { - return false; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) - */ - @Override - @Nullable - @SuppressWarnings("null") - protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty) - */ - @Override - @SuppressWarnings("null") - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) - throws JsonMappingException { - return new HalFormsEntityModelSerializer(builder, property); - } - } - - /** - * Serializer for {@link CollectionModel} - */ - static class HalFormsCollectionModelSerializer extends ContainerSerializer> - implements ContextualSerializer { - - private static final long serialVersionUID = -3601146866067500734L; - - private final BeanProperty property; - private final Jackson2HalModule.EmbeddedMapper embeddedMapper; - private final HalFormsTemplateBuilder customizations; - private final HalConfiguration configuration; - - HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations, - Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration, - @Nullable BeanProperty property) { - - super(CollectionModel.class, false); - - this.property = property; - this.embeddedMapper = embeddedMapper; - this.customizations = customizations; - this.configuration = configuration; - } - - HalFormsCollectionModelSerializer(HalFormsTemplateBuilder customizations, - Jackson2HalModule.EmbeddedMapper embeddedMapper, HalConfiguration configuration) { - this(customizations, embeddedMapper, configuration, null); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) - */ - @Override - @SuppressWarnings("null") - public void serialize(CollectionModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { - - EmbeddedMapper mapper = configuration.isApplyPropertyNamingStrategy() // - ? embeddedMapper.with(provider.getConfig().getPropertyNamingStrategy()) // - : embeddedMapper; - - Map embeddeds = mapper.map(value); - - HalFormsDocument doc; - - if (value instanceof PagedModel) { - - doc = HalFormsDocument.empty() // - .withEmbedded(embeddeds) // - .withPageMetadata(((PagedModel) value).getMetadata()) // - .withLinks(value.getLinks()) // - .withTemplates(customizations.findTemplates(value)); - - } else { - - doc = HalFormsDocument.empty() // - .withEmbedded(embeddeds) // - .withLinks(value.getLinks()) // - .withTemplates(customizations.findTemplates(value)); - } - - provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() - */ - @Override - @Nullable - public JavaType getContentType() { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() - */ - @Override - @Nullable - public JsonSerializer getContentSerializer() { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) - */ - @Override - @SuppressWarnings("null") - public boolean hasSingleElement(CollectionModel resources) { - return resources.getContent().size() == 1; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer) - */ - @Override - @Nullable - @SuppressWarnings("null") - protected ContainerSerializer _withValueTypeSerializer(TypeSerializer typeSerializer) { - return null; - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty) - */ - @Override - @SuppressWarnings("null") - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) - throws JsonMappingException { - return new HalFormsCollectionModelSerializer(customizations, embeddedMapper, configuration, property); - } - } -} diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplatePropertyWriter.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplatePropertyWriter.java new file mode 100644 index 00000000..38849ad2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplatePropertyWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed 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 + * + * https://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 org.springframework.hateoas.mediatype.hal.forms; + +import org.springframework.hateoas.RepresentationModel; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter; +import com.fasterxml.jackson.databind.util.Annotations; + +/** + * @author Oliver Drotbohm + */ +@SuppressWarnings("null") +class HalFormsTemplatePropertyWriter extends VirtualBeanPropertyWriter { + + private static final long serialVersionUID = 6271264033606657428L; + + private final HalFormsTemplateBuilder builder; + + /** + * @param builder must not be {@literal null}. + */ + public HalFormsTemplatePropertyWriter(HalFormsTemplateBuilder builder) { + + Assert.notNull(builder, "HalFormsTemplateBuilder must not be null!"); + + this.builder = builder; + } + + /** + * @param builder2 + * @param config + * @param declaringClass + * @param propDef + * @param type + */ + public HalFormsTemplatePropertyWriter(HalFormsTemplateBuilder builder, MapperConfig config, + Annotations annotations, BeanPropertyDefinition propDef, JavaType type) { + + super(propDef, annotations, type); + + this.builder = builder; + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter#value(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + */ + @Override + protected Object value(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception { + + RepresentationModel model = (RepresentationModel) bean; + + return builder.findTemplates(model); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter#withConfig(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.AnnotatedClass, com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition, com.fasterxml.jackson.databind.JavaType) + */ + @Override + public VirtualBeanPropertyWriter withConfig(MapperConfig config, AnnotatedClass declaringClass, + BeanPropertyDefinition propDef, JavaType type) { + return new HalFormsTemplatePropertyWriter(builder, config, declaringClass.getAnnotations(), propDef, type); + } +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java index 814474dd..9031e6e5 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java @@ -15,57 +15,22 @@ */ package org.springframework.hateoas.mediatype.hal.forms; -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.hateoas.CollectionModel; -import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; -import org.springframework.hateoas.Links; -import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.mediatype.MessageResolver; -import org.springframework.hateoas.mediatype.hal.CurieProvider; -import org.springframework.hateoas.mediatype.hal.HalConfiguration; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.EmbeddedMapper; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListDeserializer; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; +import org.springframework.hateoas.mediatype.hal.CollectionModelMixin; import org.springframework.hateoas.mediatype.hal.LinkMixin; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsDeserializers.HalFormsCollectionModelDeserializer; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsCollectionModelSerializer; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsEntityModelSerializer; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsRepresentationModelSerializer; -import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.mvc.JacksonSerializers.MediaTypeDeserializer; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.DeserializationConfig; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.KeyDeserializer; -import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.annotation.JsonAppend; +import com.fasterxml.jackson.databind.annotation.JsonAppend.Prop; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.cfg.MapperConfig; -import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase; -import com.fasterxml.jackson.databind.introspect.Annotated; -import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; -import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; /** * Serialize / deserialize all the parts of HAL-FORMS documents using Jackson. @@ -83,176 +48,16 @@ public class Jackson2HalFormsModule extends SimpleModule { super("hal-forms-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas")); setMixInAnnotation(Link.class, LinkMixin.class); - setMixInAnnotation(Links.class, LinksMixin.class); setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class); - setMixInAnnotation(EntityModel.class, EntityModelMixin.class); setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class); - setMixInAnnotation(PagedModel.class, PagedModelMixin.class); setMixInAnnotation(MediaType.class, MediaTypeMixin.class); } - @JsonSerialize(using = HalLinkListSerializer.class) - abstract class LinksMixin {} - - @JsonSerialize(using = HalFormsRepresentationModelSerializer.class) + @JsonAppend( + props = @Prop(name = "_templates", value = HalFormsTemplatePropertyWriter.class, include = Include.NON_EMPTY)) abstract class RepresentationModelMixin extends org.springframework.hateoas.mediatype.hal.RepresentationModelMixin {} - @JsonSerialize(using = HalFormsEntityModelSerializer.class) - abstract class EntityModelMixin extends EntityModel {} - - @JsonSerialize(using = HalFormsCollectionModelSerializer.class) - abstract class CollectionModelMixin extends CollectionModel { - - @Override - @JsonProperty("_embedded") - @JsonInclude(Include.NON_EMPTY) - @JsonDeserialize(using = HalFormsCollectionModelDeserializer.class) - public abstract Collection getContent(); - } - - abstract class PagedModelMixin extends PagedModel { - - @Nullable - @Override - @JsonProperty("page") - @JsonInclude(Include.NON_EMPTY) - public PageMetadata getMetadata() { - return super.getMetadata(); - } - } - @JsonSerialize(using = ToStringSerializer.class) @JsonDeserialize(using = MediaTypeDeserializer.class) interface MediaTypeMixin {} - - static class HalFormsLinksDeserializer extends ContainerDeserializerBase { - - private static final long serialVersionUID = -848240531474910385L; - - private final HalLinkListDeserializer delegate = new HalLinkListDeserializer(); - - public HalFormsLinksDeserializer() { - super(TypeFactory.defaultInstance().constructType(Links.class)); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer() - */ - @Override - @Nullable - public JsonDeserializer getContentDeserializer() { - return delegate.getContentDeserializer(); - } - - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) - */ - @Override - @SuppressWarnings("null") - public Links deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { - return Links.of(delegate.deserialize(p, ctxt)); - } - } - - /** - * Create new HAL-FORMS serializers based on the context. - */ - public static class HalFormsHandlerInstantiator extends HalHandlerInstantiator { - - private final Map, Object> serializers = new HashMap<>(); - - public HalFormsHandlerInstantiator(LinkRelationProvider resolver, CurieProvider curieProvider, - MessageResolver accessor, HalFormsConfiguration configuration, AutowireCapableBeanFactory beanFactory) { - - super(resolver, curieProvider, accessor, configuration.getHalConfiguration(), beanFactory); - - HalConfiguration halConfiguration = configuration.getHalConfiguration(); - EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, - halConfiguration.isEnforceEmbeddedCollections()); - HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, accessor); - - this.serializers.put(HalFormsRepresentationModelSerializer.class, - new HalFormsRepresentationModelSerializer(builder)); - this.serializers.put(HalFormsEntityModelSerializer.class, new HalFormsEntityModelSerializer(builder)); - this.serializers.put(HalFormsCollectionModelSerializer.class, - new HalFormsCollectionModelSerializer(builder, mapper, halConfiguration)); - this.serializers.put(HalLinkListSerializer.class, - new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration)); - } - - public HalFormsHandlerInstantiator(LinkRelationProvider relProvider, CurieProvider curieProvider, - MessageResolver resolver, AutowireCapableBeanFactory beanFactory) { - - this(relProvider, curieProvider, resolver, beanFactory.getBean(HalFormsConfiguration.class), beanFactory); - } - - @Nullable - private Object findInstance(Class type) { - return this.serializers.get(type); - } - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, - Class deserClass) { - - Object jsonDeser = findInstance(deserClass); - return jsonDeser != null ? (JsonDeserializer) jsonDeser - : super.deserializerInstance(config, annotated, deserClass); - } - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, - Class keyDeserClass) { - - Object keyDeser = findInstance(keyDeserClass); - return keyDeser != null ? (KeyDeserializer) keyDeser - : super.keyDeserializerInstance(config, annotated, keyDeserClass); - } - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { - - Object jsonSer = findInstance(serClass); - return jsonSer != null ? (JsonSerializer) jsonSer : super.serializerInstance(config, annotated, serClass); - } - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, - Class builderClass) { - - Object builder = findInstance(builderClass); - return builder != null ? (TypeResolverBuilder) builder - : super.typeResolverBuilderInstance(config, annotated, builderClass); - } - - /* - * (non-Javadoc) - * @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) - */ - @Override - public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { - - Object resolver = findInstance(resolverClass); - return resolver != null ? (TypeIdResolver) resolver - : super.typeIdResolverInstance(config, annotated, resolverClass); - } - } } diff --git a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java index 2ead8563..aada39f6 100755 --- a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java @@ -19,11 +19,6 @@ import java.io.StringWriter; import java.io.Writer; import org.junit.jupiter.api.BeforeEach; -import org.springframework.hateoas.mediatype.MessageResolver; -import org.springframework.hateoas.mediatype.hal.CurieProvider; -import org.springframework.hateoas.mediatype.hal.HalConfiguration; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerInstantiator; -import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider; import com.fasterxml.jackson.databind.ObjectMapper; @@ -44,16 +39,6 @@ public abstract class AbstractJackson2MarshallingIntegrationTest { mapper = MappingTestUtils.defaultObjectMapper(); } - protected ObjectMapper with(HalConfiguration configuration) { - - ObjectMapper copy = mapper.copy(); - - copy.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE, - MessageResolver.DEFAULTS_ONLY, configuration)); - - return copy; - } - protected String write(Object object) throws Exception { Writer writer = new StringWriter(); mapper.writeValue(writer, object); diff --git a/src/test/java/org/springframework/hateoas/MappingTestUtils.java b/src/test/java/org/springframework/hateoas/MappingTestUtils.java index e521061b..2b245241 100644 --- a/src/test/java/org/springframework/hateoas/MappingTestUtils.java +++ b/src/test/java/org/springframework/hateoas/MappingTestUtils.java @@ -86,27 +86,45 @@ public class MappingTestUtils { } } - public RepresentationModel readObject(String filename) { - return readObject(filename, RepresentationModel.class); + public RepresentationModel readObject(String serialized) { + try { + return mapper.readValue(serialized, RepresentationModel.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } - public T readObject(String filename, Class type) { + public RepresentationModel readFile(String filename) { + return readFile(filename, RepresentationModel.class); + } + + public T readFile(String filename, Class type) { TypeFactory factory = mapper.getTypeFactory(); JavaType javaType = factory.constructType(type); - return readObject(filename, javaType); + return readFile(filename, javaType); } - public S readObject(String filename, Class type, Class elementType) { + public S readFile(String filename, Class type, Class elementType) { TypeFactory factory = mapper.getTypeFactory(); JavaType javaType = factory.constructParametricType(type, elementType); - return readObject(filename, javaType); + return readFile(filename, javaType); } - public S readObject(String filename, JavaType type) { + public S readFile(String filename, Class type, Class elementType, Class nested) { + + TypeFactory factory = mapper.getTypeFactory(); + + JavaType genericElement = factory.constructParametricType(elementType, nested); + JavaType javaType = factory.constructParametricType(type, genericElement); + + return readFile(filename, javaType); + } + + public S readFile(String filename, JavaType type) { ClassPathResource resource = new ClassPathResource(filename, context); @@ -119,7 +137,7 @@ public class MappingTestUtils { } } - public String readFile(String filename) { + public String readFileContent(String filename) { ClassPathResource resource = new ClassPathResource(filename, context); diff --git a/src/test/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonIntegrationTest.java index d9a20803..c65b979e 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/collectionjson/Jackson2CollectionJsonIntegrationTest.java @@ -37,7 +37,6 @@ import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.mediatype.hal.SimplePojo; -import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.SerializationFeature; /** @@ -71,7 +70,7 @@ class Jackson2CollectionJsonIntegrationTest { RepresentationModel resourceSupport = new RepresentationModel<>(); resourceSupport.add(Link.of("localhost").withSelfRel()); - assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFile("resource-support.json")); + assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFileContent("resource-support.json")); } @Test @@ -80,7 +79,7 @@ class Jackson2CollectionJsonIntegrationTest { RepresentationModel expected = new RepresentationModel<>(); expected.add(Link.of("localhost")); - assertThat(mapper.readObject("resource-support.json")).isEqualTo(expected); + assertThat(mapper.readFile("resource-support.json")).isEqualTo(expected); } @Test @@ -90,7 +89,7 @@ class Jackson2CollectionJsonIntegrationTest { resourceSupport.add(Link.of("localhost")); resourceSupport.add(Link.of("localhost2").withRel("orders")); - assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFile("resource-support-2.json")); + assertThat(mapper.writeObject(resourceSupport)).isEqualTo(mapper.readFileContent("resource-support-2.json")); } @Test @@ -99,7 +98,7 @@ class Jackson2CollectionJsonIntegrationTest { ResourceWithAttributes resource = new ResourceWithAttributes("test value"); resource.add(Link.of("localhost").withSelfRel()); - assertThat(mapper.writeObject(resource)).isEqualTo(mapper.readFile("resource-support-3.json")); + assertThat(mapper.writeObject(resource)).isEqualTo(mapper.readFileContent("resource-support-3.json")); } @Test @@ -108,7 +107,7 @@ class Jackson2CollectionJsonIntegrationTest { ResourceWithAttributes expected = new ResourceWithAttributes("test value"); expected.add(Link.of("localhost").withSelfRel()); - assertThat(mapper.readObject("resource-support-3.json", ResourceWithAttributes.class)).isEqualTo(expected); + assertThat(mapper.readFile("resource-support-3.json", ResourceWithAttributes.class)).isEqualTo(expected); } @Test @@ -118,7 +117,7 @@ class Jackson2CollectionJsonIntegrationTest { expected.add(Link.of("localhost")); expected.add(Link.of("localhost2").withRel("orders")); - assertThat(mapper.readObject("resource-support-2.json").getLinks()).containsAll(expected.getLinks()); + assertThat(mapper.readFile("resource-support-2.json").getLinks()).containsAll(expected.getLinks()); } @Test @@ -131,7 +130,7 @@ class Jackson2CollectionJsonIntegrationTest { CollectionModel resources = CollectionModel.of(content); resources.add(Link.of("localhost")); - assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources.json")); + assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources.json")); } @Test @@ -144,7 +143,7 @@ class Jackson2CollectionJsonIntegrationTest { CollectionModel expected = CollectionModel.of(content); expected.add(Link.of("localhost")); - CollectionModel result = mapper.readObject("resources.json", CollectionModel.class, String.class); + CollectionModel result = mapper.readFile("resources.json", CollectionModel.class, String.class); assertThat(result).isEqualTo(expected); } @@ -153,13 +152,13 @@ class Jackson2CollectionJsonIntegrationTest { void renderResource() { assertThat(mapper.writeObject(EntityModel.of("first", Link.of("localhost")))) // - .isEqualTo(mapper.readFile("resource.json")); + .isEqualTo(mapper.readFileContent("resource.json")); } @Test void deserializeResource() { - EntityModel actual = mapper.readObject("resource.json", EntityModel.class, String.class); + EntityModel actual = mapper.readFile("resource.json", EntityModel.class, String.class); assertThat(actual).isEqualTo(EntityModel.of("first", Link.of("localhost"))); } @@ -175,7 +174,7 @@ class Jackson2CollectionJsonIntegrationTest { resources.add(Link.of("localhost")); resources.add(Link.of("/page/2").withRel("next")); - assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources-with-resource-objects.json")); + assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources-with-resource-objects.json")); } @Test @@ -189,10 +188,8 @@ class Jackson2CollectionJsonIntegrationTest { expected.add(Link.of("localhost")); expected.add(Link.of("/page/2").withRel("next")); - JavaType entityModel = mapper.getGenericType(EntityModel.class, String.class); - JavaType collectionModel = mapper.getGenericType(CollectionModel.class, entityModel); - - CollectionModel actual = mapper.readObject("resources-with-resource-objects.json", collectionModel); + CollectionModel actual = mapper.readFile("resources-with-resource-objects.json", CollectionModel.class, + EntityModel.class, String.class); assertThat(actual).isEqualTo(expected); } @@ -208,25 +205,21 @@ class Jackson2CollectionJsonIntegrationTest { resources.add(Link.of("localhost")); resources.add(Link.of("/page/2").withRel("next")); - assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFile("resources-simple-pojos.json")); + assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("resources-simple-pojos.json")); } @Test void serializesPagedResource() throws Exception { assertThat(mapper.writeObject(setupAnnotatedPagedResources())) // - .isEqualTo(mapper.readFile("paged-resources.json")); + .isEqualTo(mapper.readFileContent("paged-resources.json")); } @Test void deserializesPagedResource() throws Exception { - JavaType entityModel = mapper.getGenericType(EntityModel.class, SimplePojo.class); - JavaType pagedModel = mapper.getGenericType(PagedModel.class, entityModel); - - mapper.readObject("paged-resources.json", pagedModel); - - PagedModel result = mapper.readObject("paged-resources.json", pagedModel); + PagedModel result = mapper.readFile("paged-resources.json", PagedModel.class, EntityModel.class, + SimplePojo.class); assertThat(result).isEqualTo(setupAnnotatedPagedResources()); } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalModelBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalModelBuilderUnitTest.java index 2f79aad7..4bb4bc1a 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/HalModelBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalModelBuilderUnitTest.java @@ -99,7 +99,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) - .isEqualTo(contextualMapper.readFile("hal-embedded-author-illustrator.json")); + .isEqualTo(contextualMapper.readFileContent("hal-embedded-author-illustrator.json")); } @Test // #864 @@ -119,7 +119,7 @@ public class HalModelBuilderUnitTest { .forLink(Link.of("/people/john-smith", ILLUSTRATOR_REL)).build(); assertThat(this.mapper.writeValueAsString(model)) - .isEqualTo(contextualMapper.readFile("hal-embedded-author-illustrator.json")); + .isEqualTo(contextualMapper.readFileContent("hal-embedded-author-illustrator.json")); } @Test // #864 @@ -130,7 +130,7 @@ public class HalModelBuilderUnitTest { .link(ALAN_WATTS_SELF) // .build(); - assertThat(this.mapper.writeValueAsString(model)).isEqualTo(contextualMapper.readFile("hal-single-item.json")); + assertThat(this.mapper.writeValueAsString(model)).isEqualTo(contextualMapper.readFileContent("hal-single-item.json")); } @Test // #864 @@ -142,7 +142,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) // - .isEqualTo(contextualMapper.readFile("hal-single-item.json")); + .isEqualTo(contextualMapper.readFileContent("hal-single-item.json")); } @Test // #864 @@ -173,7 +173,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) - .isEqualTo(contextualMapper.readFile("hal-embedded-collection.json")); + .isEqualTo(contextualMapper.readFileContent("hal-embedded-collection.json")); } @Test // #864 @@ -199,7 +199,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) - .isEqualTo(contextualMapper.readFile("hal-embedded-collection.json")); + .isEqualTo(contextualMapper.readFileContent("hal-embedded-collection.json")); } @Test @@ -208,7 +208,7 @@ public class HalModelBuilderUnitTest { HalModelBuilder halModelBuilder = halModel(); assertThat(this.mapper.writeValueAsString(halModelBuilder.build())) - .isEqualTo(contextualMapper.readFile("hal-empty.json")); + .isEqualTo(contextualMapper.readFileContent("hal-empty.json")); halModelBuilder // .entity(halModel() // @@ -218,13 +218,13 @@ public class HalModelBuilderUnitTest { .build()); assertThat(this.mapper.writeValueAsString(halModelBuilder.build())) - .isEqualTo(contextualMapper.readFile("hal-one-thing.json")); + .isEqualTo(contextualMapper.readFileContent("hal-one-thing.json")); halModelBuilder // .embed(new Product("Alf alarm clock", 19.99), LinkRelation.of("product")).build(); assertThat(this.mapper.writeValueAsString(halModelBuilder.build())) - .isEqualTo(contextualMapper.readFile("hal-two-things.json")); + .isEqualTo(contextualMapper.readFileContent("hal-two-things.json")); } @Test // #193 @@ -239,7 +239,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) // - .isEqualTo(contextualMapper.readFile("hal-multiple-types.json")); + .isEqualTo(contextualMapper.readFileContent("hal-multiple-types.json")); } @Test // #193 @@ -260,7 +260,7 @@ public class HalModelBuilderUnitTest { .build(); assertThat(this.mapper.writeValueAsString(model)) - .isEqualTo(contextualMapper.readFile("hal-explicit-and-implicit-relations.json")); + .isEqualTo(contextualMapper.readFileContent("hal-explicit-and-implicit-relations.json")); } @Test // #175 #864 @@ -306,7 +306,7 @@ public class HalModelBuilderUnitTest { } assertThat(this.mapper.writeValueAsString(builder.build())) - .isEqualTo(contextualMapper.readFile("zoom-hypermedia.json")); + .isEqualTo(contextualMapper.readFileContent("zoom-hypermedia.json")); } @Test // #864 diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java index fd5976f1..d448dead 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java @@ -63,7 +63,7 @@ class HalObjectMapperCustomizerTest { void objectMapperCustomizerShouldBeApplied() throws Exception { String actualHalJson = this.mockMvc.perform(get("/employees/0")).andReturn().getResponse().getContentAsString(); - String expectedHalJson = this.mapper.readFile("hal-custom.json"); + String expectedHalJson = this.mapper.readFileContent("hal-custom.json"); assertThat(actualHalJson).isEqualTo(expectedHalJson); } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalTestUtils.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalTestUtils.java index afff4d4e..7cf2d1b0 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/HalTestUtils.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalTestUtils.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.mediatype.hal; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.MappingTestUtils; import org.springframework.hateoas.mediatype.MessageResolver; @@ -59,7 +60,8 @@ public class HalTestUtils { mapper.registerModule(new Jackson2HalModule()); mapper.setHandlerInstantiator( - new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, configuration)); + new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, configuration, + new DefaultListableBeanFactory())); return mapper; } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsLinkDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsLinkDiscovererUnitTest.java index f8658f24..9f03904e 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsLinkDiscovererUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsLinkDiscovererUnitTest.java @@ -16,25 +16,28 @@ package org.springframework.hateoas.mediatype.hal.forms; import static org.assertj.core.api.Assertions.*; -import static org.springframework.hateoas.support.MappingUtils.*; import java.io.IOException; import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; +import org.springframework.hateoas.MappingTestUtils.ContextualMapper; import org.springframework.hateoas.client.LinkDiscoverer; import org.springframework.hateoas.client.LinkDiscovererUnitTest; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Unit tests for {@link HalFormsLinkDiscoverer}. * * @author Greg Turnquist + * @author Oliver Drotbohm */ class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest { static final LinkDiscoverer discoverer = new HalFormsLinkDiscoverer(); + static final ContextualMapper mapper = ContextualMapper.of(HalFormsLinkDiscovererUnitTest.class, new ObjectMapper()); /** * @see #314 @@ -50,7 +53,7 @@ class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest { @Test void discoversAllTheLinkAttributes() throws IOException { - String linkText = read(new ClassPathResource("hal-forms-link.json", getClass())); + String linkText = mapper.readFileContent("hal-forms-link.json"); Link expected = Link.valueOf(";" // + "rel=\"self\";" // @@ -73,12 +76,7 @@ class HalFormsLinkDiscovererUnitTest extends LinkDiscovererUnitTest { @Override protected String getInputString() { - - try { - return read(new ClassPathResource("hal-forms-link-discoverer.json", getClass())); - } catch (IOException e) { - throw new RuntimeException(e); - } + return mapper.readFileContent("hal-forms-link-discoverer.json"); } @Override diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java deleted file mode 100644 index 2186d6d0..00000000 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2017-2021 the original author or authors. - * - * Licensed 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 - * - * https://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 org.springframework.hateoas.mediatype.hal.forms; - -import static org.assertj.core.api.Assertions.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.core.io.ClassPathResource; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.mediatype.MessageResolver; -import org.springframework.hateoas.mediatype.hal.CurieProvider; -import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider; -import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.converter.HttpMessageConverter; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * @author Greg Turnquist - * @author Oliver Gierke - */ -class HalFormsMessageConverterUnitTest { - - ObjectMapper mapper; - HttpMessageConverter messageConverter; - - @BeforeEach - void setUp() { - - this.mapper = new ObjectMapper(); - this.mapper.registerModule(new Jackson2HalFormsModule()); - this.mapper.setHandlerInstantiator( - new Jackson2HalFormsModule.HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE, - MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(), new DefaultListableBeanFactory())); - - TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( - RepresentationModel.class); - converter.setObjectMapper(mapper); - - this.messageConverter = converter; - } - - @Test - void canReadAHalFormsDocumentMessage() throws IOException { - - HttpInputMessage message = new HttpInputMessage() { - @Override - public InputStream getBody() throws IOException { - return new ClassPathResource("reference.json", getClass()).getInputStream(); - } - - @Override - public HttpHeaders getHeaders() { - return new HttpHeaders(); - } - }; - - Object convertedMessage = this.messageConverter.read(HalFormsDocument.class, message); - - assertThat(convertedMessage).isInstanceOf(HalFormsDocument.class); - - HalFormsDocument halFormsDocument = (HalFormsDocument) convertedMessage; - - assertThat(halFormsDocument.getLinks()).hasSize(2); - assertThat(halFormsDocument.getLinks()).extracting(Link::getHref).containsExactly("/employees", "/employees/1"); - - assertThat(halFormsDocument.getTemplates().size()).isEqualTo(1); - assertThat(halFormsDocument.getTemplates().keySet()).containsExactly("default"); - assertThat(halFormsDocument.getTemplates().get("default").getContentType()).isEqualTo("application/hal+json"); - assertThat(halFormsDocument.getTemplates().get("default").getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(halFormsDocument.getTemplates().get("default").getMethod()) - .isEqualTo(HttpMethod.GET.toString().toLowerCase()); - } - - @Test - @SuppressWarnings("rawtypes") - void canWriteAHalFormsDocumentMessage() throws IOException { - - HalFormsProperty property = HalFormsProperty.named("my-name")// - .withReadOnly(true) // - .withValue("my-value") // - .withPrompt("my-prompt") // - .withRegex("my-regex") // - .withRequired(true); - - HalFormsTemplate template = HalFormsTemplate.forMethod(HttpMethod.GET) // - .withTitle("HAL-FORMS unit test") // - .withContentType(MediaTypes.HAL_JSON) // - .andProperty(property); // - - HalFormsDocument expected = HalFormsDocument.empty() // - .andLink(Link.of("/employees").withRel("collection")) // - .andLink(Link.of("/employees/1").withSelfRel())// - .andTemplate("foo", template); - - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - HttpOutputMessage convertedMessage = new HttpOutputMessage() { - @Override - public OutputStream getBody() { - return stream; - } - - @Override - public HttpHeaders getHeaders() { - return new HttpHeaders(); - } - }; - - this.messageConverter.write(expected, MediaTypes.HAL_FORMS_JSON, convertedMessage); - - assertThat(this.mapper.readValue(stream.toString(), HalFormsDocument.class)).isEqualTo(expected); - } -} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java index 255012f1..49fc7562 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java @@ -64,7 +64,7 @@ class HalFormsObjectMapperCustomizerTest { String actualHalFormsJson = mockMvc.perform(get("/employees/0")).andReturn().getResponse() .getContentAsString(); - String expectedHalFormsJson = mapper.readFile("hal-forms-custom.json"); + String expectedHalFormsJson = mapper.readFileContent("hal-forms-custom.json"); assertThat(actualHalFormsJson).isEqualTo(expectedHalFormsJson); } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java index 30fe67a8..a5814db3 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java @@ -29,26 +29,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.Links; import org.springframework.hateoas.MappingTestUtils; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.mediatype.hal.HalConfiguration; -import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; import org.springframework.hateoas.support.WebMvcEmployeeController; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; - /** * @author Greg Turnquist */ @@ -121,7 +115,7 @@ class HalFormsWebMvcIntegrationTest { @Test void createNewEmployee() throws Exception { - String specBasedJson = MappingTestUtils.createMapper(getClass()).readFile("new-employee.json"); + String specBasedJson = MappingTestUtils.createMapper(getClass()).readFileContent("new-employee.json"); this.mockMvc.perform(post("/employees") // .content(specBasedJson) // @@ -145,18 +139,9 @@ class HalFormsWebMvcIntegrationTest { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass)) { HalFormsMediaTypeConfiguration mediaTypeConfiguration = context.getBean(HalFormsMediaTypeConfiguration.class); - ObjectMapper mapper = mediaTypeConfiguration.configureObjectMapper(new ObjectMapper()); + HalFormsConfiguration actual = mediaTypeConfiguration.getResolvedConfiguration(); - assertThatCode(() -> { - - JsonSerializer serializer = mapper.getSerializerProviderInstance() // - .findValueSerializer(Links.class); - - assertThat(serializer).isInstanceOfSatisfying(HalLinkListSerializer.class, it -> { - assertThat(ReflectionTestUtils.getField(serializer, "halConfiguration")).isSameAs(configuration); - }); - - }).doesNotThrowAnyException(); + assertThat(actual.getHalConfiguration()).isSameAs(configuration); } } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java index aadc642b..93efca74 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java @@ -27,6 +27,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.function.Consumer; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; @@ -37,36 +38,40 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.MessageSource; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.io.ClassPathResource; -import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; +import org.springframework.hateoas.MappingTestUtils; +import org.springframework.hateoas.MappingTestUtils.ContextualMapper; import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.UriTemplate; +import org.springframework.hateoas.config.HateoasConfiguration; import org.springframework.hateoas.mediatype.Affordances; import org.springframework.hateoas.mediatype.MessageResolver; import org.springframework.hateoas.mediatype.hal.CurieProvider; import org.springframework.hateoas.mediatype.hal.DefaultCurieProvider; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; import org.springframework.hateoas.mediatype.hal.HalTestUtils; +import org.springframework.hateoas.mediatype.hal.Jackson2HalModule; import org.springframework.hateoas.mediatype.hal.SimpleAnnotatedPojo; import org.springframework.hateoas.mediatype.hal.SimplePojo; -import org.springframework.hateoas.mediatype.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator; import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.core.AnnotationLinkRelationProvider; import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider; import org.springframework.hateoas.server.core.EmbeddedWrappers; import org.springframework.hateoas.support.EmployeeResource; -import org.springframework.hateoas.support.MappingUtils; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; -import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -77,26 +82,34 @@ import com.jayway.jsonpath.PathNotFoundException; * @author Greg Turnquist * @author Oliver Drotbohm */ -class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegrationTest { +class Jackson2HalFormsIntegrationTest { static final Links PAGINATION_LINKS = Links.of( // Link.of("foo", IanaLinkRelations.NEXT), // Link.of("bar", IanaLinkRelations.PREV) // ); + final LinkRelationProvider provider = new DelegatingLinkRelationProvider(new AnnotationLinkRelationProvider(), + HalTestUtils.DefaultLinkRelationProvider.INSTANCE); + final Consumer configurer = it -> { + + it.registerModule(new Jackson2HalFormsModule()); + it.configure(SerializationFeature.INDENT_OUTPUT, true); + }; + + final ContextualMapper mapper = MappingTestUtils.createMapper(Jackson2HalFormsIntegrationTest.class, + configurer.andThen(it -> { + + GenericApplicationContext context = new AnnotationConfigApplicationContext( + HalFormsMediaTypeConfiguration.class, HateoasConfiguration.class); + + it.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider, CurieProvider.NONE, + MessageResolver.DEFAULTS_ONLY, new HalConfiguration(), context.getAutowireCapableBeanFactory())); + })); + @BeforeEach void setUpModule() { - LocaleContextHolder.setLocale(Locale.US); - - LinkRelationProvider provider = new DelegatingLinkRelationProvider(new AnnotationLinkRelationProvider(), - HalTestUtils.DefaultLinkRelationProvider.INSTANCE); - - mapper.registerModule(new Jackson2HalFormsModule()); - mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( // - provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(), - new DefaultListableBeanFactory())); - mapper.configure(SerializationFeature.INDENT_OUTPUT, true); } @Test @@ -105,8 +118,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra RepresentationModel resourceSupport = new RepresentationModel<>(); resourceSupport.add(Link.of("localhost")); - assertThat(write(resourceSupport)) - .isEqualTo(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass()))); + assertThat(mapper.writeObject(resourceSupport)) + .isEqualTo(mapper.readFileContent("single-link-reference.json")); } @Test @@ -115,8 +128,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra RepresentationModel expected = new RepresentationModel<>(); expected.add(Link.of("localhost")); - assertThat(read(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())), - RepresentationModel.class)).isEqualTo(expected); + assertThat(mapper.readFile("single-link-reference.json")).isEqualTo(expected); } @Test @@ -126,8 +138,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra resourceSupport.add(Link.of("localhost")); resourceSupport.add(Link.of("localhost2")); - assertThat(write(resourceSupport)) - .isEqualTo(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass()))); + assertThat(mapper.writeObject(resourceSupport)) + .isEqualTo(mapper.readFileContent("list-link-reference.json")); } @Test @@ -137,8 +149,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra expected.add(Link.of("localhost")); expected.add(Link.of("localhost2")); - assertThat(read(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())), - RepresentationModel.class)).isEqualTo(expected); + assertThat(mapper.readFile("list-link-reference.json", RepresentationModel.class)).isEqualTo(expected); } @Test @@ -154,8 +165,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra resource.add(link); - assertThat(write(resource)) - .isEqualTo(MappingUtils.read(new ClassPathResource("employee-resource-support.json", getClass()))); + assertThat(mapper.writeObject(resource)) + .isEqualTo(mapper.readFileContent("employee-resource-support.json")); } @Test @@ -163,20 +174,17 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra EntityModel resource = EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost")); - assertThat(write(resource)) - .isEqualTo(MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass()))); + assertThat(mapper.writeObject(resource)) + .isEqualTo(mapper.readFileContent("simple-resource-unwrapped.json")); } @Test void deserializesResource() throws IOException { - EntityModel expected = EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost")); + EntityModel result = mapper.readFile("simple-resource-unwrapped.json", EntityModel.class, + SimplePojo.class); - EntityModel result = mapper.readValue( - MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())), - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class)); - - assertThat(result).isEqualTo(expected); + assertThat(result).isEqualTo(EntityModel.of(new SimplePojo("test1", 1), Link.of("localhost"))); } @Test @@ -189,8 +197,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel resources = CollectionModel.of(content); resources.add(Link.of("localhost")); - assertThat(write(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass()))); + assertThat(mapper.writeObject(resources)) + .isEqualTo(mapper.readFileContent("simple-embedded-resource-reference.json")); } @Test @@ -203,9 +211,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel expected = CollectionModel.of(content); expected.add(Link.of("localhost")); - CollectionModel result = mapper.readValue( - MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())), - mapper.getTypeFactory().constructParametricType(CollectionModel.class, String.class)); + CollectionModel result = mapper.readFile("simple-embedded-resource-reference.json", CollectionModel.class, + String.class); assertThat(result).isEqualTo(expected); @@ -220,8 +227,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> resources = CollectionModel.of(content); resources.add(Link.of("localhost")); - assertThat(write(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass()))); + assertThat(mapper.writeObject(resources)) + .isEqualTo(mapper.readFileContent("single-embedded-resource-reference.json")); } @Test @@ -233,10 +240,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> expected = CollectionModel.of(content); expected.add(Link.of("localhost")); - CollectionModel> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())), - mapper.getTypeFactory().constructParametricType(CollectionModel.class, - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class))); + CollectionModel> result = mapper.readFile("single-embedded-resource-reference.json", + CollectionModel.class, EntityModel.class, SimplePojo.class); assertThat(result).isEqualTo(expected); } @@ -247,8 +252,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> resources = setupResources(); resources.add(Link.of("localhost")); - assertThat(write(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass()))); + assertThat(mapper.writeObject(resources)) + .isEqualTo(mapper.readFileContent("multiple-resource-resources.json")); } @Test @@ -257,10 +262,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> expected = setupResources(); expected.add(Link.of("localhost")); - CollectionModel> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())), - mapper.getTypeFactory().constructParametricType(CollectionModel.class, - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimplePojo.class))); + CollectionModel> result = mapper.readFile("multiple-resource-resources.json", + CollectionModel.class, EntityModel.class, SimplePojo.class); assertThat(result).isEqualTo(expected); } @@ -274,8 +277,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> resources = CollectionModel.of(content); resources.add(Link.of("localhost")); - assertThat(write(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass()))); + assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("annotated-resource-resources.json")); } @Test @@ -287,43 +289,43 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel> expected = CollectionModel.of(content); expected.add(Link.of("localhost")); - CollectionModel> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())), - mapper.getTypeFactory().constructParametricType(CollectionModel.class, - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class))); + CollectionModel> result = mapper.readFile("annotated-resource-resources.json", + CollectionModel.class, EntityModel.class, SimpleAnnotatedPojo.class); assertThat(result).isEqualTo(expected); } @Test void serializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { - assertThat(write(setupAnnotatedResources())) - .isEqualTo(MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass()))); + assertThat(mapper.writeObject(setupAnnotatedResources())) + .isEqualTo(mapper.readFileContent("annotated-embedded-resources-reference.json")); } @Test void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { - CollectionModel> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())), - mapper.getTypeFactory().constructParametricType(CollectionModel.class, - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class))); + mapper.readFile("annotated-embedded-resources-reference.json", + CollectionModel.class, EntityModel.class, SimpleAnnotatedPojo.class); + + CollectionModel> result = mapper.readFile( + "annotated-embedded-resources-reference.json", CollectionModel.class, EntityModel.class, + SimpleAnnotatedPojo.class); assertThat(result).isEqualTo(setupAnnotatedResources()); } @Test void serializesPagedResource() throws Exception { - assertThat(write(setupAnnotatedPagedResources())) - .isEqualTo(MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass()))); + + assertThat(mapper.writeObject(setupAnnotatedPagedResources())) + .isEqualTo(mapper.readFileContent("annotated-paged-resources.json")); } @Test void deserializesPagedResource() throws Exception { - PagedModel> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())), - mapper.getTypeFactory().constructParametricType(PagedModel.class, - mapper.getTypeFactory().constructParametricType(EntityModel.class, SimpleAnnotatedPojo.class))); + + PagedModel> result = mapper.readFile("annotated-paged-resources.json", + PagedModel.class, EntityModel.class, SimpleAnnotatedPojo.class); assertThat(result).isEqualTo(setupAnnotatedPagedResources()); } @@ -334,16 +336,16 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel resources = CollectionModel.of(Collections.emptySet(), Link.of("foo"), Link.of("bar", "myrel")); - assertThat(getCuriedObjectMapper().writeValueAsString(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("curied-document.json", getClass()))); + assertThat(getCuriedObjectMapper().writeObject(resources)) + .isEqualTo(mapper.readFileContent("curied-document.json")); } @Test void doesNotRenderCuriesIfNoLinkIsPresent() throws Exception { CollectionModel resources = CollectionModel.of(Collections.emptySet()); - assertThat(getCuriedObjectMapper().writeValueAsString(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("empty-document.json", getClass()))); + assertThat(getCuriedObjectMapper().writeObject(resources)) + .isEqualTo(mapper.readFileContent("empty-document.json")); } @Test @@ -352,8 +354,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel resources = CollectionModel.of(Collections.emptySet()); resources.add(Link.of("foo")); - assertThat(getCuriedObjectMapper().writeValueAsString(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("single-non-curie-document.json", getClass()))); + assertThat(getCuriedObjectMapper().writeObject(resources)) + .isEqualTo(mapper.readFileContent("single-non-curie-document.json")); } @Test @@ -362,7 +364,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra RepresentationModel support = new RepresentationModel<>(); support.add(Link.of("/foo{?bar}", "search")); - assertThat(write(support)).isEqualTo(MappingUtils.read(new ClassPathResource("link-template.json", getClass()))); + assertThat(mapper.writeObject(support)).isEqualTo(mapper.readFileContent("link-template.json")); } @Test @@ -378,8 +380,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra } }; - assertThat(getCuriedObjectMapper(provider).writeValueAsString(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("multiple-curies-document.json", getClass()))); + assertThat(getCuriedObjectMapper(provider).writeObject(resources)) + .isEqualTo(mapper.readFileContent("multiple-curies-document.json")); } @Test @@ -392,8 +394,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra CollectionModel resources = CollectionModel.of(values); - assertThat(write(resources)) - .isEqualTo(MappingUtils.read(new ClassPathResource("empty-embedded-pojos.json", getClass()))); + assertThat(mapper.writeObject(resources)).isEqualTo(mapper.readFileContent("empty-embedded-pojos.json")); } @Test @@ -415,13 +416,13 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra RepresentationModel original = new RepresentationModel<>(); original.add(Link.of("/orders{?id}", "order")); - String serialized = mapper.writeValueAsString(original); + String serialized = mapper.writeObject(original); String expected = "{\n \"_links\" : {\n \"order\" : {\n \"href\" : \"/orders{?id}\",\n \"templated\" : true\n }\n }\n}"; assertThat(serialized).isEqualTo(expected); - RepresentationModel deserialized = mapper.readValue(serialized, RepresentationModel.class); + RepresentationModel deserialized = mapper.readObject(serialized); assertThat(deserialized).isEqualTo(original); } @@ -443,12 +444,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra .toLink(); EntityModel model = EntityModel.of(new HalFormsPayload(), link); - ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); + ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); assertThatCode(() -> { String promptString = JsonPath.compile("$._templates.default.properties[0].prompt") // - .read(mapper.writeValueAsString(model)); + .read(mapper.writeObject(model)); assertThat(promptString).isEqualTo("Vorname"); @@ -474,12 +475,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra .toLink(); EntityModel model = EntityModel.of(new HalFormsPayload(), link); - ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); + ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); assertThatCode(() -> { String promptString = JsonPath.compile("$._templates.default.title") // - .read(mapper.writeValueAsString(model)); + .read(mapper.writeObject(model)); assertThat(promptString).isEqualTo("Template title"); @@ -545,12 +546,12 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra .toLink(); EntityModel model = EntityModel.of(new HalFormsPayload(), link); - ObjectMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); + ContextualMapper mapper = getCuriedObjectMapper(CurieProvider.NONE, source); assertThatCode(() -> { String promptString = JsonPath.compile("$._templates.default.properties[0].placeholder") // - .read(mapper.writeValueAsString(model)); + .read(mapper.writeObject(model)); assertThat(promptString).isEqualTo("Property placeholder"); @@ -559,8 +560,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra private void assertThatPathDoesNotExist(Object toMarshall, String path) throws Exception { - ObjectMapper mapper = getCuriedObjectMapper(); - String json = mapper.writeValueAsString(toMarshall); + String json = getCuriedObjectMapper().writeObject(toMarshall); assertThatExceptionOfType(PathNotFoundException.class) // .isThrownBy(() -> JsonPath.compile(path).read(json)); @@ -568,8 +568,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra private void assertValueForPath(Object toMarshall, String path, Object expected) throws Exception { - ObjectMapper mapper = getCuriedObjectMapper(); - String json = mapper.writeValueAsString(toMarshall); + String json = getCuriedObjectMapper().writeObject(toMarshall); Object actual = JsonPath.compile(path).read(json); @@ -585,13 +584,13 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra StaticMessageSource messageSource = new StaticMessageSource(); messageSource.addMessage(resourceBundleKey, Locale.US, "Foobar's title!"); - ObjectMapper objectMapper = getCuriedObjectMapper(CurieProvider.NONE, messageSource); + ContextualMapper objectMapper = getCuriedObjectMapper(CurieProvider.NONE, messageSource); RepresentationModel resource = new RepresentationModel<>(); resource.add(Link.of("target", "ns:foobar")); - assertThat(objectMapper.writeValueAsString(resource)) - .isEqualTo(MappingUtils.read(new ClassPathResource("link-with-title.json", getClass()))); + assertThat(objectMapper.writeObject(resource)) + .isEqualTo(objectMapper.readFileContent("link-with-title.json")); } private static CollectionModel> setupResources() { @@ -621,32 +620,34 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra return PagedModel.of(content, new PagedModel.PageMetadata(2, 0, 4), PAGINATION_LINKS); } - private ObjectMapper getCuriedObjectMapper() { - + private ContextualMapper getCuriedObjectMapper() { return getCuriedObjectMapper(new DefaultCurieProvider("foo", UriTemplate.of("http://localhost:8080/rels/{rel}"))); } - private ObjectMapper getCuriedObjectMapper(CurieProvider provider) { + private ContextualMapper getCuriedObjectMapper(CurieProvider provider) { return getCuriedObjectMapper(provider, null); } - private ObjectMapper getCuriedObjectMapper(CurieProvider provider, @Nullable MessageSource messageSource) { + private ContextualMapper getCuriedObjectMapper(CurieProvider provider, @Nullable MessageSource messageSource) { - ObjectMapper mapper = new ObjectMapper(); + MessageResolver resolver = MessageResolver.of(messageSource); - mapper.registerModule(new Jackson2HalFormsModule()); - mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), provider, - MessageResolver.of(messageSource), new HalFormsConfiguration(), new DefaultListableBeanFactory())); - mapper.configure(SerializationFeature.INDENT_OUTPUT, true); - mapper.setSerializationInclusion(Include.NON_NULL); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), resolver); + factory.registerSingleton("foobar", new HalFormsTemplatePropertyWriter(builder)); - return mapper; + return MappingTestUtils.createMapper(Jackson2HalFormsIntegrationTest.class, configurer.andThen(it -> { + it.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(this.provider, provider, + resolver, new HalConfiguration(), factory)); + })); } + @JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY) public static class HalFormsPayload { private @Getter String firstname; } + @JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY) public static class Jsr303Sample { private String firstname; @@ -671,6 +672,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra } } + @JsonAutoDetect(getterVisibility = Visibility.PUBLIC_ONLY) public static class UnwrappedExampleElement { private @Getter String firstname; } diff --git a/src/test/java/org/springframework/hateoas/support/EmployeeResource.java b/src/test/java/org/springframework/hateoas/support/EmployeeResource.java index b16d723f..df202344 100644 --- a/src/test/java/org/springframework/hateoas/support/EmployeeResource.java +++ b/src/test/java/org/springframework/hateoas/support/EmployeeResource.java @@ -18,13 +18,17 @@ package org.springframework.hateoas.support; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.Getter; import org.springframework.hateoas.RepresentationModel; +import com.fasterxml.jackson.annotation.JsonProperty; + /** * @author Greg Turnquist */ @Data +@Getter(onMethod = @__(@JsonProperty)) @EqualsAndHashCode(callSuper = true) @AllArgsConstructor public class EmployeeResource extends RepresentationModel { @@ -32,7 +36,7 @@ public class EmployeeResource extends RepresentationModel { private String name; public EmployeeResource(EmployeeResource employeeResource) { - + this.name = employeeResource.getName(); add(employeeResource.getLinks()); } diff --git a/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/employee-resource-support.json b/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/employee-resource-support.json index 4ed39e9c..70fb38cc 100644 --- a/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/employee-resource-support.json +++ b/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/employee-resource-support.json @@ -1,4 +1,5 @@ { + "name" : "Frodo Baggins", "_links" : { "self" : { "href" : "/employees/1" @@ -12,6 +13,5 @@ "type" : "text" } ] } - }, - "name" : "Frodo Baggins" + } } \ No newline at end of file