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\"}}}";