From 092ceb987f76791f4bb42d152f4c7242cad6785e Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 28 Aug 2017 16:47:26 +0200 Subject: [PATCH] #340 - Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved AffordanceModelFactory into core package as it's SPI. Switched to Spring Factories lookup of implementation classes so that we avoid a package dependency between the MVC package and the media type specific packages. Removed reference to MediaType from AffordanceModelFactory to AffordanceModel so that a factory can even provide models for different MediaTypes (i.e. different flavors of the same one, e.g. HAL Forms for JSON and XML). Also removed addAffordanceModel(…) from Affordance to not force the implementations into mutability. Made most of the affordance building API types package protected. HalFormsAffordanceModel now uses MethodParameters abstraction to simplify model parsing code. Tweaked HAL forms model to work with factory methods for required properties and wither methods to add optional properties. Tweaked and inlined mixin types in Jackson module for HAL forms. Slight API polishing on Link to make sure Affordance collecting methods are not named with…. Tweaked Lombok setup to use all caps for logger constants. Removed deprecation warnings in Jackson2HalModule. --- lombok.config | 3 + .../springframework/hateoas/Affordance.java | 15 +- .../hateoas/AffordanceModel.java | 14 +- .../org/springframework/hateoas/Link.java | 36 ++-- .../config/EnableHypermediaSupport.java | 12 +- ...ermediaSupportBeanDefinitionRegistrar.java | 10 +- .../{ => core}/AffordanceModelFactory.java | 27 +-- .../core/AnnotationMappingDiscoverer.java | 14 +- .../hateoas/core/LinkBuilderSupport.java | 14 +- .../hateoas/core/MappingDiscoverer.java | 13 +- .../hateoas/hal/Jackson2HalModule.java | 19 +- .../hateoas/hal/LinkMixin.java | 4 +- .../hal/forms/HalFormsAffordanceModel.java | 122 +++++++------ .../forms/HalFormsAffordanceModelFactory.java | 26 ++- .../hal/forms/HalFormsDeserializers.java | 12 +- .../hateoas/hal/forms/HalFormsDocument.java | 165 ++++++++++++++---- .../hal/forms/HalFormsMessageConverter.java | 15 +- .../hateoas/hal/forms/HalFormsProperty.java | 33 ++-- .../hal/forms/HalFormsSerializers.java | 78 ++++----- .../hateoas/hal/forms/HalFormsTemplate.java | 83 ++++++--- .../hal/forms/Jackson2HalFormsModule.java | 83 ++++++--- .../hal/forms/PagedResourcesMixin.java | 37 ---- .../hateoas/hal/forms/ResourceMixin.java | 28 --- .../hateoas/hal/forms/ResourcesMixin.java | 45 ----- .../hateoas/mvc/ControllerLinkBuilder.java | 56 +++--- .../hateoas/mvc/JacksonSerializers.java | 59 +++++++ .../hateoas/mvc/SpringMvcAffordance.java | 61 ++++--- .../mvc/SpringMvcAffordanceBuilder.java | 37 ++-- src/main/resources/META-INF/spring.factories | 1 + ...actJackson2MarshallingIntegrationTest.java | 2 - .../springframework/hateoas/LinkUnitTest.java | 43 +++-- .../hateoas/hal/forms/EmployeeResource.java | 2 + ... => HalFormsMessageConverterUnitTest.java} | 42 ++--- ...=> HalFormsValidationIntegrationTest.java} | 81 ++++----- ...ava => HalFormsWebMvcIntegrationTest.java} | 130 ++++++-------- .../Jackson2HalFormsIntegrationTest.java | 101 ++++++----- ...> SpringMvcAffordanceBuilderUnitTest.java} | 32 ++-- .../hateoas/hal/forms/curied-document.json | 3 +- .../hateoas/hal/forms/empty-document.json | 4 +- .../hal/forms/multiple-curies-document.json | 3 +- .../hal/forms/single-non-curie-document.json | 3 +- 41 files changed, 836 insertions(+), 732 deletions(-) create mode 100644 lombok.config rename src/main/java/org/springframework/hateoas/{ => core}/AffordanceModelFactory.java (65%) delete mode 100644 src/main/java/org/springframework/hateoas/hal/forms/PagedResourcesMixin.java delete mode 100644 src/main/java/org/springframework/hateoas/hal/forms/ResourceMixin.java delete mode 100644 src/main/java/org/springframework/hateoas/hal/forms/ResourcesMixin.java create mode 100644 src/main/java/org/springframework/hateoas/mvc/JacksonSerializers.java create mode 100644 src/main/resources/META-INF/spring.factories rename src/test/java/org/springframework/hateoas/hal/forms/{HalFormsMessageConverterTest.java => HalFormsMessageConverterUnitTest.java} (79%) rename src/test/java/org/springframework/hateoas/hal/forms/{HalFormsValidationTest.java => HalFormsValidationIntegrationTest.java} (75%) rename src/test/java/org/springframework/hateoas/hal/forms/{HalFormsWebMvcTest.java => HalFormsWebMvcIntegrationTest.java} (57%) rename src/test/java/org/springframework/hateoas/mvc/{SpringMvcAffordanceBuilderUnitTests.java => SpringMvcAffordanceBuilderUnitTest.java} (74%) diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..5bad9682 --- /dev/null +++ b/lombok.config @@ -0,0 +1,3 @@ +lombok.anyConstructor.suppressConstructorProperties=true +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.log.fieldName = LOG diff --git a/src/main/java/org/springframework/hateoas/Affordance.java b/src/main/java/org/springframework/hateoas/Affordance.java index 63745688..ea550516 100644 --- a/src/main/java/org/springframework/hateoas/Affordance.java +++ b/src/main/java/org/springframework/hateoas/Affordance.java @@ -15,12 +15,14 @@ */ package org.springframework.hateoas; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; /** * Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation. * * @author Greg Turnquist + * @author Oliver Gierke */ public interface Affordance { @@ -29,7 +31,7 @@ public interface Affordance { * * @return */ - String getHttpMethod(); + HttpMethod getHttpMethod(); /** * Name for the REST action this {@link Affordance} can take. @@ -44,14 +46,5 @@ public interface Affordance { * @param mediaType * @return */ - AffordanceModel getAffordanceModel(MediaType mediaType); - - /** - * Add a new {@link AffordanceModel} for a given {@link MediaType}. - * - * @param mediaType - * @param affordanceModel - */ - void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel); - + T getAffordanceModel(MediaType mediaType); } diff --git a/src/main/java/org/springframework/hateoas/AffordanceModel.java b/src/main/java/org/springframework/hateoas/AffordanceModel.java index 16f730c6..997a7ab1 100644 --- a/src/main/java/org/springframework/hateoas/AffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/AffordanceModel.java @@ -15,11 +15,23 @@ */ package org.springframework.hateoas; +import java.util.Collection; + +import org.springframework.http.MediaType; + /** - * Marker interface for mediatypes to build up type-specific details for an {@link Affordance} + * An affordance model is a media type specific description of an affordance. * * @author Greg Turnquist + * @author Oliver Gierke */ public interface AffordanceModel { + /** + * The media types this is a model for. Can be multiple ones as often media types come in different flavors like an + * XML and JSON one and in simple cases a single model might serve them all. + * + * @return will never be {@literal null}. + */ + Collection getMediaTypes(); } diff --git a/src/main/java/org/springframework/hateoas/Link.java b/src/main/java/org/springframework/hateoas/Link.java index 1ecb000e..6f721032 100755 --- a/src/main/java/org/springframework/hateoas/Link.java +++ b/src/main/java/org/springframework/hateoas/Link.java @@ -34,12 +34,12 @@ import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; -import org.springframework.hateoas.core.LinkBuilderSupport; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; /** * Value object for links. @@ -48,6 +48,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; * @author Greg Turnquist */ @XmlType(name = "link", namespace = Link.ATOM_NAMESPACE) +@JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties("templated") @AllArgsConstructor(access = AccessLevel.PACKAGE) @Getter @@ -113,11 +114,11 @@ public class Link implements Serializable { } public Link(String href, String rel, List affordances) { - + this(href, rel); Assert.notNull(affordances, "affordances must not be null!"); - + this.affordances = affordances; } @@ -134,7 +135,7 @@ public class Link implements Serializable { * @return */ public List getAffordances() { - return new ArrayList(Collections.unmodifiableCollection(this.affordances)); + return Collections.unmodifiableList(this.affordances); } /** @@ -149,35 +150,46 @@ public class Link implements Serializable { /** * Create new {@link Link} with an additional {@link Affordance}. * - * @param affordance + * @param affordance must not be {@literal null}. * @return */ - public Link withAffordance(Affordance affordance) { + public Link andAffordance(Affordance affordance) { + + Assert.notNull(affordance, "Affordance must not be null!"); List newAffordances = new ArrayList(); newAffordances.addAll(this.affordances); newAffordances.add(affordance); - return new Link(this.rel, this.href, this.hreflang ,this.media, this.title, this.type, - this.deprecation, this.template, newAffordances); + return withAffordances(newAffordances); } /** * Create new {@link Link} with additional {@link Affordance}s. * - * @param affordances + * @param affordances must not be {@literal null}. * @return */ - public Link addAffordances(List affordances) { + public Link andAffordances(List affordances) { List newAffordances = new ArrayList(); newAffordances.addAll(this.affordances); newAffordances.addAll(affordances); - return new Link(this.rel, this.href, this.hreflang ,this.media, this.title, this.type, - this.deprecation, this.template, newAffordances); + return withAffordances(newAffordances); } + /** + * Creats a new {@link Link} with the given {@link Affordance}s. + * + * @param affordances must not be {@literal null}. + * @return + */ + public Link withAffordances(List affordances) { + + return new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation, + this.template, affordances); + } /** * Returns the variable names contained in the template. diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index 11ce2144..6bc59818 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -15,6 +15,8 @@ */ package org.springframework.hateoas.config; +import lombok.extern.slf4j.Slf4j; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -25,8 +27,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import lombok.extern.slf4j.Slf4j; - import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; @@ -54,7 +54,7 @@ import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer; @Target(ElementType.TYPE) @Documented @Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class, - EnableHypermediaSupport.HypermediaConfigurationImportSelector.class}) + EnableHypermediaSupport.HypermediaConfigurationImportSelector.class }) public @interface EnableHypermediaSupport { /** @@ -68,6 +68,7 @@ public @interface EnableHypermediaSupport { * Hypermedia representation types supported. * * @author Oliver Gierke + * @author Greg Turnquist */ enum HypermediaType { @@ -81,6 +82,7 @@ public @interface EnableHypermediaSupport { /** * HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support + * * @see https://rwcbook.github.io/hal-forms/ */ HAL_FORMS(HalFormsWebMvcConfigurer.class); @@ -109,8 +111,8 @@ public @interface EnableHypermediaSupport { types = HypermediaType.values(); } - log.debug("Registering support for hypermedia types {} according to configuration on {}", - types, metadata.getClassName()); + LOG.debug("Registering support for hypermedia types {} according to configuration on {}", types, + metadata.getClassName()); List configurationNames = new ArrayList(); diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 0cdf51a9..f4009636 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -86,7 +86,6 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider"; private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry"; - private static final String AFFORDANCE_MODEL_FACTORY_REGISTRY_BEAN_NAME = "_affordanceModelFactoryRegistry"; private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper"; private static final String HAL_FORMS_OBJECT_MAPPER_BEAN_NAME = "_halFormsObjectMapper"; private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource"; @@ -146,7 +145,8 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe registerRelProviderPluginRegistryAndDelegate(registry); } - private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, String objectMapperBeanName) { + private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, + String objectMapperBeanName) { if (JACKSON2_PRESENT) { @@ -316,7 +316,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class); MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); + MessageSourceAccessor.class); halObjectMapper.registerModule(new Jackson2HalModule()); @@ -340,7 +340,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class); MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); + MessageSourceAccessor.class); halFormsObjectMapper.registerModule(new Jackson2HalFormsModule()); @@ -354,7 +354,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe } MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class); + ResourceSupport.class); halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON)); halFormsConverter.setObjectMapper(halFormsObjectMapper); result.add(halFormsConverter); diff --git a/src/main/java/org/springframework/hateoas/AffordanceModelFactory.java b/src/main/java/org/springframework/hateoas/core/AffordanceModelFactory.java similarity index 65% rename from src/main/java/org/springframework/hateoas/AffordanceModelFactory.java rename to src/main/java/org/springframework/hateoas/core/AffordanceModelFactory.java index 28a95d84..efacf419 100644 --- a/src/main/java/org/springframework/hateoas/AffordanceModelFactory.java +++ b/src/main/java/org/springframework/hateoas/core/AffordanceModelFactory.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.hateoas; +package org.springframework.hateoas.core; +import org.springframework.hateoas.Affordance; +import org.springframework.hateoas.AffordanceModel; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.http.MediaType; import org.springframework.plugin.core.Plugin; @@ -24,15 +26,9 @@ import org.springframework.web.util.UriComponents; * TODO: Replace this with an interface and a default implementation of {@link #supports(MediaType)} in Java 8. * * @author Greg Turnquist + * @author Oliver Gierke */ -public abstract class AffordanceModelFactory implements Plugin { - - /** - * Look up the {@link MediaType} of this factory. - * - * @return - */ - abstract public MediaType getMediaType(); +public interface AffordanceModelFactory extends Plugin { /** * Look up the {@link AffordanceModel} for this factory. @@ -42,16 +38,5 @@ public abstract class AffordanceModelFactory implements Plugin { * @param components * @return */ - abstract public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components); - - /** - * Find factories based on {@link MediaType}. - * - * @param delimiter - * @return - */ - @Override - public boolean supports(MediaType delimiter) { - return delimiter != null && delimiter.equals(this.getMediaType()); - } + AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components); } diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java index 3d56667f..5e457b51 100644 --- a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java @@ -21,9 +21,11 @@ import static org.springframework.core.annotation.AnnotationUtils.*; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.regex.Pattern; +import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMethod; @@ -111,15 +113,15 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { } /** - * Extract {@link org.springframework.web.bind.annotation.RequestMapping}'s list of {@link RequestMethod}s - * into an array of {@link String}s. + * Extract {@link org.springframework.web.bind.annotation.RequestMapping}'s list of {@link RequestMethod}s into an + * array of {@link String}s. * * @param type * @param method * @return */ @Override - public String[] getRequestType(Class type, Method method) { + public Collection getRequestMethod(Class type, Method method) { Assert.notNull(type, "Type must not be null!"); Assert.notNull(method, "Method must not be null!"); @@ -129,13 +131,13 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { RequestMethod[] requestMethods = (RequestMethod[]) value; - List requestMethodNames = new ArrayList(); + List requestMethodNames = new ArrayList(); for (RequestMethod requestMethod : requestMethods) { - requestMethodNames.add(requestMethod.toString()); + requestMethodNames.add(HttpMethod.valueOf(requestMethod.toString())); } - return requestMethodNames.toArray(new String[]{}); + return requestMethodNames; } private String[] getMappingFrom(Annotation annotation) { diff --git a/src/main/java/org/springframework/hateoas/core/LinkBuilderSupport.java b/src/main/java/org/springframework/hateoas/core/LinkBuilderSupport.java index 47a44482..7eb32f4f 100644 --- a/src/main/java/org/springframework/hateoas/core/LinkBuilderSupport.java +++ b/src/main/java/org/springframework/hateoas/core/LinkBuilderSupport.java @@ -23,6 +23,7 @@ import lombok.Getter; import java.net.URI; import java.util.Optional; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.springframework.hateoas.Affordance; @@ -143,10 +144,10 @@ public abstract class LinkBuilderSupport implements LinkB return uriComponents.encode().toUri().normalize(); } - public LinkBuilderSupport addAffordances(List affordances) { + public T addAffordances(Collection affordances) { this.affordances.addAll(affordances); - return this; + return getThis(); } /* @@ -154,14 +155,7 @@ public abstract class LinkBuilderSupport implements LinkB * @see org.springframework.hateoas.LinkBuilder#withRel(java.lang.String) */ public Link withRel(String rel) { - - Link link = new Link(toString(), rel); - - for (Affordance affordance : this.affordances) { - link = link.withAffordance(affordance); - } - - return link; + return new Link(toString(), rel).withAffordances(affordances); } /* diff --git a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java index 2b6971bd..b058c7a2 100644 --- a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java @@ -16,10 +16,13 @@ package org.springframework.hateoas.core; import java.lang.reflect.Method; +import java.util.Collection; + +import org.springframework.http.HttpMethod; /** - * Strategy interface to discover a URI mapping and related {@link org.springframework.hateoas.Affordance}s - * for either a given type or method. + * Strategy interface to discover a URI mapping and related {@link org.springframework.hateoas.Affordance}s for either a + * given type or method. * * @author Oliver Gierke * @author Greg Turnquist @@ -53,12 +56,12 @@ public interface MappingDiscoverer { String getMapping(Class type, Method method); /** - * Returns the HTTP verbs for the given {@link Method} invoked on the given type. This can be used to build - * hypermedia templates. + * Returns the HTTP verbs for the given {@link Method} invoked on the given type. This can be used to build hypermedia + * templates. * * @param type * @param method * @return */ - String[] getRequestType(Class type, Method method); + Collection getRequestMethod(Class type, Method method); } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index bf17563a..1f7af5a7 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -207,9 +207,8 @@ public class Jackson2HalModule extends SimpleModule { JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class); JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType); - MapSerializer serializer = MapSerializer.construct(Collections.emptySet(), mapType, true, null, - provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration), - null); + MapSerializer serializer = MapSerializer.construct(Collections. emptySet(), mapType, true, null, + provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration), null); serializer.serialize(sortedLinks, jgen, provider); } @@ -402,14 +401,6 @@ public class Jackson2HalModule extends SimpleModule { private final Map, JsonSerializer> serializers; private final HalConfiguration halConfiguration; - public OptionalListJackson2Serializer() { - this(null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE)); - } - - public OptionalListJackson2Serializer(BeanProperty property) { - this(property, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE)); - } - /** * Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}. * @@ -527,7 +518,7 @@ public class Jackson2HalModule extends SimpleModule { @Override public JsonSerializer createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { - return new OptionalListJackson2Serializer(property); + return new OptionalListJackson2Serializer(property, halConfiguration); } } @@ -573,7 +564,7 @@ public class Jackson2HalModule extends SimpleModule { while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { - throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation()); + throw new JsonParseException(jp, "Expected relation name"); } // save the relation in case the link does not contain it @@ -649,7 +640,7 @@ public class Jackson2HalModule extends SimpleModule { while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { - throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation()); + throw new JsonParseException(jp, "Expected relation name"); } if (JsonToken.START_ARRAY.equals(jp.nextToken())) { diff --git a/src/main/java/org/springframework/hateoas/hal/LinkMixin.java b/src/main/java/org/springframework/hateoas/hal/LinkMixin.java index 4f009be1..b6e46235 100644 --- a/src/main/java/org/springframework/hateoas/hal/LinkMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/LinkMixin.java @@ -30,7 +30,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; * @author Oliver Gierke * @author Greg Turnquist */ -@JsonIgnoreProperties({"rel", "media"}) +@JsonIgnoreProperties({ "rel", "media", "affordances", "template" }) public abstract class LinkMixin extends Link { private static final long serialVersionUID = 4720588561299667409L; @@ -66,7 +66,7 @@ public abstract class LinkMixin extends Link { @Override @JsonInclude(Include.NON_NULL) public abstract String getDeprecation(); - + /* * (non-Javadoc) * @see org.springframework.hateoas.Link#isTemplate() diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModel.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModel.java index 3155de5c..1160f982 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModel.java @@ -15,20 +15,29 @@ */ package org.springframework.hateoas.hal.forms; +import lombok.extern.slf4j.Slf4j; + import java.beans.PropertyDescriptor; -import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.TreeMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; +import org.springframework.core.MethodParameter; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.AffordanceModel; +import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; +import org.springframework.hateoas.core.MethodParameters; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.util.UriComponents; @@ -37,42 +46,28 @@ import org.springframework.web.util.UriComponents; * * @author Greg Turnquist */ -public class HalFormsAffordanceModel implements AffordanceModel { +@Slf4j +class HalFormsAffordanceModel implements AffordanceModel { - private static final Logger log = LoggerFactory.getLogger(HalFormsAffordanceModel.class); + private static final List METHODS_FOR_INPUT_DETECTTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT, + HttpMethod.PATCH); - /** - * Details about the affordance's - */ private final UriComponents components; - - /** - * Is this required/not required? - */ private final boolean required; - - /** - * {@link Map} of property names and their types associated with the incoming request body. - */ private final Map> properties; public HalFormsAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) { this.components = components; this.required = determineRequired(affordance.getHttpMethod()); - - this.properties = new TreeMap>(); - - if (affordance.getHttpMethod().equalsIgnoreCase("POST") || - affordance.getHttpMethod().equalsIgnoreCase("PUT") || - affordance.getHttpMethod().equalsIgnoreCase("PATCH")) { - - determineAffordanceInputs(invocationValue.getMethod()); - } + this.properties = METHODS_FOR_INPUT_DETECTTION.contains(affordance.getHttpMethod()) // + ? determineAffordanceInputs(invocationValue.getMethod()) // + : Collections.> emptyMap(); } /** - * Transform the details of the Spring MVC method's {@link RequestBody} into a collection of {@link HalFormsProperty}s. + * Transform the details of the Spring MVC method's {@link RequestBody} into a collection of + * {@link HalFormsProperty}s. * * @return */ @@ -80,20 +75,42 @@ public class HalFormsAffordanceModel implements AffordanceModel { List halFormsProperties = new ArrayList(); - for (Map.Entry> entry : this.properties.entrySet()) { - halFormsProperties.add(new HalFormsProperty(entry.getKey(), null, null, null, null, false, this.required, false)); + for (Entry> entry : this.properties.entrySet()) { + + HalFormsProperty property = HalFormsProperty// + .named(entry.getKey())// + .withRequired(required); + + halFormsProperties.add(property); } return halFormsProperties; } + public String getPath() { + return components.getPath(); + } + /** - * Look up the path of the {@link UriComponents}. + * Returns whether the affordance is pointing to the same path as the given one. * + * @param path must not be {@literal null}. * @return */ - public String getPath() { - return this.components.getPath(); + public boolean hasPath(String path) { + + Assert.notNull(path, "Path must not be null!"); + + return getPath().equals(path); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModel#getMediaType() + */ + @Override + public Collection getMediaTypes() { + return Collections.singleton(MediaTypes.HAL_FORMS_JSON); } /** @@ -102,48 +119,45 @@ public class HalFormsAffordanceModel implements AffordanceModel { * @param httpMethod - string representation of an HTTP method, e.g. GET, POST, etc. * @return */ - private boolean determineRequired(String httpMethod) { - - if (httpMethod.equalsIgnoreCase("POST") || httpMethod.equalsIgnoreCase("PUT")) { - return true; - } else { - return false; - } + private boolean determineRequired(HttpMethod httpMethod) { + return Arrays.asList(HttpMethod.POST, HttpMethod.PUT).contains(httpMethod); } - + /** * Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties. * * @param method - {@link Method} of the Spring MVC controller tied to this affordance */ - private void determineAffordanceInputs(Method method) { + private Map> determineAffordanceInputs(Method method) { if (method == null) { - return; + return Collections.emptyMap(); } - log.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName()); + LOG.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName()); - for (int i = 0; i < method.getParameterTypes().length; i++) { + Map> properties = new TreeMap>(); + MethodParameters parameters = new MethodParameters(method); - for (Annotation annotation : method.getParameterAnnotations()[i]) { + for (MethodParameter parameter : parameters.getParametersWith(RequestBody.class)) { - if (annotation.annotationType().equals(RequestBody.class)) { + Class parameterType = parameter.getParameterType(); - log.debug("\tRequest body: " + method.getParameterTypes()[i].getCanonicalName() + "("); + LOG.debug("\tRequest body: " + parameterType.getCanonicalName() + "("); - for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(method.getParameterTypes()[i])) { + for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(parameterType)) { - if (!descriptor.getName().equals("class")) { - log.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName()); - this.properties.put(descriptor.getName(), descriptor.getPropertyType()); - } - } - log.debug(")"); + if (!descriptor.getName().equals("class")) { + + LOG.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName()); + properties.put(descriptor.getName(), descriptor.getPropertyType()); } } + + LOG.debug(")"); } - log.debug("Assembled " + this.toString()); + LOG.debug("Assembled " + this.toString()); + return properties; } } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModelFactory.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModelFactory.java index 02dc1605..2c014164 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModelFactory.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsAffordanceModelFactory.java @@ -15,12 +15,10 @@ */ package org.springframework.hateoas.hal.forms; -import lombok.Getter; - import org.springframework.hateoas.Affordance; import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.AffordanceModelFactory; import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.core.AffordanceModelFactory; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.http.MediaType; import org.springframework.web.util.UriComponents; @@ -29,14 +27,26 @@ import org.springframework.web.util.UriComponents; * Factory for creating {@link HalFormsAffordanceModel}s. * * @author Greg Turnquist + * @author Oliver Gierke */ -@Getter -public class HalFormsAffordanceModelFactory extends AffordanceModelFactory { +class HalFormsAffordanceModelFactory implements AffordanceModelFactory { - private final MediaType mediaType = MediaTypes.HAL_FORMS_JSON; - + /* + * (non-Javadoc) + * @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.Affordance, org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation, org.springframework.web.util.UriComponents) + */ @Override - public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) { + public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, + UriComponents components) { return new HalFormsAffordanceModel(affordance, invocationValue, components); } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(MediaType mediaType) { + return MediaTypes.HAL_FORMS_JSON.equals(mediaType); + } } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDeserializers.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDeserializers.java index 6bce25f1..59f1f377 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDeserializers.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDeserializers.java @@ -38,9 +38,12 @@ import com.fasterxml.jackson.databind.type.TypeFactory; * * @author Greg Turnquist */ -public class HalFormsDeserializers { +class HalFormsDeserializers { - static class HalFormsResourcesDeserializer extends ContainerDeserializerBase> implements ContextualDeserializer { + static class HalFormsResourcesDeserializer extends ContainerDeserializerBase> + implements ContextualDeserializer { + + private static final long serialVersionUID = -7325599536381465624L; private JavaType contentType; @@ -65,7 +68,7 @@ public class HalFormsDeserializers { while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { - throw new JsonParseException("Expected relation name", jp.getCurrentLocation()); + throw new JsonParseException(jp, "Expected relation name"); } if (JsonToken.START_ARRAY.equals(jp.nextToken())) { @@ -93,7 +96,8 @@ public class HalFormsDeserializers { } @Override - public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { if (property != null) { JavaType vc = property.getType().getContentType(); diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDocument.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDocument.java index f0f42392..fa8492be 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDocument.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsDocument.java @@ -15,24 +15,28 @@ */ package org.springframework.hateoas.hal.forms; -import static com.fasterxml.jackson.annotation.JsonInclude.*; -import static org.springframework.hateoas.hal.Jackson2HalModule.*; - -import lombok.Builder; -import lombok.Data; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; import lombok.Singular; +import lombok.Value; +import lombok.experimental.Wither; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer; +import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer; +import org.springframework.util.Assert; 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; @@ -44,69 +48,154 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; * * @author Dietrich Schulten * @author Greg Turnquist + * @author Oliver Gierke */ -@Data -@Builder(builderMethodName = "halFormsDocument") +@Value +@Wither +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) @JsonPropertyOrder({ "resource", "resources", "embedded", "links", "templates", "metadata" }) public class HalFormsDocument { - @JsonUnwrapped - @JsonInclude(Include.NON_NULL) + @JsonUnwrapped // + @JsonInclude(Include.NON_NULL) // + @Wither(AccessLevel.PRIVATE) // private T resource; - @JsonIgnore @JsonInclude(Include.NON_EMPTY) + @JsonIgnore // + @Wither(AccessLevel.PRIVATE) // private Collection resources; - @JsonProperty("_embedded") - @JsonInclude(Include.NON_NULL) + @JsonProperty("_embedded") // + @JsonInclude(Include.NON_EMPTY) // private Map embedded; - @JsonProperty("page") - @JsonInclude(Include.NON_NULL) + @JsonProperty("page") // + @JsonInclude(Include.NON_NULL) // private PagedResources.PageMetadata pageMetadata; - @Singular private List links; + @Singular // + @JsonProperty("_links") // + @JsonInclude(Include.NON_EMPTY) // + @JsonSerialize(using = HalLinkListSerializer.class) // + @JsonDeserialize(using = HalLinkListDeserializer.class) // + private List links; - @Singular private Map templates; + @Singular // + @JsonProperty("_templates") // + @JsonInclude(Include.NON_EMPTY) // + private Map templates; - HalFormsDocument(T resource, Collection resources, Map embedded, - PagedResources.PageMetadata pageMetadata, List links, Map templates) { - - this.resource = resource; - this.resources = resources; - this.embedded = embedded; - this.pageMetadata = pageMetadata; - this.links = links; - this.templates = templates; + private HalFormsDocument() { + this(null, null, Collections. emptyMap(), null, Collections. emptyList(), + Collections. emptyMap()); } - HalFormsDocument() { - this(null, null, null, null, new ArrayList(), new HashMap()); + /** + * Creates a new {@link HalFormsDocument} for the given resource. + * + * @param resource can be {@literal null}. + * @return + */ + public static HalFormsDocument forResource(T resource) { + return new HalFormsDocument().withResource(resource); } - @JsonProperty("_links") - @JsonInclude(Include.NON_EMPTY) - @JsonSerialize(using = HalLinkListSerializer.class) - @JsonDeserialize(using = HalLinkListDeserializer.class) - public List getLinks() { - return this.links; + /** + * returns a new {@link HalFormsDocument} for the given resources. + * + * @param resources must not be {@literal null}. + * @return + */ + public static HalFormsDocument forResources(Collection resources) { + + Assert.notNull(resources, "Resources must not be null!"); + + return new HalFormsDocument().withResources(resources); } - @JsonProperty("_templates") - @JsonInclude(Include.NON_EMPTY) - public Map getTemplates() { - return this.templates; + /** + * Creates a new empty {@link HalFormsDocument}. + * + * @return + */ + public static HalFormsDocument empty() { + return new HalFormsDocument(); } + /** + * Returns the default template of the document. + * + * @return + */ @JsonIgnore - public HalFormsTemplate getTemplate() { + public HalFormsTemplate getDefaultTemplate() { return getTemplate(HalFormsTemplate.DEFAULT_KEY); } + /** + * Returns the template with the given name. + * + * @param key must not be {@literal null}. + * @return + */ @JsonIgnore public HalFormsTemplate getTemplate(String key) { + + Assert.notNull(key, "Template key must not be null!"); + return this.templates.get(key); } + /** + * Adds the given {@link Link} to the current document. + * + * @param link must not be {@literal null}. + * @return + */ + public HalFormsDocument andLink(Link link) { + + Assert.notNull(link, "Link must not be null!"); + + List links = new ArrayList(this.links); + links.add(link); + + return new HalFormsDocument(resource, resources, embedded, pageMetadata, links, templates); + } + + /** + * Adds the given {@link HalFormsTemplate} to the current document. + * + * @param name must not be {@literal null} or empty. + * @param template must not be {@literal null}. + * @return + */ + public 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(resource, resources, embedded, pageMetadata, links, templates); + } + + /** + * Adds the given value as embedded one. + * + * @param key must not be {@literal null} or empty. + * @param value must not be {@literal null}. + * @return + */ + public HalFormsDocument andEmbedded(String 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(resource, resources, embedded, pageMetadata, links, templates); + } } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java index 64358777..f0c03a60 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java @@ -32,14 +32,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; /** - * A message converter that converts any object into a HAL-FORMS document before bundling up - * as an {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into - * an object. + * A message converter that converts any object into a HAL-FORMS document before bundling up as an + * {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into an object. * * @author Dietrich Schulten * @author Greg Turnquist */ -public class HalFormsMessageConverter extends AbstractHttpMessageConverter { +class HalFormsMessageConverter extends AbstractHttpMessageConverter { private final ObjectMapper objectMapper; @@ -47,6 +46,7 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter clazz) { + protected boolean supports(Class clazz) { return true; } @@ -64,14 +64,14 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter clazz, final HttpInputMessage inputMessage) + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return this.objectMapper.readValue(inputMessage.getBody(), clazz); } @Override - protected void writeInternal(final Object t, final HttpOutputMessage outputMessage) + protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8); @@ -88,5 +88,4 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter> implements ContextualSerializer { + private static final long serialVersionUID = -7912243216469101379L; + private final BeanProperty property; HalFormsResourceSerializer(BeanProperty property) { @@ -70,15 +72,11 @@ public class HalFormsSerializers { @Override public void serialize(Resource value, JsonGenerator gen, SerializerProvider provider) throws IOException { - HalFormsDocument doc = HalFormsDocument. halFormsDocument() - .resource(value.getContent()) - .links(value.getLinks()) - .templates(findTemplates(value)) - .build(); + HalFormsDocument doc = HalFormsDocument.forResource(value.getContent()) // + .withLinks(value.getLinks()) // + .withTemplates(findTemplates(value)); - provider - .findValueSerializer(HalFormsDocument.class, property) - .serialize(doc, gen, provider); + provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider); } @Override @@ -102,7 +100,8 @@ public class HalFormsSerializers { } @Override - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { return new HalFormsResourceSerializer(property); } } @@ -112,12 +111,15 @@ public class HalFormsSerializers { */ static class HalFormsResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { + private static final long serialVersionUID = -3601146866067500734L; + private final BeanProperty property; private final Jackson2HalModule.EmbeddedMapper embeddedMapper; HalFormsResourcesSerializer(BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) { super(Resources.class, false); + this.property = property; this.embeddedMapper = embeddedMapper; } @@ -135,25 +137,21 @@ public class HalFormsSerializers { if (value instanceof PagedResources) { - doc = HalFormsDocument. halFormsDocument() - .embedded(embeddeds) - .pageMetadata(((PagedResources) value).getMetadata()) - .links(value.getLinks()) - .templates(findTemplates(value)) - .build(); + doc = HalFormsDocument.empty() // + .withEmbedded(embeddeds) // + .withPageMetadata(((PagedResources) value).getMetadata()) // + .withLinks(value.getLinks()) // + .withTemplates(findTemplates(value)); + } else { - doc = HalFormsDocument. halFormsDocument() - .embedded(embeddeds) - .pageMetadata(null) - .links(value.getLinks()) - .templates(findTemplates(value)) - .build(); + doc = HalFormsDocument.empty() // + .withEmbedded(embeddeds) // + .withLinks(value.getLinks()) // + .withTemplates(findTemplates(value)); } - provider - .findValueSerializer(HalFormsDocument.class, property) - .serialize(doc, gen, provider); + provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider); } @Override @@ -177,7 +175,8 @@ public class HalFormsSerializers { } @Override - public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { + public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) + throws JsonMappingException { return new HalFormsResourcesSerializer(property, embeddedMapper); } } @@ -195,25 +194,19 @@ public class HalFormsSerializers { if (resource.hasLink(Link.REL_SELF)) { for (Affordance affordance : resource.getLink(Link.REL_SELF).map(Link::getAffordances).orElse(Collections.emptyList())) { - HalFormsAffordanceModel model = - (HalFormsAffordanceModel) affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON); + HalFormsAffordanceModel model = affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON); - if (!affordance.getHttpMethod().equals(HttpMethod.GET.toString())) { + if (!affordance.getHttpMethod().equals(HttpMethod.GET)) { validate(resource, affordance, model); - HalFormsTemplate template = new HalFormsTemplate(); - template.setHttpMethod(HttpMethod.valueOf(affordance.getHttpMethod())); - template.setProperties(model.getProperties()); + HalFormsTemplate template = HalFormsTemplate.forMethod(affordance.getHttpMethod()) // + .withProperties(model.getProperties()); /** * First template in HAL-FORMS is "default". */ - if (templates.isEmpty()) { - templates.put("default", template); - } else { - templates.put(affordance.getName(), template); - } + templates.put(templates.isEmpty() ? "default" : affordance.getName(), template); } } } @@ -223,7 +216,8 @@ public class HalFormsSerializers { /** * Verify that the resource's self link and the affordance's URI have the same relative path. - * @param resource + * + * @param resource * @param affordance * @param model */ @@ -231,15 +225,15 @@ public class HalFormsSerializers { try { Optional selfLink = resource.getLink(Link.REL_SELF); - URI selfLinkUri = new URI(selfLink.map(link -> link.expand().getHref()).orElse("")); - if (!selfLinkUri.getPath().equals(model.getPath())) { - throw new IllegalStateException("Affordance's URI " + model.getPath() + " doesn't match self link " + selfLinkUri.getPath() + " as expected in HAL-FORMS"); + if (!model.hasPath(selfLinkUri.getPath())) { + throw new IllegalStateException("Affordance's URI " + model.getPath() + " doesn't match self link " + + selfLinkUri.getPath() + " as expected in HAL-FORMS"); } + } catch (URISyntaxException e) { throw new RuntimeException(e); } } - } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsTemplate.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsTemplate.java index 3f3c4b1a..c630f628 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsTemplate.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsTemplate.java @@ -18,19 +18,25 @@ package org.springframework.hateoas.hal.forms; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Wither; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.springframework.hateoas.hal.forms.HalFormsDeserializers.MediaTypesDeserializer; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -43,50 +49,79 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; * @see https://rwcbook.github.io/hal-forms/#_the_code__templates_code_element */ @Data +@Setter(AccessLevel.NONE) +@Wither @AllArgsConstructor(access = AccessLevel.PRIVATE) -@JsonInclude(JsonInclude.Include.NON_DEFAULT) +@EqualsAndHashCode +@ToString +@JsonAutoDetect(getterVisibility = Visibility.NON_PRIVATE) +@JsonIgnoreProperties({ "httpMethod", "contentTypes" }) @JsonPropertyOrder({ "title", "method", "contentType", "properties" }) -@JsonIgnoreProperties({ "key" }) public class HalFormsTemplate { public static final String DEFAULT_KEY = "default"; - private @JsonIgnore String key; - private List properties = new ArrayList(); - private String title; - private @JsonIgnore HttpMethod httpMethod; - private List contentType; + private @Wither(AccessLevel.PRIVATE) HttpMethod httpMethod; + private List properties; + private List contentTypes; - /** - * Configure a HAL-FORMS template with a key value. - * @param key - */ - public HalFormsTemplate(String key) { - this.key = key; + private HalFormsTemplate() { + this(null, null, Collections. emptyList(), Collections. emptyList()); + } + + public static HalFormsTemplate forMethod(HttpMethod httpMethod) { + return new HalFormsTemplate().withHttpMethod(httpMethod); } /** - * A HAL-FORMS template with no name is dubbed the "default" template. + * Returns a new {@link HalFormsTemplate} with the given {@link HalFormsProperty} added. + * + * @param property must not be {@literal null}. + * @return */ - public HalFormsTemplate() { - this(HalFormsTemplate.DEFAULT_KEY); + public HalFormsTemplate andProperty(HalFormsProperty property) { + + Assert.notNull(property, "Property must not be null!"); + + ArrayList properties = new ArrayList(this.properties); + properties.add(property); + + return new HalFormsTemplate(title, httpMethod, properties, contentTypes); } - public String getContentType() { - return StringUtils.collectionToCommaDelimitedString(contentType); + /** + * Returns a new {@link HalFormsTemplate} with the given {@link MediaType} added as content type. + * + * @param mediaType must not be {@literal null}. + * @return + */ + public HalFormsTemplate andContentType(MediaType mediaType) { + + Assert.notNull(mediaType, "Media type must not be null!"); + + ArrayList contentTypes = new ArrayList(this.contentTypes); + contentTypes.add(mediaType); + + return new HalFormsTemplate(title, httpMethod, properties, contentTypes); + } + + // Jackson helper methods to create the right representation format + + String getContentType() { + return StringUtils.collectionToDelimitedString(contentTypes, ", "); } @JsonDeserialize(using = MediaTypesDeserializer.class) - public void setContentType(List contentType) { - this.contentType = contentType; + void setContentType(List mediaTypes) { + this.contentTypes = mediaTypes; } - public String getMethod() { + String getMethod() { return this.httpMethod == null ? null : this.httpMethod.toString().toLowerCase(); } - public void setMethod(String method) { + void setMethod(String method) { this.httpMethod = HttpMethod.valueOf(method.toUpperCase()); } } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsModule.java b/src/main/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsModule.java index 8cedb8b1..16a959c3 100644 --- a/src/main/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsModule.java +++ b/src/main/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsModule.java @@ -15,9 +15,12 @@ */ package org.springframework.hateoas.hal.forms; +import java.util.Collection; import java.util.HashMap; import java.util.Map; +import javax.xml.bind.annotation.XmlElement; + import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.hateoas.Link; @@ -32,25 +35,36 @@ import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer; import org.springframework.hateoas.hal.LinkMixin; import org.springframework.hateoas.hal.ResourceSupportMixin; +import org.springframework.hateoas.hal.forms.HalFormsDeserializers.HalFormsResourcesDeserializer; +import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourceSerializer; import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourcesSerializer; +import org.springframework.hateoas.mvc.JacksonSerializers.MediaTypeDeserializer; +import org.springframework.http.MediaType; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.Version; 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.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; 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.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; /** - * Serialize/Deserialize all the parts of HAL-FORMS documents using Jackson. + * Serialize / deserialize all the parts of HAL-FORMS documents using Jackson. * * @author Dietrich Schulten * @author Greg Turnquist + * @author Oliver Gierke */ public class Jackson2HalFormsModule extends SimpleModule { @@ -65,8 +79,37 @@ public class Jackson2HalFormsModule extends SimpleModule { setMixInAnnotation(Resource.class, ResourceMixin.class); setMixInAnnotation(Resources.class, ResourcesMixin.class); setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class); + setMixInAnnotation(MediaType.class, MediaTypeMixin.class); } + @JsonSerialize(using = HalFormsResourceSerializer.class) + static interface ResourceMixin {} + + @JsonSerialize(using = HalFormsResourcesSerializer.class) + abstract class ResourcesMixin extends Resources { + + @Override + @XmlElement(name = "embedded") + @JsonProperty("_embedded") + @JsonInclude(Include.NON_EMPTY) + @JsonDeserialize(using = HalFormsResourcesDeserializer.class) + public abstract Collection getContent(); + } + + abstract class PagedResourcesMixin extends PagedResources { + + @Override + @JsonProperty("page") + @JsonInclude(Include.NON_EMPTY) + public PageMetadata getMetadata() { + return super.getMetadata(); + } + } + + @JsonSerialize(using = ToStringSerializer.class) + @JsonDeserialize(using = MediaTypeDeserializer.class) + static interface MediaTypeMixin {} + /** * Create new HAL-FORMS serializers based on the context. */ @@ -99,41 +142,33 @@ public class Jackson2HalFormsModule extends SimpleModule { /* * (non-Javadoc) - * - * @see - * com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind. - * DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + * @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) { + Class deserClass) { Object jsonDeser = findInstance(deserClass); return jsonDeser != null ? (JsonDeserializer) jsonDeser - : super.deserializerInstance(config, annotated, deserClass); + : super.deserializerInstance(config, annotated, 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) + * @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) { + Class keyDeserClass) { Object keyDeser = findInstance(keyDeserClass); return keyDeser != null ? (KeyDeserializer) keyDeser - : super.keyDeserializerInstance(config, annotated, keyDeserClass); + : super.keyDeserializerInstance(config, annotated, 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) + * @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) { @@ -144,33 +179,27 @@ public class Jackson2HalFormsModule extends SimpleModule { /* * (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) + * @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) { + Class builderClass) { Object builder = findInstance(builderClass); return builder != null ? (TypeResolverBuilder) builder - : super.typeResolverBuilderInstance(config, annotated, builderClass); + : super.typeResolverBuilderInstance(config, annotated, 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) + * @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); + : super.typeIdResolverInstance(config, annotated, resolverClass); } } } diff --git a/src/main/java/org/springframework/hateoas/hal/forms/PagedResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/forms/PagedResourcesMixin.java deleted file mode 100644 index f3942b32..00000000 --- a/src/main/java/org/springframework/hateoas/hal/forms/PagedResourcesMixin.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.hal.forms; - -import org.springframework.hateoas.PagedResources; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Custom mixin to render {@link org.springframework.hateoas.PagedResources.PageMetadata} in HAL. - * - * @author Greg Turnquist - */ -abstract class PagedResourcesMixin extends PagedResources { - - @Override - @JsonProperty("page") - @JsonInclude(Include.NON_EMPTY) - public PageMetadata getMetadata() { - return super.getMetadata(); - } -} diff --git a/src/main/java/org/springframework/hateoas/hal/forms/ResourceMixin.java b/src/main/java/org/springframework/hateoas/hal/forms/ResourceMixin.java deleted file mode 100644 index 5b27bd3b..00000000 --- a/src/main/java/org/springframework/hateoas/hal/forms/ResourceMixin.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.hal.forms; - -import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourceSerializer; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -/** - * @author Greg Turnquist - */ -@JsonSerialize(using = HalFormsResourceSerializer.class) -abstract class ResourceMixin { - -} diff --git a/src/main/java/org/springframework/hateoas/hal/forms/ResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/forms/ResourcesMixin.java deleted file mode 100644 index 540d4017..00000000 --- a/src/main/java/org/springframework/hateoas/hal/forms/ResourcesMixin.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2017 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 - * - * http://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.hal.forms; - -import java.util.Collection; - -import javax.xml.bind.annotation.XmlElement; - -import org.springframework.hateoas.Resources; -import org.springframework.hateoas.hal.forms.HalFormsDeserializers.HalFormsResourcesDeserializer; -import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourcesSerializer; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -/** - * @author Greg Turnquist - */ -@JsonSerialize(using = HalFormsResourcesSerializer.class) -abstract class ResourcesMixin extends Resources { - - @Override - @XmlElement(name = "embedded") - @JsonProperty("_embedded") - @JsonInclude(Include.NON_EMPTY) - @JsonDeserialize(using = HalFormsResourcesDeserializer.class) - public abstract Collection getContent(); - -} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java index f9c003e9..0f5f1e60 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -22,24 +22,25 @@ import lombok.experimental.Delegate; import java.lang.reflect.Method; import java.net.URI; -import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.hateoas.Affordance; -import org.springframework.hateoas.AffordanceModelFactory; import org.springframework.hateoas.Link; import org.springframework.hateoas.TemplateVariables; +import org.springframework.hateoas.core.AffordanceModelFactory; import org.springframework.hateoas.core.AnnotationMappingDiscoverer; import org.springframework.hateoas.core.DummyInvocationUtils; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.hateoas.core.LinkBuilderSupport; import org.springframework.hateoas.core.MappingDiscoverer; -import org.springframework.hateoas.hal.forms.HalFormsAffordanceModelFactory; import org.springframework.http.MediaType; import org.springframework.plugin.core.OrderAwarePluginRegistry; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.web.bind.annotation.RequestMapping; @@ -70,6 +71,17 @@ public class ControllerLinkBuilder extends LinkBuilderSupport factories = SpringFactoriesLoader.loadFactories(AffordanceModelFactory.class, + ControllerLinkBuilder.class.getClassLoader()); + + PluginRegistry MODEL_FACTORIES = OrderAwarePluginRegistry + .create(factories); + AFFORDANCE_BUILDER = new SpringMvcAffordanceBuilder(MODEL_FACTORIES); + } private final TemplateVariables variables; @@ -102,24 +114,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport findAffordances(MethodInvocation invocation, UriComponents components) { - - OrderAwarePluginRegistry modelFactories = - OrderAwarePluginRegistry.create(Arrays.asList(new HalFormsAffordanceModelFactory())); - - SpringMvcAffordanceBuilder springMvcAffordanceBuilder = new SpringMvcAffordanceBuilder(modelFactories); - - return springMvcAffordanceBuilder.create(invocation, DISCOVERER, components); - } - /** * Creates a new {@link ControllerLinkBuilder} with a base of the mapping annotated to the given controller class. * @@ -222,12 +216,12 @@ public class ControllerLinkBuilder extends LinkBuilderSupport - * Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel(); - * findOneLink.withAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) + * Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel() + * .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))); * * * This takes a link and adds an {@link Affordance} based on another Spring MVC handler method. @@ -351,6 +345,18 @@ public class ControllerLinkBuilder extends LinkBuilderSupport findAffordances(MethodInvocation invocation, UriComponents components) { + return AFFORDANCE_BUILDER.create(invocation, DISCOVERER, components); + } + @RequiredArgsConstructor private static class CachingAnnotationMappingDiscoverer implements MappingDiscoverer { diff --git a/src/main/java/org/springframework/hateoas/mvc/JacksonSerializers.java b/src/main/java/org/springframework/hateoas/mvc/JacksonSerializers.java new file mode 100644 index 00000000..92f3f602 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/JacksonSerializers.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 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 + * + * http://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.mvc; + +import java.io.IOException; + +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Simple Jackson serializers and deserializers. + * + * @author Oliver Gierke + */ +public class JacksonSerializers { + + /** + * Custom {@link JsonDeserializer} for Spring's {@link MediaType} using the {@link MediaType#paparseMediaType(String)} + * method. + * + * @author Oliver Gierke + */ + public static class MediaTypeDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 391537719262033410L; + + public MediaTypeDeserializer() { + super(MediaType.class); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + */ + @Override + public MediaType deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + return MediaType.parseMediaType(p.getText()); + } + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordance.java b/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordance.java index b46a1955..4934c38b 100644 --- a/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordance.java +++ b/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordance.java @@ -15,16 +15,19 @@ */ package org.springframework.hateoas.mvc; -import lombok.Data; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.AffordanceModel; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMethod; /** @@ -32,50 +35,58 @@ import org.springframework.web.bind.annotation.RequestMethod; * * @author Greg Turnquist */ -@Data -public class SpringMvcAffordance implements Affordance { - - private static final Logger log = LoggerFactory.getLogger(SpringMvcAffordance.class); - - private final HashMap affordanceModels; +@Value +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +class SpringMvcAffordance implements Affordance { /** * Request method verb associated with the Spring MVC controller method. */ - private final RequestMethod requestMethod; + private final HttpMethod httpMethod; /** * Handle on the Spring MVC controller {@link Method}. */ private final Method method; + private final Map affordanceModels; /** * Construct a Spring MVC-based {@link Affordance} based on Spring MVC controller method and {@link RequestMethod}. */ - public SpringMvcAffordance(RequestMethod requestMethod, Method method) { - - this.requestMethod = requestMethod; - this.method = method; - this.affordanceModels = new HashMap(); - } - - @Override - public String getHttpMethod() { - return this.requestMethod.toString(); + public SpringMvcAffordance(HttpMethod httpMethod, Method method) { + this(httpMethod, method, new HashMap()); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Affordance#getName() + */ @Override public String getName() { return this.method.getName(); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Affordance#getAffordanceModel(org.springframework.http.MediaType) + */ @Override - public AffordanceModel getAffordanceModel(MediaType mediaType) { - return this.affordanceModels.get(mediaType); + @SuppressWarnings("unchecked") + public T getAffordanceModel(MediaType mediaType) { + return (T) this.affordanceModels.get(mediaType); } - @Override - public void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel) { - this.affordanceModels.put(mediaType, affordanceModel); + /** + * Adds the given {@link AffordanceModel} to the {@link Affordance}. + * + * @param affordanceModel must not be {@literal null}. + */ + public void addAffordanceModel(AffordanceModel affordanceModel) { + + Assert.notNull(affordanceModel, "Affordance model must not be null!"); + + for (MediaType mediaType : affordanceModel.getMediaTypes()) { + this.affordanceModels.put(mediaType, affordanceModel); + } } } diff --git a/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilder.java b/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilder.java index 6d1b3506..9984fc8f 100644 --- a/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilder.java @@ -15,18 +15,21 @@ */ package org.springframework.hateoas.mvc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.springframework.hateoas.Affordance; -import org.springframework.hateoas.AffordanceModelFactory; +import org.springframework.hateoas.core.AffordanceModelFactory; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.hateoas.core.MappingDiscoverer; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.plugin.core.PluginRegistry; -import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.util.UriComponents; /** @@ -34,41 +37,35 @@ import org.springframework.web.util.UriComponents; * * @author Greg Turnquist */ -public class SpringMvcAffordanceBuilder { +@RequiredArgsConstructor +class SpringMvcAffordanceBuilder { - private final PluginRegistry factories; - - public SpringMvcAffordanceBuilder(PluginRegistry factories) { - - Assert.notNull(factories, "Registry of LinkDiscoverer must not be null!"); - this.factories = factories; - } + private final @NonNull PluginRegistry factories; /** - * Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to - * create a set of {@link Affordance}s. + * Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create + * a set of {@link Affordance}s. * * @param invocation * @param discoverer * @param components * @return */ - public List create(MethodInvocation invocation, MappingDiscoverer discoverer, UriComponents components) { + public Collection create(MethodInvocation invocation, MappingDiscoverer discoverer, + UriComponents components) { Method method = invocation.getMethod(); - String[] httpMethods = discoverer.getRequestType(invocation.getTargetType(), method); - List affordances = new ArrayList(); - for (String requestMethod : httpMethods) { + for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), method)) { - SpringMvcAffordance springMvcAffordance = new SpringMvcAffordance(RequestMethod.valueOf(requestMethod), invocation.getMethod()); + SpringMvcAffordance affordance = new SpringMvcAffordance(requestMethod, invocation.getMethod()); for (AffordanceModelFactory factory : factories) { - springMvcAffordance.addAffordanceModel(factory.getMediaType(), factory.getAffordanceModel(springMvcAffordance, invocation, components)); + affordance.addAffordanceModel(factory.getAffordanceModel(affordance, invocation, components)); } - affordances.add(springMvcAffordance); + affordances.add(affordance); } return affordances; diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bd7be18b --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.hateoas.core.AffordanceModelFactory=org.springframework.hateoas.hal.forms.HalFormsAffordanceModelFactory diff --git a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java index 49723e6a..d5399cda 100755 --- a/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/AbstractJackson2MarshallingIntegrationTest.java @@ -20,7 +20,6 @@ import java.io.Writer; import org.junit.Before; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -37,7 +36,6 @@ public abstract class AbstractJackson2MarshallingIntegrationTest { @Before public void setUp() { mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } protected String write(Object object) throws Exception { diff --git a/src/test/java/org/springframework/hateoas/LinkUnitTest.java b/src/test/java/org/springframework/hateoas/LinkUnitTest.java index 6a04eb0d..47bc97b7 100755 --- a/src/test/java/org/springframework/hateoas/LinkUnitTest.java +++ b/src/test/java/org/springframework/hateoas/LinkUnitTest.java @@ -22,9 +22,8 @@ import java.io.ObjectOutputStream; import org.apache.commons.io.output.ByteArrayOutputStream; import org.junit.Test; - -import org.springframework.hateoas.mvc.SpringMvcAffordance; -import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; /** * Unit tests for {@link Link}. @@ -148,10 +147,7 @@ public class LinkUnitTest { @Test(expected = IllegalArgumentException.class) public void rejectsLinkWithoutAttributesAtAll() { - - Link link = Link.valueOf(""); - - System.out.println(link); + Link.valueOf(""); } @Test(expected = IllegalArgumentException.class) @@ -234,8 +230,8 @@ public class LinkUnitTest { public void linkWithAffordancesShouldWorkProperly() { Link originalLink = new Link("/foo"); - Link linkWithAffordance = originalLink.withAffordance(new TestSpringMvcAffordance()); - Link linkWithTwoAffordances = linkWithAffordance.withAffordance(new TestSpringMvcAffordance()); + Link linkWithAffordance = originalLink.andAffordance(new TestAffordance()); + Link linkWithTwoAffordances = linkWithAffordance.andAffordance(new TestAffordance()); assertThat(originalLink.getAffordances()).hasSize(0); assertThat(linkWithAffordance.getAffordances()).hasSize(1); @@ -272,10 +268,33 @@ public class LinkUnitTest { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> link.hasRel("")); } - static class TestSpringMvcAffordance extends SpringMvcAffordance { + static class TestAffordance implements Affordance { - TestSpringMvcAffordance() { - super(RequestMethod.PATCH, null); + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Affordance#getAffordanceModel(org.springframework.http.MediaType) + */ + @Override + public T getAffordanceModel(MediaType mediaType) { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Affordance#getHttpMethod() + */ + @Override + public HttpMethod getHttpMethod() { + return HttpMethod.PATCH; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Affordance#getName() + */ + @Override + public String getName() { + return null; } } } diff --git a/src/test/java/org/springframework/hateoas/hal/forms/EmployeeResource.java b/src/test/java/org/springframework/hateoas/hal/forms/EmployeeResource.java index dd60eb67..08766460 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/EmployeeResource.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/EmployeeResource.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.hal.forms; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import org.springframework.hateoas.ResourceSupport; @@ -24,6 +25,7 @@ import org.springframework.hateoas.ResourceSupport; * @author Greg Turnquist */ @Data +@EqualsAndHashCode(callSuper = true) @AllArgsConstructor class EmployeeResource extends ResourceSupport { diff --git a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterTest.java b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java similarity index 79% rename from src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterTest.java rename to src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java index 6ebe8e82..8c4c911e 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java @@ -19,17 +19,14 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasItems; import static org.junit.Assert.*; -import static org.springframework.hateoas.hal.forms.HalFormsDocument.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collections; import org.junit.Before; import org.junit.Test; - import org.springframework.core.io.ClassPathResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; @@ -44,7 +41,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Greg Turnquist */ -public class HalFormsMessageConverterTest { +public class HalFormsMessageConverterUnitTest { ObjectMapper mapper; HttpMessageConverter messageConverter; @@ -92,28 +89,30 @@ public class HalFormsMessageConverterTest { assertThat(halFormsDocument.getTemplates().size(), is(1)); assertThat(halFormsDocument.getTemplates().keySet(), hasItems("default")); assertThat(halFormsDocument.getTemplates().get("default").getContentType(), is("application/hal+json")); - assertThat(halFormsDocument.getTemplates().get("default").getKey(), is(HalFormsTemplate.DEFAULT_KEY)); assertThat(halFormsDocument.getTemplates().get("default").getHttpMethod(), is(HttpMethod.GET)); assertThat(halFormsDocument.getTemplates().get("default").getMethod(), is(HttpMethod.GET.toString().toLowerCase())); } @Test + @SuppressWarnings("rawtypes") public void canWriteAHalFormsDocumentMessage() throws IOException { - HalFormsProperty property = new HalFormsProperty("my-name", true, "my-value", "my-prompt", - "my-regex", false, true, false); - HalFormsTemplate template = new HalFormsTemplate(); - template.setHttpMethod(HttpMethod.GET); - template.setContentType(Collections.singletonList(MediaTypes.HAL_JSON)); - template.setTitle("HAL-FORMS unit test"); - template.getProperties().add(property); + HalFormsProperty property = HalFormsProperty.named("my-name")// + .withReadOnly(true) // + .withValue("my-value") // + .withPrompt("my-prompt") // + .withRegex("my-regex") // + .withRequired(true); - HalFormsDocument expected = halFormsDocument() - .link(new Link("/employees").withRel("collection")) - .link(new Link("/employees/1").withSelfRel()) - .template("foo", template) - .build(); + HalFormsTemplate template = HalFormsTemplate.forMethod(HttpMethod.GET) // + .withTitle("HAL-FORMS unit test") // + .andContentType(MediaTypes.HAL_JSON) // + .andProperty(property); // + HalFormsDocument expected = HalFormsDocument.empty() // + .andLink(new Link("/employees").withRel("collection")) // + .andLink(new Link("/employees/1").withSelfRel())// + .andTemplate("foo", template); final ByteArrayOutputStream stream = new ByteArrayOutputStream(); @@ -131,13 +130,6 @@ public class HalFormsMessageConverterTest { this.messageConverter.write(expected, MediaTypes.HAL_FORMS_JSON, convertedMessage); - String json = stream.toString(); - - System.out.println(json); - - HalFormsDocument actual = this.mapper.readValue(json, HalFormsDocument.class); - - assertThat(actual, is(expected)); + assertThat(this.mapper.readValue(stream.toString(), HalFormsDocument.class), is(expected)); } - } diff --git a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationTest.java b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java similarity index 75% rename from src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationTest.java rename to src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java index 766b403c..07e0895b 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java @@ -19,7 +19,6 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; @@ -33,7 +32,6 @@ import java.util.TreeMap; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -61,38 +59,34 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import com.fasterxml.jackson.databind.ObjectMapper; /** - * Test that when an {@link org.springframework.hateoas.Affordance} is included that does NOT match the self link, - * an exception is thrown. + * Test that when an {@link org.springframework.hateoas.Affordance} is included that does NOT match the self link, an + * exception is thrown. * * @author Greg Turnquist */ @RunWith(SpringRunner.class) @WebAppConfiguration @ContextConfiguration -public class HalFormsValidationTest { +public class HalFormsValidationIntegrationTest { - @Autowired - WebApplicationContext context; + @Autowired WebApplicationContext context; - @Autowired - ObjectMapper mapper; + @Autowired ObjectMapper mapper; MockMvc mockMvc; @Before public void setUp() { - this.mockMvc = webAppContextSetup(this.context) - .build(); + this.mockMvc = webAppContextSetup(this.context).build(); } @Test public void singleEmployee() throws Exception { Exception exception = this.mockMvc.perform(get("/employees/0").accept(MediaTypes.HAL_FORMS_JSON)) - .andDo(print()) - .andExpect(status().is5xxServerError()) - .andReturn() - .getResolvedException(); + .andExpect(status().is5xxServerError()) // + .andReturn() // + .getResolvedException(); assertThat(exception.getMessage(), containsString("Affordance's URI /employees")); assertThat(exception.getMessage(), containsString("doesn't match self link /employees/0")); @@ -101,11 +95,9 @@ public class HalFormsValidationTest { @Test public void collectionOfEmployees() throws Exception { - Exception exception = this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON)) - .andDo(print()) - .andExpect(status().is5xxServerError()) - .andReturn() - .getResolvedException(); + Exception exception = this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON)) // + .andExpect(status().is5xxServerError()) // + .andReturn().getResolvedException(); assertThat(exception.getMessage(), containsString("Affordance's URI /employees/0")); assertThat(exception.getMessage(), containsString("doesn't match self link /employees")); @@ -131,36 +123,31 @@ public class HalFormsValidationTest { List> employees = new ArrayList>(); // Fetch each Resource using the controller's findOne method. - for (int i=0; i < EMPLOYEES.size(); i++) { - employees.add(findOne(String.valueOf(i))); + for (int i = 0; i < EMPLOYEES.size(); i++) { + employees.add(findOne(i)); } // Generate an "Affordance" based on this method (the "self" link) Link selfLink = linkTo(methodOn(BadController.class).all()).withSelfRel() - .withAffordance(afford(methodOn(BadController.class).updateEmployee(null, "0"))); + .andAffordance(afford(methodOn(BadController.class).updateEmployee(null, 0))); // Return the collection of employee resources along with the composite affordance return new Resources>(employees, selfLink); } @GetMapping("/employees/{id}") - public Resource findOne(@PathVariable String id) { + public Resource findOne(@PathVariable Integer id) { // Start the affordance with the "self" link, i.e. this method. - Link findOneLink = - linkTo(methodOn(BadController.class).findOne(id)).withSelfRel(); + Link findOneLink = linkTo(methodOn(BadController.class).findOne(id)).withSelfRel(); // Define final link as means to find entire collection. - Link employeesLink = - linkTo(methodOn(BadController.class).all()).withRel("employees") - .withAffordance(afford(methodOn(BadController.class).newEmployee(null))); + Link employeesLink = linkTo(methodOn(BadController.class).all()).withRel("employees") + .andAffordance(afford(methodOn(BadController.class).newEmployee(null))); // Return the affordance + a link back to the entire collection resource. - return new Resource( - EMPLOYEES.get(Integer.parseInt(id)), - findOneLink - .addAffordances(employeesLink.getAffordances()), - employeesLink); + return new Resource(EMPLOYEES.get(id), findOneLink.andAffordances(employeesLink.getAffordances()), + employeesLink); } @PostMapping("/employees") @@ -171,9 +158,7 @@ public class HalFormsValidationTest { EMPLOYEES.put(newEmployeeId, employee); try { - return ResponseEntity - .noContent() - .location(new URI(findOne(String.valueOf(newEmployeeId)).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -181,13 +166,12 @@ public class HalFormsValidationTest { } @PutMapping("/employees/{id}") - public ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable String id) { + public ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable Integer id) { + + EMPLOYEES.put(id, employee); - EMPLOYEES.put(Integer.parseInt(id), employee); try { - return ResponseEntity - .noContent() - .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -195,7 +179,7 @@ public class HalFormsValidationTest { } @PatchMapping("/employees/{id}") - public ResponseEntity partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable String id) { + public ResponseEntity partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable Integer id) { Employee oldEmployee = EMPLOYEES.get(id); @@ -209,11 +193,10 @@ public class HalFormsValidationTest { newEmployee = newEmployee.withRole(employee.getRole()); } - EMPLOYEES.put(Integer.parseInt(id), newEmployee); + EMPLOYEES.put(id, newEmployee); + try { - return ResponseEntity - .noContent() - .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -223,7 +206,7 @@ public class HalFormsValidationTest { @Configuration @EnableWebMvc - @EnableHypermediaSupport(type = {HypermediaType.HAL_FORMS}) + @EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS }) static class TestConfig { @Bean @@ -231,6 +214,4 @@ public class HalFormsValidationTest { return new BadController(); } } - - } diff --git a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcTest.java b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcIntegrationTest.java similarity index 57% rename from src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcTest.java rename to src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcIntegrationTest.java index 00cf1519..54bffdf7 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcIntegrationTest.java @@ -19,7 +19,6 @@ import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.collection.IsCollectionWithSize.*; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; @@ -65,71 +64,64 @@ import com.fasterxml.jackson.databind.ObjectMapper; @RunWith(SpringRunner.class) @WebAppConfiguration @ContextConfiguration -public class HalFormsWebMvcTest { +public class HalFormsWebMvcIntegrationTest { - @Autowired - WebApplicationContext context; - - @Autowired - ObjectMapper mapper; + @Autowired WebApplicationContext context; + @Autowired ObjectMapper mapper; MockMvc mockMvc; @Before public void setUp() { - this.mockMvc = webAppContextSetup(this.context) - .build(); + this.mockMvc = webAppContextSetup(this.context).build(); } @Test public void singleEmployee() throws Exception { - this.mockMvc.perform(get("/employees/0").accept(MediaTypes.HAL_FORMS_JSON)) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.name", is("Frodo Baggins"))) - .andExpect(jsonPath("$.role", is("ring bearer"))) + this.mockMvc.perform(get("/employees/0").accept(MediaTypes.HAL_FORMS_JSON)) // + .andExpect(status().isOk()) // + .andExpect(jsonPath("$.name", is("Frodo Baggins"))).andExpect(jsonPath("$.role", is("ring bearer"))) - .andExpect(jsonPath("$._links.*", hasSize(2))) - .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) - .andExpect(jsonPath("$._links['employees'].href", is("http://localhost/employees"))) + .andExpect(jsonPath("$._links.*", hasSize(2))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._links['employees'].href", is("http://localhost/employees"))) - .andExpect(jsonPath("$._templates.*", hasSize(2))) - .andExpect(jsonPath("$._templates['default'].method", is("put"))) - .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) - .andExpect(jsonPath("$._templates['default'].properties[0].required", is(true))) - .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) - .andExpect(jsonPath("$._templates['default'].properties[1].required", is(true))) + .andExpect(jsonPath("$._templates.*", hasSize(2))) + .andExpect(jsonPath("$._templates['default'].method", is("put"))) + .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['default'].properties[0].required", is(true))) + .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['default'].properties[1].required", is(true))) - .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].method", is("patch"))) - .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].name", is("name"))) - .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].required", is(false))) - .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].name", is("role"))) - .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required", is(false))); + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].method", is("patch"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].required", is(false))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required", is(false))); } @Test public void collectionOfEmployees() throws Exception { - this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON)) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) - .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) - .andExpect(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) - .andExpect(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) - .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) - .andExpect(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) + this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON)) // + .andExpect(status().isOk()) // + .andExpect(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .andExpect(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0"))) + .andExpect(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins"))) + .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .andExpect(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1"))) - .andExpect(jsonPath("$._links.*", hasSize(1))) - .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees"))) + .andExpect(jsonPath("$._links.*", hasSize(1))) + .andExpect(jsonPath("$._links['self'].href", is("http://localhost/employees"))) - .andExpect(jsonPath("$._templates.*", hasSize(1))) - .andExpect(jsonPath("$._templates['default'].method", is("post"))) - .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) - .andExpect(jsonPath("$._templates['default'].properties[0].required", is(true))) - .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) - .andExpect(jsonPath("$._templates['default'].properties[1].required", is(true))); + .andExpect(jsonPath("$._templates.*", hasSize(1))) + .andExpect(jsonPath("$._templates['default'].method", is("post"))) + .andExpect(jsonPath("$._templates['default'].properties[0].name", is("name"))) + .andExpect(jsonPath("$._templates['default'].properties[0].required", is(true))) + .andExpect(jsonPath("$._templates['default'].properties[1].name", is("role"))) + .andExpect(jsonPath("$._templates['default'].properties[1].required", is(true))); } @RestController @@ -149,35 +141,32 @@ public class HalFormsWebMvcTest { List> employees = new ArrayList>(); // Fetch each Resource using the controller's findOne method. - for (int i=0; i < EMPLOYEES.size(); i++) { - employees.add(findOne(String.valueOf(i))); + for (int i = 0; i < EMPLOYEES.size(); i++) { + employees.add(findOne(i)); } // Generate an "Affordance" based on this method (the "self" link) Link selfLink = linkTo(methodOn(EmployeeController.class).all()).withSelfRel() - .withAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))); + .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))); // Return the collection of employee resources along with the composite affordance return new Resources>(employees, selfLink); } @GetMapping("/employees/{id}") - public Resource findOne(@PathVariable String id) { + public Resource findOne(@PathVariable Integer id) { // Start the affordance with the "self" link, i.e. this method. - Link findOneLink = - linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel(); + Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel(); // Define final link as means to find entire collection. Link employeesLink = linkTo(methodOn(EmployeeController.class).all()).withRel("employees"); // Return the affordance + a link back to the entire collection resource. - return new Resource( - EMPLOYEES.get(Integer.parseInt(id)), - findOneLink - .withAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) - .withAffordance(afford(methodOn(EmployeeController.class).partiallyUpdateEmployee(null, id))), - employeesLink); + return new Resource(EMPLOYEES.get(id), + findOneLink.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) // + .andAffordance(afford(methodOn(EmployeeController.class).partiallyUpdateEmployee(null, id))), + employeesLink); } @PostMapping("/employees") @@ -188,9 +177,7 @@ public class HalFormsWebMvcTest { EMPLOYEES.put(newEmployeeId, employee); try { - return ResponseEntity - .noContent() - .location(new URI(findOne(String.valueOf(newEmployeeId)).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -198,13 +185,12 @@ public class HalFormsWebMvcTest { } @PutMapping("/employees/{id}") - public ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable String id) { + public ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable Integer id) { + + EMPLOYEES.put(id, employee); - EMPLOYEES.put(Integer.parseInt(id), employee); try { - return ResponseEntity - .noContent() - .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -212,10 +198,9 @@ public class HalFormsWebMvcTest { } @PatchMapping("/employees/{id}") - public ResponseEntity partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable String id) { + public ResponseEntity partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable Integer id) { Employee oldEmployee = EMPLOYEES.get(id); - Employee newEmployee = oldEmployee; if (employee.getName() != null) { @@ -226,11 +211,10 @@ public class HalFormsWebMvcTest { newEmployee = newEmployee.withRole(employee.getRole()); } - EMPLOYEES.put(Integer.parseInt(id), newEmployee); + EMPLOYEES.put(id, newEmployee); + try { - return ResponseEntity - .noContent() - .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); @@ -240,7 +224,7 @@ public class HalFormsWebMvcTest { @Configuration @EnableWebMvc - @EnableHypermediaSupport(type = {HypermediaType.HAL_FORMS}) + @EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS }) static class TestConfig { @Bean @@ -248,6 +232,4 @@ public class HalFormsWebMvcTest { return new EmployeeController(); } } - - } diff --git a/src/test/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsIntegrationTest.java index 304591b1..dd6b9fce 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/Jackson2HalFormsIntegrationTest.java @@ -28,7 +28,6 @@ import java.util.Locale; import org.junit.Before; import org.junit.Test; - import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.MessageSourceAccessor; @@ -51,6 +50,7 @@ import org.springframework.hateoas.hal.SimplePojo; import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator; import org.springframework.hateoas.support.MappingUtils; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -60,13 +60,12 @@ import com.fasterxml.jackson.databind.SerializationFeature; public class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegrationTest { static final Links PAGINATION_LINKS = new Links(new Link("foo", Link.REL_NEXT), new Link("bar", Link.REL_PREVIOUS)); - + @Before public void setUpModule() { mapper.registerModule(new Jackson2HalFormsModule()); - mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( - new AnnotationRelProvider(), null, null, true, new HalFormsConfiguration())); + mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationRelProvider(), null, null, true, new HalFormsConfiguration())); mapper.configure(SerializationFeature.INDENT_OUTPUT, true); } @@ -77,7 +76,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resourceSupport.add(new Link("localhost")); assertThat(write(resourceSupport), - is(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())))); } @Test @@ -86,8 +85,9 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling ResourceSupport expected = new ResourceSupport(); expected.add(new Link("localhost")); - assertThat(read(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())), - ResourceSupport.class), is(expected)); + assertThat( + read(MappingUtils.read(new ClassPathResource("single-link-reference.json", getClass())), ResourceSupport.class), + is(expected)); } @Test @@ -98,7 +98,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resourceSupport.add(new Link("localhost2")); assertThat(write(resourceSupport), - is(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())))); } @Test @@ -108,8 +108,9 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling expected.add(new Link("localhost")); expected.add(new Link("localhost2")); - assertThat(read(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())), - ResourceSupport.class), is(expected)); + assertThat( + read(MappingUtils.read(new ClassPathResource("list-link-reference.json", getClass())), ResourceSupport.class), + is(expected)); } @Test @@ -118,7 +119,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resource resource = new Resource(new SimplePojo("test1", 1), new Link("localhost")); assertThat(write(resource), - is(MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())))); } @Test @@ -127,8 +128,8 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resource expected = new Resource(new SimplePojo("test1", 1), new Link("localhost")); Resource result = mapper.readValue( - MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())), - mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)); + MappingUtils.read(new ClassPathResource("simple-resource-unwrapped.json", getClass())), + mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)); assertThat(result, is(expected)); } @@ -144,7 +145,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resources.add(new Link("localhost")); assertThat(write(resources), - is(MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())))); } @Test @@ -158,8 +159,8 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling expected.add(new Link("localhost")); Resources result = mapper.readValue( - MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())), - mapper.getTypeFactory().constructParametricType(Resources.class, String.class)); + MappingUtils.read(new ClassPathResource("simple-embedded-resource-reference.json", getClass())), + mapper.getTypeFactory().constructParametricType(Resources.class, String.class)); assertThat(result, is(expected)); @@ -175,7 +176,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resources.add(new Link("localhost")); assertThat(write(resources), - is(MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())))); } @Test @@ -188,9 +189,9 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling expected.add(new Link("localhost")); Resources> result = mapper.readValue( - MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())), - mapper.getTypeFactory().constructParametricType(Resources.class, - mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); + MappingUtils.read(new ClassPathResource("single-embedded-resource-reference.json", getClass())), + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); assertThat(result, is(expected)); } @@ -202,7 +203,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resources.add(new Link("localhost")); assertThat(write(resources), - is(MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())))); } @Test @@ -211,10 +212,10 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resources> expected = setupResources(); expected.add(new Link("localhost")); - Resources> result = - mapper.readValue(MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())), + Resources> result = mapper.readValue( + MappingUtils.read(new ClassPathResource("multiple-resource-resources.json", getClass())), mapper.getTypeFactory().constructParametricType(Resources.class, - mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); + mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); assertThat(result, is(expected)); } @@ -229,7 +230,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resources.add(new Link("localhost")); assertThat(write(resources), - is(MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())))); } @Test @@ -241,10 +242,10 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resources> expected = new Resources>(content); expected.add(new Link("localhost")); - Resources> result = - mapper.readValue(MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())), + Resources> result = mapper.readValue( + MappingUtils.read(new ClassPathResource("annotated-resource-resources.json", getClass())), mapper.getTypeFactory().constructParametricType(Resources.class, - mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); assertThat(result, is(expected)); } @@ -252,16 +253,16 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling @Test public void serializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { assertThat(write(setupAnnotatedResources()), - is(MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())))); } @Test public void deserializesMultipleAnnotatedResourceResourcesAsEmbedded() throws Exception { - Resources> result = - mapper.readValue(MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())), + Resources> result = mapper.readValue( + MappingUtils.read(new ClassPathResource("annotated-embedded-resources-reference.json", getClass())), mapper.getTypeFactory().constructParametricType(Resources.class, - mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); assertThat(result, is(setupAnnotatedResources())); } @@ -269,15 +270,15 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling @Test public void serializesPagedResource() throws Exception { assertThat(write(setupAnnotatedPagedResources()), - is(MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())))); } @Test public void deserializesPagedResource() throws Exception { - PagedResources> result = - mapper.readValue(MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())), + PagedResources> result = mapper.readValue( + MappingUtils.read(new ClassPathResource("annotated-paged-resources.json", getClass())), mapper.getTypeFactory().constructParametricType(PagedResources.class, - mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); assertThat(result, is(setupAnnotatedPagedResources())); } @@ -286,10 +287,10 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling public void rendersCuriesCorrectly() throws Exception { Resources resources = new Resources(Collections.emptySet(), new Link("foo"), - new Link("bar", "myrel")); + new Link("bar", "myrel")); assertThat(getCuriedObjectMapper().writeValueAsString(resources), - is(MappingUtils.read(new ClassPathResource("curied-document.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("curied-document.json", getClass())))); } @Test @@ -297,7 +298,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resources resources = new Resources(Collections.emptySet()); assertThat(getCuriedObjectMapper().writeValueAsString(resources), - is(MappingUtils.read(new ClassPathResource("empty-document.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("empty-document.json", getClass())))); } @Test @@ -307,7 +308,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resources.add(new Link("foo")); assertThat(getCuriedObjectMapper().writeValueAsString(resources), - is(MappingUtils.read(new ClassPathResource("single-non-curie-document.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("single-non-curie-document.json", getClass())))); } @Test @@ -316,8 +317,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling ResourceSupport support = new ResourceSupport(); support.add(new Link("/foo{?bar}", "search")); - assertThat(write(support), - is(MappingUtils.read(new ClassPathResource("link-template.json", getClass())))); + assertThat(write(support), is(MappingUtils.read(new ClassPathResource("link-template.json", getClass())))); } @Test @@ -334,7 +334,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling }; assertThat(getCuriedObjectMapper(provider, null).writeValueAsString(resources), - is(MappingUtils.read(new ClassPathResource("multiple-curies-document.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("multiple-curies-document.json", getClass())))); } @Test @@ -347,8 +347,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling Resources resources = new Resources(values); - assertThat(write(resources), - is(MappingUtils.read(new ClassPathResource("empty-embedded-pojos.json", getClass())))); + assertThat(write(resources), is(MappingUtils.read(new ClassPathResource("empty-embedded-pojos.json", getClass())))); } @Test @@ -374,7 +373,7 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling resource.add(new Link("target", "ns:foobar")); assertThat(objectMapper.writeValueAsString(resource), - is(MappingUtils.read(new ClassPathResource("link-with-title.json", getClass())))); + is(MappingUtils.read(new ClassPathResource("link-with-title.json", getClass())))); } private static Resources> setupResources() { @@ -401,13 +400,14 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); content.add(new Resource(new SimpleAnnotatedPojo("test2", 2), new Link("localhost"))); - return new PagedResources>(content, new PagedResources.PageMetadata(2, 0, 4), PAGINATION_LINKS); + return new PagedResources>(content, new PagedResources.PageMetadata(2, 0, 4), + PAGINATION_LINKS); } private static ObjectMapper getCuriedObjectMapper() { return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")), - null); + null); } private static ObjectMapper getCuriedObjectMapper(CurieProvider provider, MessageSource messageSource) { @@ -415,11 +415,10 @@ public class Jackson2HalFormsIntegrationTest extends AbstractJackson2Marshalling ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Jackson2HalFormsModule()); mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationRelProvider(), provider, - messageSource == null ? null : new MessageSourceAccessor(messageSource), true, new HalFormsConfiguration())); + messageSource == null ? null : new MessageSourceAccessor(messageSource), true, new HalFormsConfiguration())); mapper.configure(SerializationFeature.INDENT_OUTPUT, true); + mapper.setSerializationInclusion(Include.NON_NULL); return mapper; } - - } diff --git a/src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTests.java b/src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTest.java similarity index 74% rename from src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTests.java rename to src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTest.java index 84a9573f..fb512ac1 100644 --- a/src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTests.java +++ b/src/test/java/org/springframework/hateoas/mvc/SpringMvcAffordanceBuilderUnitTest.java @@ -23,16 +23,18 @@ import org.junit.Test; import org.springframework.core.annotation.Order; import org.springframework.hateoas.Affordance; import org.springframework.hateoas.AffordanceModel; -import org.springframework.hateoas.AffordanceModelFactory; +import org.springframework.hateoas.core.AffordanceModelFactory; import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation; import org.springframework.http.MediaType; import org.springframework.plugin.core.OrderAwarePluginRegistry; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.web.util.UriComponents; /** * @author Greg Turnquist + * @author Oliver Gierke */ -public class SpringMvcAffordanceBuilderUnitTests { +public class SpringMvcAffordanceBuilderUnitTest { @Test(expected = IllegalArgumentException.class) public void rejectsNullPluginRegistry() { @@ -45,22 +47,18 @@ public class SpringMvcAffordanceBuilderUnitTests { AffordanceModelFactory low = new LowPriorityModelFactory(); AffordanceModelFactory high = new HighPriorityModelFactory(); - OrderAwarePluginRegistry registry = - OrderAwarePluginRegistry.create(Arrays.asList(low, high)); - + PluginRegistry registry = OrderAwarePluginRegistry + .create(Arrays.asList(low, high)); + assertThat(registry.getPluginFor(MediaType.APPLICATION_JSON).get()).isEqualTo(high); } @Order(20) - static class LowPriorityModelFactory extends AffordanceModelFactory { + static class LowPriorityModelFactory implements AffordanceModelFactory { @Override - public MediaType getMediaType() { - return MediaType.APPLICATION_JSON; - } - - @Override - public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) { + public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, + UriComponents components) { return null; } @@ -71,15 +69,11 @@ public class SpringMvcAffordanceBuilderUnitTests { } @Order(10) - static class HighPriorityModelFactory extends AffordanceModelFactory { + static class HighPriorityModelFactory implements AffordanceModelFactory { @Override - public MediaType getMediaType() { - return MediaType.APPLICATION_JSON; - } - - @Override - public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) { + public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, + UriComponents components) { return null; } diff --git a/src/test/resources/org/springframework/hateoas/hal/forms/curied-document.json b/src/test/resources/org/springframework/hateoas/hal/forms/curied-document.json index da89ff40..cf77f7bb 100644 --- a/src/test/resources/org/springframework/hateoas/hal/forms/curied-document.json +++ b/src/test/resources/org/springframework/hateoas/hal/forms/curied-document.json @@ -1,5 +1,4 @@ { - "_embedded" : { }, "_links" : { "self" : { "href" : "foo" @@ -13,4 +12,4 @@ "templated" : true } ] } -} \ No newline at end of file +} diff --git a/src/test/resources/org/springframework/hateoas/hal/forms/empty-document.json b/src/test/resources/org/springframework/hateoas/hal/forms/empty-document.json index cf947791..ffcd4415 100644 --- a/src/test/resources/org/springframework/hateoas/hal/forms/empty-document.json +++ b/src/test/resources/org/springframework/hateoas/hal/forms/empty-document.json @@ -1,3 +1 @@ -{ - "_embedded" : { } -} \ No newline at end of file +{ } diff --git a/src/test/resources/org/springframework/hateoas/hal/forms/multiple-curies-document.json b/src/test/resources/org/springframework/hateoas/hal/forms/multiple-curies-document.json index c963301b..563c5547 100644 --- a/src/test/resources/org/springframework/hateoas/hal/forms/multiple-curies-document.json +++ b/src/test/resources/org/springframework/hateoas/hal/forms/multiple-curies-document.json @@ -1,5 +1,4 @@ { - "_embedded" : { }, "_links" : { "default:myrel" : { "href" : "foo" @@ -12,4 +11,4 @@ "name" : "bar" } ] } -} \ No newline at end of file +} diff --git a/src/test/resources/org/springframework/hateoas/hal/forms/single-non-curie-document.json b/src/test/resources/org/springframework/hateoas/hal/forms/single-non-curie-document.json index ca582e97..df5d9a7a 100644 --- a/src/test/resources/org/springframework/hateoas/hal/forms/single-non-curie-document.json +++ b/src/test/resources/org/springframework/hateoas/hal/forms/single-non-curie-document.json @@ -1,8 +1,7 @@ { - "_embedded" : { }, "_links" : { "self" : { "href" : "foo" } } -} \ No newline at end of file +}