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 +}