diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java index 1096146e..4a74a87c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDeserializers.java @@ -41,20 +41,20 @@ import com.fasterxml.jackson.databind.type.TypeFactory; */ class HalFormsDeserializers { - static class HalFormsResourcesDeserializer extends ContainerDeserializerBase> + static class HalFormsCollectionModelDeserializer extends ContainerDeserializerBase> implements ContextualDeserializer { private static final long serialVersionUID = -7325599536381465624L; private JavaType contentType; - HalFormsResourcesDeserializer(JavaType contentType) { + HalFormsCollectionModelDeserializer(JavaType contentType) { super(contentType); this.contentType = contentType; } - HalFormsResourcesDeserializer() { + HalFormsCollectionModelDeserializer() { this(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, Object.class)); } @@ -119,7 +119,7 @@ class HalFormsDeserializers { public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { - return new HalFormsResourcesDeserializer( + return new HalFormsCollectionModelDeserializer( property == null ? ctxt.getContextualType() : property.getType().getContentType()); } } 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 index f906a0c4..51486991 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsDocument.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2019 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. @@ -16,6 +16,7 @@ package org.springframework.hateoas.mediatype.hal.forms; import lombok.AccessLevel; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.experimental.Wither; @@ -29,12 +30,15 @@ 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; @@ -54,10 +58,15 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; @Value @Wither @RequiredArgsConstructor(access = AccessLevel.PACKAGE) -@JsonPropertyOrder({ "resource", "resources", "embedded", "links", "templates", "metadata" }) +@JsonPropertyOrder({ "attributes", "resource", "resources", "embedded", "links", "templates", "metadata" }) public class HalFormsDocument { @Nullable // + @Getter(onMethod = @__(@JsonAnyGetter)) + @JsonInclude(Include.NON_EMPTY) // + @Wither(AccessLevel.PRIVATE) // + private Map attributes; + @JsonUnwrapped // @JsonInclude(Include.NON_NULL) // private T resource; @@ -90,7 +99,21 @@ public class HalFormsDocument { private Map templates; private HalFormsDocument() { - this(null, null, Collections.emptyMap(), null, Links.NONE, Collections.emptyMap()); + 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 + */ + public static HalFormsDocument forRepresentationModel(RepresentationModel model) { + + Map attributes = PropertyUtils.findProperties(model); + attributes.remove("links"); + + return new HalFormsDocument<>().withAttributes(attributes); } /** @@ -150,11 +173,11 @@ public class HalFormsDocument { } public HalFormsDocument withPageMetadata(@Nullable PageMetadata metadata) { - return new HalFormsDocument(resource, resources, embedded, metadata, links, templates); + return new HalFormsDocument(attributes, resource, resources, embedded, metadata, links, templates); } private HalFormsDocument withResource(@Nullable T resource) { - return new HalFormsDocument(resource, resources, embedded, pageMetadata, links, templates); + return new HalFormsDocument(attributes, resource, resources, embedded, pageMetadata, links, templates); } /** @@ -167,7 +190,7 @@ public class HalFormsDocument { Assert.notNull(link, "Link must not be null!"); - return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links.and(link), templates); + return new HalFormsDocument<>(attributes, resource, resources, embedded, pageMetadata, links.and(link), templates); } /** @@ -185,7 +208,7 @@ public class HalFormsDocument { Map templates = new HashMap<>(this.templates); templates.put(name, template); - return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates); + return new HalFormsDocument<>(attributes, resource, resources, embedded, pageMetadata, links, templates); } /** @@ -203,10 +226,6 @@ public class HalFormsDocument { Map embedded = new HashMap<>(this.embedded); embedded.put(key, value); - return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates); - } - - HalFormsDocument withLinks(Links links) { - return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates); + return new HalFormsDocument<>(attributes, resource, resources, embedded, pageMetadata, links, templates); } } 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 index 3feaaac5..97d09787 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsSerializers.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2019 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. @@ -59,24 +59,94 @@ import com.fasterxml.jackson.databind.ser.ContextualSerializer; */ class HalFormsSerializers { + static class HalFormsRepresentationModelSerializer extends ContainerSerializer> + implements ContextualSerializer { + + private static final long serialVersionUID = -4583146321934407153L; + + private final MessageSourceAccessor accessor; + private final BeanProperty property; + + HalFormsRepresentationModelSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property) { + + super(RepresentationModel.class, false); + this.property = property; + this.accessor = accessor; + } + + HalFormsRepresentationModelSerializer(MessageSourceAccessor accessor) { + this(accessor, 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(findTemplates(value, accessor)); + + 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(accessor, property); + } + } + /** * Serializer for {@link CollectionModel}. */ - static class HalFormsResourceSerializer extends ContainerSerializer> implements ContextualSerializer { + static class HalFormsEntityModelSerializer extends ContainerSerializer> + implements ContextualSerializer { private static final long serialVersionUID = -7912243216469101379L; private final MessageSourceAccessor accessor; private final BeanProperty property; - HalFormsResourceSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property) { + HalFormsEntityModelSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property) { super(EntityModel.class, false); this.accessor = accessor; this.property = property; } - HalFormsResourceSerializer(MessageSourceAccessor accessor) { + HalFormsEntityModelSerializer(MessageSourceAccessor accessor) { this(accessor, null); } @@ -144,14 +214,14 @@ class HalFormsSerializers { @SuppressWarnings("null") public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalFormsResourceSerializer(accessor, property); + return new HalFormsEntityModelSerializer(accessor, property); } } /** * Serializer for {@link CollectionModel} */ - static class HalFormsResourcesSerializer extends ContainerSerializer> + static class HalFormsCollectionModelSerializer extends ContainerSerializer> implements ContextualSerializer { private static final long serialVersionUID = -3601146866067500734L; @@ -160,7 +230,7 @@ class HalFormsSerializers { private final Jackson2HalModule.EmbeddedMapper embeddedMapper; private final MessageSourceAccessor accessor; - HalFormsResourcesSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property, + HalFormsCollectionModelSerializer(MessageSourceAccessor accessor, @Nullable BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) { super(CollectionModel.class, false); @@ -170,7 +240,7 @@ class HalFormsSerializers { this.accessor = accessor; } - HalFormsResourcesSerializer(MessageSourceAccessor accessor, Jackson2HalModule.EmbeddedMapper embeddedMapper) { + HalFormsCollectionModelSerializer(MessageSourceAccessor accessor, Jackson2HalModule.EmbeddedMapper embeddedMapper) { this(accessor, null, embeddedMapper); } @@ -254,7 +324,7 @@ class HalFormsSerializers { @SuppressWarnings("null") public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalFormsResourcesSerializer(accessor, property, embeddedMapper); + return new HalFormsCollectionModelSerializer(accessor, property, embeddedMapper); } } 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 fe24e41c..be49ad0e 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 the original author or authors. + * Copyright 2016-2019 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. @@ -34,10 +34,10 @@ import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalHandlerIns import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListDeserializer; import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; import org.springframework.hateoas.mediatype.hal.LinkMixin; -import org.springframework.hateoas.mediatype.hal.RepresentationModelMixin; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsDeserializers.HalFormsResourcesDeserializer; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsResourceSerializer; -import org.springframework.hateoas.mediatype.hal.forms.HalFormsSerializers.HalFormsResourcesSerializer; +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; @@ -93,16 +93,19 @@ class Jackson2HalFormsModule extends SimpleModule { @JsonSerialize(using = HalLinkListSerializer.class) abstract class LinksMixin {} - @JsonSerialize(using = HalFormsResourceSerializer.class) + @JsonSerialize(using = HalFormsRepresentationModelSerializer.class) + abstract class RepresentationModelMixin extends org.springframework.hateoas.mediatype.hal.RepresentationModelMixin {} + + @JsonSerialize(using = HalFormsEntityModelSerializer.class) abstract class EntityModelMixin extends EntityModel {} - @JsonSerialize(using = HalFormsResourcesSerializer.class) + @JsonSerialize(using = HalFormsCollectionModelSerializer.class) abstract class CollectionModelMixin extends CollectionModel { @Override @JsonProperty("_embedded") @JsonInclude(Include.NON_EMPTY) - @JsonDeserialize(using = HalFormsResourcesDeserializer.class) + @JsonDeserialize(using = HalFormsCollectionModelDeserializer.class) public abstract Collection getContent(); } @@ -166,8 +169,11 @@ class Jackson2HalFormsModule extends SimpleModule { EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections); - this.serializers.put(HalFormsResourceSerializer.class, new HalFormsResourceSerializer(accessor)); - this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(accessor, mapper)); + this.serializers.put(HalFormsRepresentationModelSerializer.class, + new HalFormsRepresentationModelSerializer(accessor)); + this.serializers.put(HalFormsEntityModelSerializer.class, new HalFormsEntityModelSerializer(accessor)); + this.serializers.put(HalFormsCollectionModelSerializer.class, + new HalFormsCollectionModelSerializer(accessor, mapper)); this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor, configuration.getHalConfiguration())); } 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 5248c8fb..2283b783 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 @@ -36,16 +36,9 @@ import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.StaticMessageSource; +import org.springframework.core.ResolvableType; 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.PagedModel; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.UriTemplate; +import org.springframework.hateoas.*; import org.springframework.hateoas.mediatype.hal.CurieProvider; import org.springframework.hateoas.mediatype.hal.DefaultCurieProvider; import org.springframework.hateoas.mediatype.hal.Jackson2HalIntegrationTest; @@ -56,6 +49,7 @@ 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; @@ -132,6 +126,20 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra RepresentationModel.class)).isEqualTo(expected); } + @Test + void rendersRepresentationModelWithTemplates() throws Exception { + + EmployeeResource resource = new EmployeeResource("Frodo Baggins"); + Link selfLink = new Link("/employees/1"); + selfLink = selfLink.andAffordance(new Affordance("foo", selfLink, HttpMethod.POST, + ResolvableType.forClass(EmployeeResource.class), Collections.emptyList(), + ResolvableType.forClass(EmployeeResource.class))); + resource.add(selfLink); + + assertThat(write(resource)) + .isEqualTo(MappingUtils.read(new ClassPathResource("employee-resource-support.json", getClass()))); + } + @Test void rendersResource() throws Exception { diff --git a/src/test/java/org/springframework/hateoas/support/EmployeeResource.java b/src/test/java/org/springframework/hateoas/support/EmployeeResource.java index 89cee071..7b0a28ce 100644 --- a/src/test/java/org/springframework/hateoas/support/EmployeeResource.java +++ b/src/test/java/org/springframework/hateoas/support/EmployeeResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2019 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. @@ -27,7 +27,13 @@ import org.springframework.hateoas.RepresentationModel; @Data @EqualsAndHashCode(callSuper = true) @AllArgsConstructor -class EmployeeResource extends RepresentationModel { +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 new file mode 100644 index 00000000..ef1ffb4f --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/employee-resource-support.json @@ -0,0 +1,19 @@ +{ + "_links" : { + "self" : { + "href" : "/employees/1" + } + }, + "_templates" : { + "default" : { + "title" : null, + "method" : "post", + "contentType" : "", + "properties" : [ { + "name" : "name", + "required" : true + } ] + } + }, + "name" : "Frodo Baggins" +} \ No newline at end of file