From 137e7c390f7ae050fbafd25c78e7d07d19391d09 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 18 Dec 2013 10:34:19 +0100 Subject: [PATCH] #128 - Severe refactoring regarding LinkDiscoverers. The LinkDiscoverer interface is now a Plugin to allow selecting a discoverer per media type. We also provide a LinkDiscoverers wrapper around a PluginRegistry for easier injection. Dropped DefaultLinkDiscoverer as the above mentioned changed prohibits the usage of a discoverer without a media type. Clients that previously used DefaultLinkDiscoverer should switch to JsonPathLinkDiscoverer with a custom JsonPath expression and media type. @EnableHypermediaSupport now supports multiple HypermediaType values set to be able to set up support for multiple hypermedia formats. Admittedly, the only currently supported type is HAL but we prepare for the addition of other (e.g. Collection/JSON or the like). We changed the infrastructure setup taken care of by @EnableHypermediaSupport to not enrich all ObjectMapper instances with our HAL modules but explicitly register an ObjectMapper with the application context and then registering a custom MappingJackson(2)HttpMessageConverter with the HandlerAdapter instances present in the ApplicationContext. We skip the registration if an already registered MJ2HMC has an ObjectMapper already supporting the Spring HATEOAS mixins. Removed some compiler warnings. --- .../hateoas/LinkDiscoverer.java | 5 +- .../hateoas/LinkDiscoverers.java | 62 ++++++ ...ultLinkDiscoverer.java => MediaTypes.java} | 17 +- .../config/EnableHypermediaSupport.java | 4 +- ...ermediaSupportBeanDefinitionRegistrar.java | 176 ++++++++++++------ .../hateoas/core/JsonPathLinkDiscoverer.java | 20 +- .../hateoas/hal/HalLinkDiscoverer.java | 3 +- .../hateoas/hal/Jackson2HalModule.java | 13 ++ .../hateoas/hal/ResourceSupportMixin.java | 15 +- .../hateoas/hal/ResourcesMixin.java | 15 +- .../hateoas/mvc/ControllerLinkBuilder.java | 1 + .../hateoas/LinkDiscoverersUnitTest.java | 67 +++++++ .../hateoas/VndErrorsMarshallingTest.java | 1 + ...nableHypermediaSupportIntegrationTest.java | 58 +++--- .../core/DefaultLinkDiscovererUnitTest.java | 43 ----- .../core/JsonPathLinkDiscovererUnitTest.java | 6 +- .../hal/Jackson1HalIntegrationTest.java | 1 + 17 files changed, 356 insertions(+), 151 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/LinkDiscoverers.java rename src/main/java/org/springframework/hateoas/{core/DefaultLinkDiscoverer.java => MediaTypes.java} (61%) create mode 100644 src/test/java/org/springframework/hateoas/LinkDiscoverersUnitTest.java delete mode 100644 src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java diff --git a/src/main/java/org/springframework/hateoas/LinkDiscoverer.java b/src/main/java/org/springframework/hateoas/LinkDiscoverer.java index 0a2a38f7..81f9c612 100644 --- a/src/main/java/org/springframework/hateoas/LinkDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/LinkDiscoverer.java @@ -18,12 +18,15 @@ package org.springframework.hateoas; import java.io.InputStream; import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.plugin.core.Plugin; + /** * Interface to allow discovering links by relation type from some source. * * @author Oliver Gierke */ -public interface LinkDiscoverer { +public interface LinkDiscoverer extends Plugin { /** * Finds a single link with the given relation type in the given {@link String} representation. diff --git a/src/main/java/org/springframework/hateoas/LinkDiscoverers.java b/src/main/java/org/springframework/hateoas/LinkDiscoverers.java new file mode 100644 index 00000000..ea5e6fe5 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/LinkDiscoverers.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013 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; + +import org.springframework.http.MediaType; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.util.Assert; + +/** + * Value object to wrap a {@link PluginRegistry} for {@link LinkDiscoverer} so that it's easier to inject them into + * clients wanting to lookup a {@link LinkDiscoverer} for a given {@link MediaTypes}. + * + * @author Oliver Gierke + */ +public class LinkDiscoverers { + + private final PluginRegistry discoverers; + + /** + * Creates a new {@link LinkDiscoverers} instance with the given {@link PluginRegistry}. + * + * @param discoverers must not be {@literal null}. + */ + public LinkDiscoverers(PluginRegistry discoverers) { + + Assert.notNull(discoverers, "Registry of LinkDiscoverer must not be null!"); + this.discoverers = discoverers; + } + + /** + * Returns the {@link LinkDiscoverer} suitable for the given {@link MediaType}. + * + * @param mediaType + * @return + */ + public LinkDiscoverer getLinkDiscovererFor(MediaType mediaType) { + return discoverers.getPluginFor(mediaType); + } + + /** + * Returns the {@link LinkDiscoverer} suitable for the given media type. + * + * @param mediaType + * @return + */ + public LinkDiscoverer getLinkDiscovererFor(String mediaType) { + return getLinkDiscovererFor(MediaType.valueOf(mediaType)); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/MediaTypes.java similarity index 61% rename from src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java rename to src/main/java/org/springframework/hateoas/MediaTypes.java index ef68bc4e..0b769988 100644 --- a/src/main/java/org/springframework/hateoas/core/DefaultLinkDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/MediaTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2013 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. @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.hateoas.core; +package org.springframework.hateoas; + +import org.springframework.http.MediaType; /** - * {@link JsonPathLinkDiscoverer} based on the default link representation we expose. + * Constants for well-known hypermedia types. * * @author Oliver Gierke */ -public class DefaultLinkDiscoverer extends JsonPathLinkDiscoverer { +public class MediaTypes { - /** - * Creates a new {@link DefaultLinkDiscoverer}. - */ - public DefaultLinkDiscoverer() { - super("$links[?(@.rel == '%s')].href"); - } + public static final MediaType HAL_JSON = MediaType.valueOf("application/hal+json"); } diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index 4f197cfa..28d8964b 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -54,7 +54,7 @@ public @interface EnableHypermediaSupport { * * @return */ - HypermediaType type() default HypermediaType.DEFAULT; + HypermediaType[] type(); /** * Hypermedia representation types supported. @@ -63,8 +63,6 @@ public @interface EnableHypermediaSupport { */ static enum HypermediaType { - DEFAULT, - /** * HAL - Hypermedia Application Language. * diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 7fd3c1e7..ca93c808 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -15,8 +15,13 @@ */ package org.springframework.hateoas.config; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*; import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; +import static org.springframework.hateoas.MediaTypes.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -29,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; @@ -36,10 +42,10 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.LinkDiscoverers; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.core.AnnotationRelProvider; -import org.springframework.hateoas.core.DefaultLinkDiscoverer; import org.springframework.hateoas.core.DefaultRelProvider; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.core.EvoInflectorRelProvider; @@ -68,8 +74,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; @SuppressWarnings("deprecation") class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer"; private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider"; + private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry"; + private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper"; private static final boolean JACKSON1_PRESENT = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null); private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", @@ -84,30 +91,54 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) */ @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(importingClassMetadata, registry); + linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(metadata, registry); - Map attributes = importingClassMetadata.getAnnotationAttributes(EnableHypermediaSupport.class - .getName()); - HypermediaType type = (HypermediaType) attributes.get("type"); + Map attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName()); + Collection types = Arrays.asList((HypermediaType[]) attributes.get("type")); - if (JSONPATH_PRESENT) { - registerBeanDefinition( - new BeanDefinitionHolder(getLinkDiscovererBeanDefinition(type), LINK_DISCOVERER_BEAN_NAME), registry); + for (HypermediaType type : types) { + + if (JSONPATH_PRESENT) { + + AbstractBeanDefinition linkDiscovererBeanDefinition = getLinkDiscovererBeanDefinition(type); + registerBeanDefinition( + new BeanDefinitionHolder(linkDiscovererBeanDefinition, BeanDefinitionReaderUtils.generateBeanName( + linkDiscovererBeanDefinition, registry)), registry); + } } - if (type == HypermediaType.HAL) { + if (types.contains(HypermediaType.HAL)) { if (JACKSON2_PRESENT) { - registerWithGeneratedName(new RootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class), registry); + + BeanDefinitionBuilder halQueryMapperBuilder = rootBeanDefinition(ObjectMapper.class); + registerSourcedBeanDefinition(halQueryMapperBuilder, metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME); + + BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class); + registerSourcedBeanDefinition(builder, metadata, registry); } if (JACKSON1_PRESENT) { - registerWithGeneratedName(new RootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class), registry); + BeanDefinitionBuilder builder = rootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class); + registerSourcedBeanDefinition(builder, metadata, registry); } } + if (!types.isEmpty()) { + + BeanDefinitionBuilder linkDiscoverersRegistryBuilder = BeanDefinitionBuilder + .rootBeanDefinition(PluginRegistryFactoryBean.class); + linkDiscoverersRegistryBuilder.addPropertyValue("type", LinkDiscoverer.class); + registerSourcedBeanDefinition(linkDiscoverersRegistryBuilder, metadata, registry, + LINK_DISCOVERER_REGISTRY_BEAN_NAME); + + BeanDefinitionBuilder linkDiscoverersBuilder = BeanDefinitionBuilder.rootBeanDefinition(LinkDiscoverers.class); + linkDiscoverersBuilder.addConstructorArgReference(LINK_DISCOVERER_REGISTRY_BEAN_NAME); + registerSourcedBeanDefinition(linkDiscoverersBuilder, metadata, registry); + } + registerRelProviderPluginRegistryAndDelegate(registry); } @@ -156,15 +187,33 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe case HAL: definition = new RootBeanDefinition(HalLinkDiscoverer.class); break; - case DEFAULT: default: - definition = new RootBeanDefinition(DefaultLinkDiscoverer.class); + throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type)); } definition.setSource(this); return definition; } + private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builder, AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + + AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); + String generateBeanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, registry); + return registerSourcedBeanDefinition(builder, metadata, registry, generateBeanName); + } + + private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builder, AnnotationMetadata metadata, + BeanDefinitionRegistry registry, String name) { + + AbstractBeanDefinition beanDefinition = builder.getBeanDefinition(); + beanDefinition.setSource(metadata); + + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, name); + registerBeanDefinition(holder, registry); + return name; + } + /** * {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in * the {@link ApplicationContext}. @@ -173,7 +222,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe */ private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { - private BeanFactory factory; + private CurieProvider curieProvider; + private RelProvider relProvider; + private ObjectMapper halObjectMapper; /* * (non-Javadoc) @@ -181,7 +232,10 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe */ @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.factory = beanFactory; + + this.curieProvider = getCurieProvider(beanFactory); + this.relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); + this.halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class); } /* @@ -201,39 +255,49 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { - registerModule(((RequestMappingHandlerAdapter) bean).getMessageConverters()); + + RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; + adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters())); } if (bean instanceof AnnotationMethodHandlerAdapter) { - registerModule(((AnnotationMethodHandlerAdapter) bean).getMessageConverters()); - } - if (bean instanceof ObjectMapper) { - registerModule(bean); + AnnotationMethodHandlerAdapter adapter = (AnnotationMethodHandlerAdapter) bean; + List> augmentedConverters = potentiallyRegisterModule(Arrays + .asList(adapter.getMessageConverters())); + adapter + .setMessageConverters(augmentedConverters.toArray(new HttpMessageConverter[augmentedConverters.size()])); } return bean; } - private void registerModule(List> converters) { + private List> potentiallyRegisterModule(List> converters) { for (HttpMessageConverter converter : converters) { if (converter instanceof MappingJackson2HttpMessageConverter) { - registerModule(((MappingJackson2HttpMessageConverter) converter).getObjectMapper()); + MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter; + ObjectMapper objectMapper = halConverterCandidate.getObjectMapper(); + if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) { + return converters; + } } } + + halObjectMapper.registerModule(new Jackson2HalModule()); + halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider)); + + MappingJackson2HttpMessageConverter halConverter = new MappingJackson2HttpMessageConverter(); + halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); + halConverter.setObjectMapper(halObjectMapper); + + List> result = new ArrayList>(converters.size()); + result.add(halConverter); + result.addAll(converters); + return result; } - private void registerModule(Object objectMapper) { - - RelProvider provider = factory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); - - ObjectMapper mapper = (ObjectMapper) objectMapper; - mapper.registerModule(new Jackson2HalModule()); - mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider, getCurieProvider())); - } - - private CurieProvider getCurieProvider() { + private static CurieProvider getCurieProvider(BeanFactory factory) { try { return factory.getBean(CurieProvider.class); @@ -252,7 +316,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe @Deprecated private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { - private BeanFactory beanFactory; + private RelProvider relProvider; /* * (non-Javadoc) @@ -260,7 +324,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe */ @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; + this.relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); } /* @@ -279,37 +343,39 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof AnnotationMethodHandlerAdapter) { - registerModule(((AnnotationMethodHandlerAdapter) bean).getMessageConverters()); - } - if (bean instanceof RequestMappingHandlerAdapter) { - registerModule(((RequestMappingHandlerAdapter) bean).getMessageConverters()); + + RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; + adapter.setMessageConverters(registerModule(adapter.getMessageConverters())); } - if (bean instanceof org.codehaus.jackson.map.ObjectMapper) { - registerModule(bean); + if (bean instanceof AnnotationMethodHandlerAdapter) { + + AnnotationMethodHandlerAdapter adapter = (AnnotationMethodHandlerAdapter) bean; + List> augmentedConverters = registerModule(Arrays + .asList(adapter.getMessageConverters())); + adapter + .setMessageConverters(augmentedConverters.toArray(new HttpMessageConverter[augmentedConverters.size()])); } return bean; } - private void registerModule(List> converters) { + private List> registerModule(List> converters) { - for (HttpMessageConverter converter : converters) { - if (converter instanceof MappingJacksonHttpMessageConverter) { - registerModule(((MappingJacksonHttpMessageConverter) converter).getObjectMapper()); - } - } - } + org.codehaus.jackson.map.ObjectMapper objectMapper = new org.codehaus.jackson.map.ObjectMapper(); - private void registerModule(Object objectMapper) { + objectMapper.registerModule(new Jackson1HalModule()); + objectMapper.setHandlerInstantiator(new Jackson1HalModule.HalHandlerInstantiator(relProvider)); - RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); + MappingJacksonHttpMessageConverter halConverter = new MappingJacksonHttpMessageConverter(); + halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); + halConverter.setObjectMapper(objectMapper); - org.codehaus.jackson.map.ObjectMapper mapper = (org.codehaus.jackson.map.ObjectMapper) objectMapper; - mapper.registerModule(new Jackson1HalModule()); - mapper.setHandlerInstantiator(new Jackson1HalModule.HalHandlerInstantiator(relProvider)); + List> result = new ArrayList>(converters.size()); + result.add(halConverter); + result.addAll(converters); + return result; } } } diff --git a/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java index 493af2d0..27d89b92 100644 --- a/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/core/JsonPathLinkDiscoverer.java @@ -26,6 +26,7 @@ import net.minidev.json.JSONArray; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -39,19 +40,23 @@ import com.jayway.jsonpath.JsonPath; public class JsonPathLinkDiscoverer implements LinkDiscoverer { private final String pathTemplate; + private final MediaType mediaType; /** - * Creates a new {@link JsonPathLinkDiscoverer} using the given path template. The template has to contain a single - * {@code %s} placeholder which will be replaced by the relation type. + * Creates a new {@link JsonPathLinkDiscoverer} using the given path template supporting the given {@link MediaType}. + * The template has to contain a single {@code %s} placeholder which will be replaced by the relation type. * * @param pathTemplate must not be {@literal null} or empty and contain a single placeholder. + * @param mediaType the {@link MediaType} to support. */ - public JsonPathLinkDiscoverer(String pathTemplate) { + public JsonPathLinkDiscoverer(String pathTemplate, MediaType mediaType) { Assert.hasText(pathTemplate, "Path template must not be null!"); Assert.isTrue(StringUtils.countOccurrencesOf(pathTemplate, "%s") == 1, "Path template must contain a single placeholder!"); + this.pathTemplate = pathTemplate; + this.mediaType = mediaType; } /* @@ -136,4 +141,13 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer { Link link = new Link(parseResult.toString(), rel); return Collections.unmodifiableList(Arrays.asList(link)); } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(MediaType delimiter) { + return this.mediaType == null ? true : this.mediaType.isCompatibleWith(delimiter); + } } diff --git a/src/main/java/org/springframework/hateoas/hal/HalLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/hal/HalLinkDiscoverer.java index 217bfb57..6332dbdb 100644 --- a/src/main/java/org/springframework/hateoas/hal/HalLinkDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/hal/HalLinkDiscoverer.java @@ -15,6 +15,7 @@ */ package org.springframework.hateoas.hal; +import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.LinkDiscoverer; import org.springframework.hateoas.core.JsonPathLinkDiscoverer; @@ -26,6 +27,6 @@ import org.springframework.hateoas.core.JsonPathLinkDiscoverer; public class HalLinkDiscoverer extends JsonPathLinkDiscoverer { public HalLinkDiscoverer() { - super("$_links..%s.href"); + super("$_links..%s.href", MediaTypes.HAL_JSON); } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 0895df5e..e3af51a8 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -48,6 +48,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; @@ -83,6 +84,18 @@ public class Jackson2HalModule extends SimpleModule { setMixInAnnotation(Resources.class, ResourcesMixin.class); } + /** + * Returns whether the module was already registered in the given {@link ObjectMapper}. + * + * @param mapper must not be {@literal null}. + * @return + */ + public static boolean isAlreadyRegisteredIn(ObjectMapper mapper) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + return LinkMixin.class.equals(mapper.findMixInClassFor(Link.class)); + } + /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. * diff --git a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java index e57ca81b..2ce8c829 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java @@ -22,15 +22,22 @@ import javax.xml.bind.annotation.XmlElement; import org.springframework.hateoas.Link; import org.springframework.hateoas.ResourceSupport; +@SuppressWarnings("deprecation") abstract class ResourceSupportMixin extends ResourceSupport { @Override @XmlElement(name = "link") @org.codehaus.jackson.annotate.JsonProperty("_links") @com.fasterxml.jackson.annotation.JsonProperty("_links") - @org.codehaus.jackson.map.annotate.JsonSerialize(include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListSerializer.class) - @org.codehaus.jackson.map.annotate.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListDeserializer.class) - @com.fasterxml.jackson.databind.annotation.JsonSerialize(include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer.class) - @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer.class) + @org.codehaus.jackson.map.annotate.JsonSerialize( + include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, + using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListSerializer.class) + @org.codehaus.jackson.map.annotate.JsonDeserialize( + using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListDeserializer.class) + @com.fasterxml.jackson.databind.annotation.JsonSerialize( + include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, + using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer.class) + @com.fasterxml.jackson.databind.annotation.JsonDeserialize( + using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer.class) public abstract List getLinks(); } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java index 232afd58..b7ad6b3c 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java @@ -21,16 +21,23 @@ import javax.xml.bind.annotation.XmlElement; import org.springframework.hateoas.Resources; +@SuppressWarnings("deprecation") public abstract class ResourcesMixin extends Resources { @Override @XmlElement(name = "embedded") @org.codehaus.jackson.annotate.JsonProperty("_embedded") @com.fasterxml.jackson.annotation.JsonProperty("_embedded") - @org.codehaus.jackson.map.annotate.JsonSerialize(include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesSerializer.class) - @org.codehaus.jackson.map.annotate.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesDeserializer.class) - @com.fasterxml.jackson.databind.annotation.JsonSerialize(include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesSerializer.class) - @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesDeserializer.class) + @org.codehaus.jackson.map.annotate.JsonSerialize( + include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, + using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesSerializer.class) + @org.codehaus.jackson.map.annotate.JsonDeserialize( + using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesDeserializer.class) + @com.fasterxml.jackson.databind.annotation.JsonSerialize( + include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, + using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesSerializer.class) + @com.fasterxml.jackson.databind.annotation.JsonDeserialize( + using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesDeserializer.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 43b7138d..4f335228 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -202,6 +202,7 @@ public class ControllerLinkBuilder extends LinkBuilderSupport registry = OrderAwarePluginRegistry.create(Arrays.asList(low, high)); + assertThat(registry.getPluginFor(MediaType.APPLICATION_JSON), is(high)); + } + + @Order(20) + static class LowPriorityLinkDiscoverer extends JsonPathLinkDiscoverer { + + public LowPriorityLinkDiscoverer() { + super("$.links.%s", MediaType.APPLICATION_JSON); + } + } + + @Order(10) + static class HighPriorityLinkDiscoverer extends JsonPathLinkDiscoverer { + + public HighPriorityLinkDiscoverer() { + super("$.links.%s", MediaType.APPLICATION_JSON); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java b/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java index 50311f3d..c5351845 100644 --- a/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java +++ b/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java @@ -48,6 +48,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; * * @author Oliver Gierke */ +@SuppressWarnings("deprecation") public class VndErrorsMarshallingTest { ObjectMapper jackson1Mapper; diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index bda13802..bc39e8c3 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -17,14 +17,13 @@ package org.springframework.hateoas.config; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.Map; import org.hamcrest.Matchers; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -32,14 +31,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.LinkDiscoverers; +import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; -import org.springframework.hateoas.core.DefaultLinkDiscoverer; import org.springframework.hateoas.core.DelegatingEntityLinks; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.HalLinkDiscoverer; -import org.springframework.hateoas.hal.Jackson1HalModule; -import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import com.fasterxml.jackson.databind.ObjectMapper; @@ -48,31 +51,37 @@ import com.fasterxml.jackson.databind.ObjectMapper; * * @author Oliver Gierke */ +@SuppressWarnings("deprecation") @RunWith(MockitoJUnitRunner.class) public class EnableHypermediaSupportIntegrationTest { @Test - @SuppressWarnings("deprecation") + @SuppressWarnings({ "unchecked" }) public void bootstrapHalConfiguration() { ApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(HalLinkDiscoverer.class))); + assertThat(context.getBean(ObjectMapper.class), is(notNullValue())); - ObjectMapper mapper = context.getBean(ObjectMapper.class); - verify(mapper, times(1)).registerModule(Mockito.any(Jackson2HalModule.class)); + RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); + assertThat(rmha.getMessageConverters(), Matchers.> hasItems( + instanceOf(MappingJackson2HttpMessageConverter.class), instanceOf(MappingJacksonHttpMessageConverter.class))); - org.codehaus.jackson.map.ObjectMapper jackson1Mapper = context.getBean(org.codehaus.jackson.map.ObjectMapper.class); - verify(jackson1Mapper, times(1)).registerModule(Mockito.any(Jackson1HalModule.class)); + AnnotationMethodHandlerAdapter amha = context.getBean(AnnotationMethodHandlerAdapter.class); + assertThat(Arrays.asList(amha.getMessageConverters()), Matchers.> hasItems( + instanceOf(MappingJackson2HttpMessageConverter.class), instanceOf(MappingJacksonHttpMessageConverter.class))); } @Test - public void bootstrapsDefaultConfiguration() { + public void registersLinkDiscoverers() { - ApplicationContext context = new AnnotationConfigApplicationContext(DefaultConfig.class); - assertEntityLinksSetUp(context); + ApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class); + + assertThat(discoverers, is(notNullValue())); + assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON), is(instanceOf(HalLinkDiscoverer.class))); assertRelProvidersSetUp(context); - assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(DefaultLinkDiscoverer.class))); } private static void assertEntityLinksSetUp(ApplicationContext context) { @@ -91,20 +100,21 @@ public class EnableHypermediaSupportIntegrationTest { @EnableHypermediaSupport(type = HypermediaType.HAL) static class HalConfig { + static int numberOfMessageConverters = 0; + static int numberOfMessageConvertersLegacy = 0; + @Bean - public ObjectMapper jackson2ObjectMapper() { - return mock(ObjectMapper.class); + public RequestMappingHandlerAdapter rmh() { + RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); + numberOfMessageConverters = adapter.getMessageConverters().size(); + return adapter; } @Bean - public org.codehaus.jackson.map.ObjectMapper jackson1ObjectMapper() { - return mock(org.codehaus.jackson.map.ObjectMapper.class); + public AnnotationMethodHandlerAdapter amha() { + AnnotationMethodHandlerAdapter adapter = new AnnotationMethodHandlerAdapter(); + numberOfMessageConvertersLegacy = adapter.getMessageConverters().length; + return adapter; } } - - @Configuration - @EnableHypermediaSupport - static class DefaultConfig { - - } } diff --git a/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java deleted file mode 100644 index e90dadb0..00000000 --- a/src/test/java/org/springframework/hateoas/core/DefaultLinkDiscovererUnitTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012 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.core; - -import org.springframework.hateoas.LinkDiscoverer; - -/** - * Unit tests for {@link DefaultLinkDiscoverer}. - * - * @author Oliver Gierke - */ -public class DefaultLinkDiscovererUnitTest extends AbstractLinkDiscovererUnitTest { - - static final String SAMPLE = "{ links : [ " + // - "{ rel : 'self', href : 'selfHref' }, " + // - "{ rel : 'relation', href : 'firstHref' }, " + // - "{ rel : 'relation', href : 'secondHref' }]}"; - - static final LinkDiscoverer discoverer = new DefaultLinkDiscoverer(); - - @Override - protected LinkDiscoverer getDiscoverer() { - return discoverer; - } - - @Override - protected String getInputString() { - return SAMPLE; - } -} diff --git a/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTest.java index bd036955..e597cdd9 100644 --- a/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTest.java +++ b/src/test/java/org/springframework/hateoas/core/JsonPathLinkDiscovererUnitTest.java @@ -26,16 +26,16 @@ public class JsonPathLinkDiscovererUnitTest { @Test(expected = IllegalArgumentException.class) public void rejectsNullPattern() { - new JsonPathLinkDiscoverer(null); + new JsonPathLinkDiscoverer(null, null); } @Test(expected = IllegalArgumentException.class) public void rejectsPatternWithWithoutPlaceholder() { - new JsonPathLinkDiscoverer("$links"); + new JsonPathLinkDiscoverer("$links", null); } @Test(expected = IllegalArgumentException.class) public void rejectsPatternWithMultiplePlaceholders() { - new JsonPathLinkDiscoverer("$links%s%s"); + new JsonPathLinkDiscoverer("$links%s%s", null); } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index d4d4613f..c480d6dc 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -40,6 +40,7 @@ import org.springframework.hateoas.hal.Jackson1HalModule.HalHandlerInstantiator; * @author Alexander Baetz * @author Oliver Gierke */ +@SuppressWarnings("deprecation") public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTest { static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";