#728 - Add support for Spring WebFlux.

* Render hypermedia from WebFlux controllers.
* Support consuming hypermedia through WebClient.
* Support link creation reactively with a custom filter and explicitly with a user-provided ServerWebExchange.
* Add a ReactiveResourceAssembler and SimpleReactiveResourceAssembler.
* Upgrade to Spring Framework 5.2 to align with Spring Data Moore, removing specialized Forwarded header handling.
This commit is contained in:
Greg Turnquist
2019-01-17 19:39:04 -06:00
committed by Oliver Drotbohm
parent 0ec193fa26
commit 3f0dcc7cb4
45 changed files with 3004 additions and 543 deletions

43
pom.xml
View File

@@ -13,7 +13,7 @@
hyper-text driven REST web services.
</description>
<inceptionYear>2012-2018</inceptionYear>
<inceptionYear>2012-2019</inceptionYear>
<organization>
<name>Pivotal, Inc.</name>
@@ -48,7 +48,7 @@
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
<comments>
Copyright 2011-2018 the original author or authors.
Copyright 2011-2019 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -77,6 +77,7 @@
<java-module-name>spring.hateoas</java-module-name>
<jsonpath.version>2.2.0</jsonpath.version>
<minidevjson.version>2.2.1</minidevjson.version>
<reactor-bom.version>Californium-SR4</reactor-bom.version>
<slf4j.version>1.7.25</slf4j.version>
<spring.version>5.1.5.RELEASE</spring.version>
<spring-plugin.version>2.0.0.BUILD-SNAPSHOT</spring-plugin.version>
@@ -434,6 +435,18 @@
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>${reactor-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Kotlin extension -->
@@ -490,6 +503,14 @@
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>${spring.version}</version>
<optional>true</optional>
</dependency>
<dependency>
@@ -595,6 +616,24 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-extra</artifactId>
<scope>test</scope>
</dependency>
<!-- Needs to be after Jadler to make sure it sees the Servlet 3.0 dependency pulled in for testing -->
<dependency>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2018 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,6 +41,22 @@ public class ResourceSupport implements Identifiable<Link> {
this.links = new ArrayList<>();
}
public ResourceSupport(Link initialLink) {
Assert.notNull(initialLink, "initialLink must not be null!");
this.links = new ArrayList<>();
this.links.add(initialLink);
}
public ResourceSupport(List<Link> initialLinks) {
Assert.notNull(initialLinks, "initialLinks must not be null!");
this.links = new ArrayList<>();
this.links.addAll(initialLinks);
}
/**
* Returns the {@link Link} with a rel of {@link IanaLinkRelations#SELF}.
*/

View File

@@ -1,207 +0,0 @@
/*
* Copyright 2018 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.config;
import static org.springframework.hateoas.MediaTypes.*;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Configuration
@RequiredArgsConstructor
class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectProvider<ObjectMapper> mapper;
private final ObjectProvider<DelegatingRelProvider> relProvider;
private final ObjectProvider<CurieProvider> curieProvider;
private final ObjectProvider<HalConfiguration> halConfiguration;
private final ObjectProvider<HalFormsConfiguration> halFormsConfiguration;
private BeanFactory beanFactory;
private Collection<HypermediaType> hypermediaTypes;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* @param hyperMediaTypes the hyperMediaTypes to set
*/
public void setHypermediaTypes(Collection<HypermediaType> hyperMediaTypes) {
this.hypermediaTypes = hyperMediaTypes;
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List)
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
ObjectMapper objectMapper = mapper.getIfAvailable(ObjectMapper::new);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
CurieProvider curieProvider = this.curieProvider.getIfAvailable();
RelProvider relProvider = this.relProvider.getObject();
if (hypermediaTypes.contains(HypermediaType.HAL)) {
converters.add(0, createHalConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
}
if (hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
}
}
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
}
if (hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, createUberJsonConverter(objectMapper));
}
}
/**
* @param objectMapper
* @return
*/
protected MappingJackson2HttpMessageConverter createUberJsonConverter(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
// mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), mapper);
}
/**
* @param objectMapper
* @param linkRelationMessageSource
* @return
*/
protected MappingJackson2HttpMessageConverter createCollectionJsonConverter(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(COLLECTION_JSON), mapper);
}
/**
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @return
*/
private MappingJackson2HttpMessageConverter createHalFormsConverter(ObjectMapper objectMapper,
CurieProvider curieProvider, RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
true, this.halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(HAL_FORMS_JSON), mapper);
}
/**
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @return
*/
private MappingJackson2HttpMessageConverter createHalConverter(ObjectMapper objectMapper, CurieProvider curieProvider,
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
this.halConfiguration.getIfAvailable(HalConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(HAL_JSON, HAL_JSON_UTF8), mapper);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.hateoas.config;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@@ -39,6 +38,7 @@ import org.springframework.util.ClassUtils;
* Common HATEOAS specific configuration.
*
* @author Oliver Gierke
* @author Greg Turnquist
* @soundtrack Elephants Crossing - Wait (Live at Stadtfest Dresden)
* @since 0.19
*/
@@ -66,12 +66,6 @@ class HateoasConfiguration {
}
}
@Bean
ConverterRegisteringBeanPostProcessor jackson2ModuleRegisteringBeanPostProcessor(
ObjectFactory<ConverterRegisteringWebMvcConfigurer> configurer) {
return new ConverterRegisteringBeanPostProcessor(configurer);
}
// RelProvider
@Bean

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Utility class for create {@link ObjectMapper} instances of all hypermedia types.
*
* @author Greg Turnquist
* @since 1.0
*/
public final class HypermediaObjectMapperCreator {
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @param halConfiguration
* @return
*/
public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalConfiguration halConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, halConfiguration));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_FORMS_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @param halFormsConfiguration
* @return properly configured objectMapper
*/
public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalFormsConfiguration halFormsConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(
relProvider, curieProvider, linkRelationMessageSource, true,
halFormsConfiguration));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#COLLECTION_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param linkRelationMessageSource
* @return properly configured objectMapper
*/
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#UBER_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @return properly configured objectMapper
*/
public static ObjectMapper createUberObjectMapper(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return mapper;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2018 the original author or authors.
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,8 +35,11 @@ import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.mvc.WebMvcHateoasConfiguration;
import org.springframework.hateoas.config.reactive.WebFluxHateoasConfiguration;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
import org.springframework.hateoas.support.WebStack;
import org.springframework.hateoas.uber.UberLinkDiscoverer;
import org.springframework.util.ClassUtils;
@@ -62,6 +65,19 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
Collection<HypermediaType> types = Arrays.asList((HypermediaType[]) attributes.get("type"));
/*
* Register all the {@link HypermediaType}s as individual beans, so they can be gathered as needed into a
* collection using the application context.
*/
for (HypermediaType type : types) {
BeanDefinitionBuilder hypermediaTypeBeanDefinition = genericBeanDefinition(HypermediaType.class, () -> type);
registerSourcedBeanDefinition(hypermediaTypeBeanDefinition, metadata, registry);
}
/*
* Only register JSONPath-based {@link LinkDiscoverer}s based on the registered {@link HypermediaType}s.
*/
if (JSONPATH_PRESENT) {
for (HypermediaType type : types) {
@@ -72,9 +88,24 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
}
}
BeanDefinitionBuilder configurerBeanDefinition = rootBeanDefinition(ConverterRegisteringWebMvcConfigurer.class);
configurerBeanDefinition.addPropertyValue("hypermediaTypes", types);
registerSourcedBeanDefinition(configurerBeanDefinition, metadata, registry);
/*
* Register a Spring MVC-specific HATEOAS configuration.
*/
if (WebStack.WEBMVC.isAvailable()) {
BeanDefinitionBuilder webMvcHateosConfiguration = rootBeanDefinition(WebMvcHateoasConfiguration.class);
registerSourcedBeanDefinition(webMvcHateosConfiguration, metadata, registry);
}
/*
* Register a Spring WebFlux-specific HATEOAS configuration.
*/
if (WebStack.WEBFLUX.isAvailable()) {
BeanDefinitionBuilder webFluxHateoasConfiguration = rootBeanDefinition(WebFluxHateoasConfiguration.class);
registerSourcedBeanDefinition(webFluxHateoasConfiguration, metadata, registry);
}
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 the original author or authors.
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,29 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config;
package org.springframework.hateoas.config.mvc;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in the
* {@link ApplicationContext}.
* {@link BeanPostProcessor} to register hypermedia support with {@link RestTemplate} instances found in the
* application context.
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@RequiredArgsConstructor
class ConverterRegisteringBeanPostProcessor implements BeanPostProcessor {
public class HypermediaRestTemplateBeanPostProcessor implements BeanPostProcessor {
private final ObjectFactory<ConverterRegisteringWebMvcConfigurer> configurer;
private final HypermediaWebMvcConfigurer configurer;
/*
* (non-Javadoc)
@@ -45,9 +41,7 @@ class ConverterRegisteringBeanPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RestTemplate) {
ConverterRegisteringWebMvcConfigurer object = configurer.getObject();
object.extendMessageConverters(((RestTemplate) bean).getMessageConverters());
this.configurer.extendMessageConverters(((RestTemplate) bean).getMessageConverters());
}
return bean;

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.mvc;
import static org.springframework.hateoas.MediaTypes.*;
import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Configuration
@RequiredArgsConstructor
public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectMapper mapper;
private final DelegatingRelProvider relProvider;
private final CurieProvider curieProvider;
private final HalConfiguration halConfiguration;
private final HalFormsConfiguration halFormsConfiguration;
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List)
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_JSON, HAL_JSON_UTF8),
createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halConfiguration)));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_FORMS_JSON),
createHalFormsObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halFormsConfiguration)));
}
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(COLLECTION_JSON),
createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource)));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(UBER_JSON), createUberObjectMapper(this.mapper)));
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.mvc;
import java.util.Collection;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring MVC HATEOAS Configuration
*
* @author Greg Turnquist
*/
@Configuration
public class WebMvcHateoasConfiguration {
@Bean
HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new HypermediaWebMvcConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new),
hypermediaTypes);
}
@Bean
HypermediaRestTemplateBeanPostProcessor restTemplateBeanPostProcessor(HypermediaWebMvcConfigurer configurer) {
return new HypermediaRestTemplateBeanPostProcessor(configurer);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link BeanPostProcessor} to register the proper handlers in {@link WebClient} instances found in the
* application context.
*
* @author Greg Turnquist
* @since 1.0
*/
@RequiredArgsConstructor
public class HypermediaWebClientBeanPostProcessor implements BeanPostProcessor {
private final WebClientConfigurer configurer;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebClient) {
return this.configurer.registerHypermediaTypes((WebClient) bean);
}
return bean;
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.codec.StringDecoder;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link WebFluxConfigurer} to register hypermedia-aware {@link org.springframework.core.codec.Encoder}s and
* {@link org.springframework.core.codec.Decoder}s that will render hypermedia for WebFlux controllers.
*
* @author Greg Turnquist
* @since 1.0
*/
@Configuration
@RequiredArgsConstructor
public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectMapper mapper;
private final DelegatingRelProvider relProvider;
private final CurieProvider curieProvider;
private final HalConfiguration halConfiguration;
private final HalFormsConfiguration halFormsConfiguration;
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Configure custom HTTP message readers and writers or override built-in ones.
* <p>The configured readers and writers will be used for both annotated
* controllers and functional endpoints.
*
* @param configurer the configurer to use
*/
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
if (configurer.getReaders().stream()
.flatMap(httpMessageReader -> httpMessageReader.getReadableMediaTypes().stream())
.anyMatch(mediaType -> mediaType == MediaTypes.HAL_JSON
|| mediaType == MediaTypes.HAL_JSON_UTF8
|| mediaType == MediaTypes.HAL_FORMS_JSON
|| mediaType == MediaTypes.COLLECTION_JSON
|| mediaType == MediaTypes.UBER_JSON)) {
// Already configured for hypermedia!
return;
}
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
customCodecs.encoder(
new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
customCodecs.decoder(
new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
customCodecs.encoder(
new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource);
customCodecs.encoder(
new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
ObjectMapper uberObjectMapper = createUberObjectMapper(this.mapper);
customCodecs.encoder(
new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
customCodecs.decoder(StringDecoder.allMimeTypes());
configurer.registerDefaults(false);
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Assembles {@link ExchangeStrategies} needed to wire a {@link WebClient} with hypermedia support.
*
* @author Greg Turnquist
* @since 1.0
*/
@Configuration
@RequiredArgsConstructor
public class WebClientConfigurer implements BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectMapper mapper;
private final DelegatingRelProvider relProvider;
private final CurieProvider curieProvider;
private final HalConfiguration halConfiguration;
private final HalFormsConfiguration halFormsConfiguration;
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Return a set of {@link ExchangeStrategies} driven by registered {@link HypermediaType}s.
* @return a collection of {@link Encoder}s and {@link Decoder} assembled into a {@link ExchangeStrategies}.
*/
public ExchangeStrategies hypermediaExchangeStrategies() {
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
List<Encoder<?>> encoders = new ArrayList<>();
List<Decoder<?>> decoders = new ArrayList<>();
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
encoders.add(new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
decoders.add(new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
encoders.add(new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
decoders.add(new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource);
encoders.add(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
decoders.add(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
ObjectMapper uberObjectMapper = createUberObjectMapper(this.mapper);
encoders.add(new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON));
decoders.add(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
// Include ability to decode into a plain string
decoders.add(StringDecoder.allMimeTypes());
return ExchangeStrategies.builder()
.codecs(clientCodecConfigurer -> {
encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder));
decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder));
clientCodecConfigurer.registerDefaults(false);
})
.build();
}
/**
* Register the proper {@link ExchangeStrategies} for a given {@link WebClient}.
*
* @param webClient
* @return mutated webClient with hypermedia support.
*/
public WebClient registerHypermediaTypes(WebClient webClient) {
return webClient.mutate()
.exchangeStrategies(hypermediaExchangeStrategies())
.build();
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import java.util.Collection;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.reactive.HypermediaWebFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring WebFlux HATEOAS configuration.
*
* @author Greg Turnquist
* @since 1.0
*/
@Configuration
public class WebFluxHateoasConfiguration {
@Bean
WebClientConfigurer webClientConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new WebClientConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new),
hypermediaTypes);
}
@Bean
HypermediaWebClientBeanPostProcessor webClientBeanPostProcessor(WebClientConfigurer configurer) {
return new HypermediaWebClientBeanPostProcessor(configurer);
}
@Bean
HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new HypermediaWebFluxConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new), hypermediaTypes);
}
/**
* TODO: Replace with Spring Framework filter when https://github.com/spring-projects/spring-framework/issues/21746 is completed.
*/
@Bean
HypermediaWebFilter hypermediaWebFilter() {
return new HypermediaWebFilter();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.mvc;
package org.springframework.hateoas.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@@ -28,9 +28,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
@@ -87,7 +84,7 @@ class AnnotatedParametersParameterAccessor {
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}

View File

@@ -47,13 +47,6 @@ public class DummyInvocationUtils {
private static final Map<Class<?>, Class<?>> CLASS_CACHE = new ConcurrentReferenceHashMap<>(16,
ReferenceType.WEAK);
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
MethodInvocation getLastInvocation();
}
/**
* Method interceptor that records the last method invocation and creates a proxy for the return value that exposes
* the method invocation.
@@ -179,15 +172,6 @@ public class DummyInvocationUtils {
return (T) factory;
}
public interface MethodInvocation {
Object[] getArguments();
Method getMethod();
Class<?> getTargetType();
}
/**
* Returns the already created proxy class for the given source type or creates a new one.
*
@@ -213,7 +197,7 @@ public class DummyInvocationUtils {
}
@Value
static class SimpleMethodInvocation implements MethodInvocation {
private static class SimpleMethodInvocation implements MethodInvocation {
@NonNull Class<?> targetType;
@NonNull Method method;

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 java.util.Iterator;
/**
* @author Oliver Drotbohm
* @author Greg Turnquist
*/
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
MethodInvocation getLastInvocation();
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 java.lang.reflect.Method;
/**
* @author Oliver Drotbohm
* @author Greg Turnquist
*/
public interface MethodInvocation {
Object[] getArguments();
Method getMethod();
Class<?> getTargetType();
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 static org.springframework.hateoas.TemplateVariable.VariableType.*;
import static org.springframework.hateoas.TemplateVariables.*;
import static org.springframework.hateoas.core.EncodingUtils.*;
import static org.springframework.web.util.UriComponents.UriTemplateVariables.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.AnnotatedParametersParameterAccessor.BoundMethodParameter;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* Utility for taking a a method invocation and extracting a {@link ControllerLinkBuilder}.
*
* @author Greg Turnquist
*/
public class WebHandler {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new RequestParamParameterAccessor();
public static ControllerLinkBuilder linkTo(Object invocationValue,
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder,
BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
MethodInvocation invocation = invocations.getLastInvocation();
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
UriTemplate template;
if (mapping == null) {
template = new UriTemplate("/");
} else {
template = new UriTemplate(mapping);
}
Map<String, Object> values = new HashMap<>();
Iterator<String> names = template.getVariableNames().iterator();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
while (classMappingParameters.hasNext()) {
values.put(names.next(), encodePath(classMappingParameters.next()));
}
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
values.put(parameter.getVariableName(), encodePath(parameter.asString()));
}
List<String> optionalEmptyParameters = new ArrayList<>();
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
bindRequestParameters(builder, parameter);
if (SKIP_VALUE.equals(parameter.getValue())) {
values.put(parameter.getVariableName(), SKIP_VALUE);
if (!parameter.isRequired()) {
optionalEmptyParameters.add(parameter.getVariableName());
}
}
}
for (String variable : template.getVariableNames()) {
if (!values.containsKey(variable)) {
values.put(variable, SKIP_VALUE);
}
}
UriComponents components;
if (additionalUriHandler == null) {
components = builder.buildAndExpand(values);
} else {
components = additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
}
TemplateVariables variables = NONE;
for (String parameter : optionalEmptyParameters) {
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
TemplateVariable variable = new TemplateVariable(parameter,
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
variables = variables.concat(variable);
}
return new ControllerLinkBuilder(components, variables, invocation);
}
/**
* Populates the given {@link UriComponentsBuilder} with request parameters found in the given
* {@link BoundMethodParameter}.
*
* @param builder must not be {@literal null}.
* @param parameter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private static void bindRequestParameters(UriComponentsBuilder builder, BoundMethodParameter parameter) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
if (value instanceof MultiValueMap) {
MultiValueMap<String, String> requestParams = (MultiValueMap<String, String>) value;
for (Map.Entry<String, List<String>> multiValueEntry : requestParams.entrySet()) {
for (String singleEntryValue : multiValueEntry.getValue()) {
builder.queryParam(multiValueEntry.getKey(), encodeParameter(singleEntryValue));
}
}
} else if (value instanceof Map) {
Map<String, String> requestParams = (Map<String, String>) value;
for (Map.Entry<String, String> requestParamEntry : requestParams.entrySet()) {
builder.queryParam(requestParamEntry.getKey(), encodeParameter(requestParamEntry.getValue()));
}
} else if (value instanceof Collection) {
for (Object element : (Collection<?>) value) {
builder.queryParam(key, encodeParameter(element));
}
} else if (SKIP_VALUE.equals(value)) {
if (parameter.isRequired()) {
builder.queryParam(key, String.format("{%s}", parameter.getVariableName()));
}
} else {
builder.queryParam(key, encodeParameter(parameter.asString()));
}
}
/**
* Custom extension of {@link AnnotatedParametersParameterAccessor} for {@link RequestParam} to allow {@literal null}
* values handed in for optional request parameters.
*
* @author Oliver Gierke
*/
private static class RequestParamParameterAccessor extends AnnotatedParametersParameterAccessor {
public RequestParamParameterAccessor() {
super(new AnnotationAttribute(RequestParam.class));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#createParameter(org.springframework.core.MethodParameter, java.lang.Object, org.springframework.hateoas.core.AnnotationAttribute)
*/
@Override
protected BoundMethodParameter createParameter(final MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute) {
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter#isRequired()
*/
@Override
public boolean isRequired() {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
if (parameter.isOptional()) {
return false;
}
return annotation.required() //
&& annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE);
}
};
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#verifyParameterValue(org.springframework.core.MethodParameter, java.lang.Object)
*/
@Override
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
value = ObjectUtils.unwrapOptional(value);
if (value != null) {
return value;
}
if (!annotation.required() || parameter.isOptional()) {
return SKIP_VALUE;
}
return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null;
}
}
}

View File

@@ -26,9 +26,9 @@ import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.CachingMappingDiscoverer;
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.core.MethodInvocation;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
@@ -78,7 +78,7 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
this(uriComponents, TemplateVariables.NONE, null);
}
ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, MethodInvocation invocation) {
public ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, MethodInvocation invocation) {
super(uriComponents);
@@ -271,10 +271,8 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping with scheme tweaked in case the
* request contains an {@code X-Forwarded-Ssl} header, which is not (yet) supported by the underlying
* {@link UriComponentsBuilder}. If no {@link RequestContextHolder} exists (you're outside a Spring Web call), fall
* back to relative URIs.
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping. If no
* {@link RequestContextHolder} exists (you're outside a Spring Web call), fall back to relative URIs.
*
* @return
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,17 +15,10 @@
*/
package org.springframework.hateoas.mvc;
import static org.springframework.hateoas.TemplateVariable.VariableType.*;
import static org.springframework.hateoas.TemplateVariables.*;
import static org.springframework.hateoas.core.EncodingUtils.*;
import static org.springframework.web.util.UriComponents.UriTemplateVariables.*;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -33,27 +26,9 @@ import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.CachingMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
import org.springframework.hateoas.core.WebHandler;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.
@@ -132,63 +107,26 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
@Override
public ControllerLinkBuilder linkTo(Object invocationValue) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
return WebHandler.linkTo(invocationValue,
mapping -> ControllerLinkBuilder.getBuilder().path(mapping),
(builder, invocation) -> {
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
MethodInvocation invocation = invocations.getLastInvocation();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
Method method = invocation.getMethod();
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), method);
UriComponentsBuilder builder = ControllerLinkBuilder.getBuilder().path(mapping);
UriTemplate template = UriTemplateFactory.templateFor(mapping);
Map<String, Object> values = new HashMap<>();
Iterator<String> names = template.getVariableNames().iterator();
while (classMappingParameters.hasNext()) {
values.put(names.next(), encodePath(classMappingParameters.next()));
}
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
values.put(parameter.getVariableName(), encodePath(parameter.asString()));
}
List<String> optionalEmptyParameters = new ArrayList<>();
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
bindRequestParameters(builder, parameter);
if (SKIP_VALUE.equals(parameter.getValue())) {
values.put(parameter.getVariableName(), SKIP_VALUE);
if (!parameter.isRequired()) {
optionalEmptyParameters.add(parameter.getVariableName());
for (MethodParameter parameter : parameters.getParameters()) {
Object parameterValue = parameterValues.next();
for (UriComponentsContributor contributor : this.uriComponentsContributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
}
}
}
}
}
for (String variable : template.getVariableNames()) {
if (!values.containsKey(variable)) {
values.put(variable, SKIP_VALUE);
}
}
UriComponents components = applyUriComponentsContributer(builder, invocation).buildAndExpand(values);
TemplateVariables variables = NONE;
for (String parameter : optionalEmptyParameters) {
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
TemplateVariable variable = new TemplateVariable(parameter,
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
variables = variables.concat(variable);
}
return new ControllerLinkBuilder(components, variables, invocation);
return builder;
});
}
/*
@@ -199,141 +137,4 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
public ControllerLinkBuilder linkTo(Method method, Object... parameters) {
return ControllerLinkBuilder.linkTo(method, parameters);
}
/**
* Applies the configured {@link UriComponentsContributor}s to the given {@link UriComponentsBuilder}.
*
* @param builder will never be {@literal null}.
* @param invocation will never be {@literal null}.
* @return
*/
protected UriComponentsBuilder applyUriComponentsContributer(UriComponentsBuilder builder,
MethodInvocation invocation) {
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
for (MethodParameter parameter : parameters.getParameters()) {
Object parameterValue = parameterValues.next();
for (UriComponentsContributor contributor : uriComponentsContributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
}
}
}
return builder;
}
/**
* Populates the given {@link UriComponentsBuilder} with request parameters found in the given
* {@link BoundMethodParameter}.
*
* @param builder must not be {@literal null}.
* @param parameter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private static void bindRequestParameters(UriComponentsBuilder builder, BoundMethodParameter parameter) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
if (value instanceof MultiValueMap) {
MultiValueMap<String, String> requestParams = (MultiValueMap<String, String>) value;
for (Map.Entry<String, List<String>> multiValueEntry : requestParams.entrySet()) {
for (String singleEntryValue : multiValueEntry.getValue()) {
builder.queryParam(multiValueEntry.getKey(), encodeParameter(singleEntryValue));
}
}
} else if (value instanceof Map) {
Map<String, String> requestParams = (Map<String, String>) value;
for (Map.Entry<String, String> requestParamEntry : requestParams.entrySet()) {
builder.queryParam(requestParamEntry.getKey(), encodeParameter(requestParamEntry.getValue()));
}
} else if (value instanceof Collection) {
for (Object element : (Collection<?>) value) {
builder.queryParam(key, encodeParameter(element));
}
} else if (SKIP_VALUE.equals(value)) {
if (parameter.isRequired()) {
builder.queryParam(key, String.format("{%s}", parameter.getVariableName()));
}
} else {
builder.queryParam(key, encodeParameter(parameter.asString()));
}
}
/**
* Custom extension of {@link AnnotatedParametersParameterAccessor} for {@link RequestParam} to allow {@literal null}
* values handed in for optional request parameters.
*
* @author Oliver Gierke
*/
private static class RequestParamParameterAccessor extends AnnotatedParametersParameterAccessor {
public RequestParamParameterAccessor() {
super(new AnnotationAttribute(RequestParam.class));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#createParameter(org.springframework.core.MethodParameter, java.lang.Object, org.springframework.hateoas.core.AnnotationAttribute)
*/
@Override
protected BoundMethodParameter createParameter(final MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute) {
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor.BoundMethodParameter#isRequired()
*/
@Override
public boolean isRequired() {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
if (parameter.isOptional()) {
return false;
}
return annotation.required() //
&& annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE);
}
};
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor#verifyParameterValue(org.springframework.core.MethodParameter, java.lang.Object)
*/
@Override
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
RequestParam annotation = parameter.getParameterAnnotation(RequestParam.class);
value = ObjectUtils.unwrapOptional(value);
if (value != null) {
return value;
}
if (!annotation.required() || parameter.isOptional()) {
return SKIP_VALUE;
}
return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,8 +26,8 @@ import org.springframework.hateoas.AffordanceModelFactory;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.QueryParameter;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.RequestBody;
@@ -58,7 +58,10 @@ class SpringMvcAffordanceBuilder {
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), invocation.getMethod())) {
String methodName = invocation.getMethod().getName();
Link affordanceLink = new Link(components.toUriString()).withRel(LinkRelation.of(methodName));
String href = components.toUriString().equals("") ? "/" : components.toUriString();
Link affordanceLink = new Link(href).withRel(LinkRelation.of(methodName));
MethodParameters invocationMethodParameters = new MethodParameters(invocation.getMethod());
ResolvableType inputType = invocationMethodParameters.getParametersWith(RequestBody.class).stream() //

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* {@link WebFilter} that ensures a copy of the {@link ServerWebExchange} is added to the Reactor {@link Context}.
*
* @author Greg Turnquist
* @since 1.0
*/
public class HypermediaWebFilter implements WebFilter {
public static final String SERVER_WEB_EXCHANGE = "serverWebExchange";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, exchange));
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility for building reactive {@link Link}s.
*
* @author Greg Turnquist
* @since 1.0
*/
public class ReactiveLinkBuilder {
private final ControllerLinkBuilder controllerLinkBuilder;
private ReactiveLinkBuilder(ControllerLinkBuilder controllerLinkBuilder) {
this.controllerLinkBuilder = controllerLinkBuilder;
}
/**
* Create a {@link ReactiveLinkBuilder} by checking if the Reactor Context contains a {@link ServerWebExchange}
* and using that combined with the Spring Web annotations to build a full URI.
*
* If there is no exchange, then fall back to relative URIs.
*
* @param invocationValue
*/
public static Mono<ReactiveLinkBuilder> linkTo(Object invocationValue) {
return Mono.subscriberContext()
.map(context -> linkTo(invocationValue, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
}
/**
* Create a {@link ReactiveLinkBuilder} using an explicitly defined {@link ServerWebExchange}. This is possible if
* your WebFlux method includes the exchange and you want to pass it straight in.
*
* @param invocationValue
* @param exchange
*/
public static ReactiveLinkBuilder linkTo(Object invocationValue, ServerWebExchange exchange) {
ControllerLinkBuilder controllerLinkBuilder = WebHandler.linkTo(invocationValue, path -> {
UriComponentsBuilder builder = getBuilder(exchange);
if (path == null) {
builder.replacePath("/");
} else {
builder.replacePath(path);
}
return builder;
}, null);
return new ReactiveLinkBuilder(controllerLinkBuilder);
}
/**
* Utility method to transform {@link ReactiveLinkBuilder} into a {@link Link}.
*/
public Link withSelfRel() {
return this.controllerLinkBuilder.withSelfRel();
}
/**
* Utility method to transform {@link ReactiveLinkBuilder} into a {@link Link}.
*
* @param rel
*/
public Link withRel(String rel) {
return this.controllerLinkBuilder.withRel(rel);
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the {@link ServerWebExchange}.
*
* @param exchange
*/
private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) {
if (exchange == null) {
return UriComponentsBuilder.fromPath("/");
}
return UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.SimpleResourceAssembler;
import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
* @author Greg Turnquist
* @since 1.0
*/
public interface ReactiveResourceAssembler<T, D extends ResourceSupport> {
/**
* Converts the given entity into a {@code D}, which extends {@link ResourceSupport}.
*
* @param entity
* @return
*/
Mono<D> toResource(T entity, ServerWebExchange exchange);
/**
* Converts an {@link Iterable} or {@code T}s into an {@link Iterable} of {@link ResourceSupport} and wraps
* them in a {@link Resources} instance.
*
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@code D}.
*/
default Mono<Resources<D>> toResources(Flux<? extends T> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> new Resources<>(listOfResources));
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.SimpleResourceAssembler;
import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
* @author Greg Turnquist
* @since 1.0
*/
public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAssembler<T, Resource<T>> {
/**
* Converts the given entity into a {@link Resource} wrapped in a {@link Mono}.
*
* @param entity
* @return
*/
default Mono<Resource<T>> toResource(T entity, ServerWebExchange exchange) {
Resource<T> resource = new Resource<>(entity);
addLinks(resource, exchange);
return Mono.just(resource);
}
/**
* Define links to add to every individual {@link Resource}.
*
* @param resource
*/
void addLinks(Resource<T> resource, ServerWebExchange exchange);
/**
* Converts all given entities into resources and wraps the collection as a resource as well.
*
* @see #toResource(Object, ServerWebExchange)
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@link Resource} of {@code T}.
*/
default Mono<Resources<Resource<T>>> toResources(Flux<? extends T> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> {
Resources<Resource<T>> resources = new Resources<>(listOfResources);
addLinks(resources, exchange);
return resources;
});
}
/**
* Define links to add to the {@link Resources} collection.
*
* @param resources
*/
void addLinks(Resources<Resource<T>> resources, ServerWebExchange exchange);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.support;
import org.springframework.util.ClassUtils;
/**
* Utility to glean what web stack is currently available.
*
* @author Greg Turnquist
*/
public enum WebStack {
WEBMVC("org.springframework.web.servlet.DispatcherServlet"),
WEBFLUX("org.springframework.web.reactive.DispatcherHandler");
private final boolean isAvailable;
/**
* Initialize the {@link #isAvailable} based upon a defined signature class.
*/
WebStack(String signatureClass) {
this.isAvailable = ClassUtils.isPresent(signatureClass, null);
}
/**
* Is this web stack on the classpath?
*/
public boolean isAvailable() {
return this.isAvailable;
}
}

View File

@@ -15,8 +15,7 @@
*/
package org.springframework.hateoas;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.assertj.core.api.Assertions.*;
import lombok.Value;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2018 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,16 +15,19 @@
*/
package org.springframework.hateoas.client;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
@Data
public class Actor {
String name;
Actor(@JsonProperty("name") String name) {
public Actor(@JsonProperty("name") String name) {
this.name = name;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ public class Movie {
String title;
Movie(String title) {
public Movie(String title) {
this.title = title;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package org.springframework.hateoas.config;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.hal.HalConfiguration.RenderSingleLinks.*;
import static org.springframework.hateoas.support.ContextTester.withServletContext;
import java.lang.reflect.Method;
import java.util.List;
@@ -76,8 +77,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(MockitoJUnitRunner.class)
public class EnableHypermediaSupportIntegrationTest {
MockMvc mockMvc;
@Test
public void bootstrapHalConfiguration() {
assertHalSetupForConfigClass(HalConfig.class);
@@ -101,7 +100,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersHalLinkDiscoverers() {
withContext(HalConfig.class, context -> {
withServletContext(HalConfig.class, context -> {
LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class);
@@ -117,7 +116,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersHalFormsLinkDiscoverers() {
withContext(HalFormsConfig.class, context -> {
withServletContext(HalFormsConfig.class, context -> {
LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class);
@@ -131,7 +130,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersCollectionJsonLinkDiscoverers() {
withContext(CollectionJsonConfig.class, context -> {
withServletContext(CollectionJsonConfig.class, context -> {
LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class);
@@ -145,7 +144,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersUberLinkDiscoverers() {
withContext(UberConfig.class, context -> {
withServletContext(UberConfig.class, context -> {
LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class);
@@ -183,7 +182,7 @@ public class EnableHypermediaSupportIntegrationTest {
@SuppressWarnings("unchecked")
public void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() {
withContext(HalConfig.class, context -> {
withServletContext(HalConfig.class, context -> {
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
@@ -215,7 +214,7 @@ public class EnableHypermediaSupportIntegrationTest {
@SuppressWarnings("unchecked")
public void halFormsSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() {
withContext(HalFormsConfig.class, context -> {
withServletContext(HalFormsConfig.class, context -> {
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
@@ -247,7 +246,7 @@ public class EnableHypermediaSupportIntegrationTest {
@SuppressWarnings("unchecked")
public void collectionJsonSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() {
withContext(CollectionJsonConfig.class, context -> {
withServletContext(CollectionJsonConfig.class, context -> {
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
@@ -279,7 +278,7 @@ public class EnableHypermediaSupportIntegrationTest {
@SuppressWarnings("unchecked")
public void uberSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() {
withContext(UberConfig.class, context -> {
withServletContext(UberConfig.class, context -> {
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
@@ -316,7 +315,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersHalHttpMessageConvertersForRestTemplate() {
withContext(HalConfig.class, context -> {
withServletContext(HalConfig.class, context -> {
RestTemplate template = context.getBean(RestTemplate.class);
@@ -328,7 +327,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersHalFormsHttpMessageConvertersForRestTemplate() {
withContext( //
withServletContext( //
HalFormsConfig.class, //
context -> foo( //
context, //
@@ -381,7 +380,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersCollectionJsonHttpMessageConvertersForRestTemplate() {
withContext(CollectionJsonConfig.class, context -> {
withServletContext(CollectionJsonConfig.class, context -> {
RestTemplate template = context.getBean(RestTemplate.class);
assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1)
@@ -392,7 +391,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void registersUberHttpMessageConvertersForRestTemplate() {
withContext(UberConfig.class, context -> {
withServletContext(UberConfig.class, context -> {
RestTemplate template = context.getBean(RestTemplate.class);
assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()) //
@@ -407,7 +406,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void configuresDefaultObjectMapperForHalToIgnoreUnknownProperties() {
withContext( //
withServletContext( //
HalConfig.class, //
context -> assertObjectMapper( //
context, //
@@ -424,7 +423,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void configuresDefaultObjectMapperForHalFormsToIgnoreUnknownProperties() {
withContext( //
withServletContext( //
HalFormsConfig.class, //
context -> assertObjectMapper( //
context, //
@@ -438,7 +437,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void configuresDefaultObjectMapperForCollectionJsonToIgnoreUnknownProperties() {
withContext( //
withServletContext( //
CollectionJsonConfig.class, //
context -> assertObjectMapper( //
context, //
@@ -452,7 +451,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void configuresDefaultObjectMapperForUberToIgnoreUnknownProperties() {
withContext( //
withServletContext( //
UberConfig.class, //
context -> assertObjectMapper( //
context, //
@@ -466,7 +465,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException {
withContext(HalConfig.class, context -> {
withServletContext(HalConfig.class, context -> {
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
@@ -492,7 +491,7 @@ public class EnableHypermediaSupportIntegrationTest {
@Test
public void verifyRenderSingleLinkAsArrayViaOverridingBean() {
withContext( //
withServletContext( //
RenderLinkAsSingleLinksConfig.class, //
context -> assertObjectMapper( //
context, //
@@ -507,19 +506,6 @@ public class EnableHypermediaSupportIntegrationTest {
);
}
private static <E extends Exception> void withContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.register(configuration);
context.setServletContext(new MockServletContext());
context.refresh();
consumer.accept(context);
}
}
private static void assertEntityLinksSetUp(ApplicationContext context) {
assertThat(context.getBeansOfType(EntityLinks.class).values()) //
@@ -534,7 +520,7 @@ public class EnableHypermediaSupportIntegrationTest {
private static void assertHalSetupForConfigClass(Class<?> configClass) {
withContext(configClass, context -> {
withServletContext(configClass, context -> {
assertEntityLinksSetUp(context);
assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(HalLinkDiscoverer.class);
@@ -547,7 +533,7 @@ public class EnableHypermediaSupportIntegrationTest {
private static void assertHalFormsSetupForConfigClass(Class<?> configClass) {
withContext(configClass, context -> {
withServletContext(configClass, context -> {
assertEntityLinksSetUp(context);
assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(HalFormsLinkDiscoverer.class);
@@ -560,7 +546,7 @@ public class EnableHypermediaSupportIntegrationTest {
private static void assertCollectionJsonSetupForConfigClass(Class<?> configClass) {
withContext(configClass, context -> {
withServletContext(configClass, context -> {
assertEntityLinksSetUp(context);
assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(CollectionJsonLinkDiscoverer.class);
@@ -572,7 +558,7 @@ public class EnableHypermediaSupportIntegrationTest {
private static void assertUberSetupForConfigClass(Class<?> configClass) {
withContext(configClass, context -> {
withServletContext(configClass, context -> {
assertEntityLinksSetUp(context);
assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(UberLinkDiscoverer.class);

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.mvc;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.support.ContextTester.*;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
/**
* @author Greg Turnquist
*/
public class HypermediaRestTemplateBeanPostProcessorTest {
@Test
public void shouldRegisterJustHal() {
withContext(HalConfig.class, context -> {
assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class)))
.containsExactlyInAnyOrder(
MediaTypes.HAL_JSON,
MediaTypes.HAL_JSON_UTF8,
MediaType.APPLICATION_JSON,
MediaType.parseMediaType("application/*+json"));
});
}
@Test
public void shouldRegisterHalAndCollectionJsonMessageConverters() {
withContext(HalAndCollectionJsonConfig.class, context -> {
assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class)))
.containsExactlyInAnyOrder(
MediaTypes.HAL_JSON,
MediaTypes.HAL_JSON_UTF8,
MediaTypes.COLLECTION_JSON,
MediaType.APPLICATION_JSON,
MediaType.parseMediaType("application/*+json"));
});
}
@Test
public void shouldRegisterHypermediaMessageConverters() {
withContext(AllHypermediaConfig.class, context -> {
assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class)))
.containsExactlyInAnyOrder(
MediaTypes.HAL_JSON,
MediaTypes.HAL_JSON_UTF8,
MediaTypes.HAL_FORMS_JSON,
MediaTypes.COLLECTION_JSON,
MediaTypes.UBER_JSON,
MediaType.APPLICATION_JSON,
MediaType.parseMediaType("application/*+json"));
});
}
private List<MediaType> lookupSupportedHypermediaTypes(RestTemplate restTemplate) {
return restTemplate.getMessageConverters().stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractHttpMessageConverter::getSupportedMediaTypes)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
static class BaseConfig {
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
static class HalConfig extends BaseConfig {
}
@Configuration
@EnableHypermediaSupport(type = {HypermediaType.HAL, HypermediaType.COLLECTION_JSON})
static class HalAndCollectionJsonConfig extends BaseConfig {
}
@Configuration
@EnableHypermediaSupport(type = {HypermediaType.HAL, HypermediaType.HAL_FORMS, HypermediaType.COLLECTION_JSON, HypermediaType.UBER})
static class AllHypermediaConfig extends BaseConfig {
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.support.ContextTester.*;
import java.io.IOException;
import java.net.URI;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.client.Actor;
import org.springframework.hateoas.client.Movie;
import org.springframework.hateoas.client.Server;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.mvc.TypeReferences.ResourceType;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Greg Turnquist
*/
public class HypermediaWebClientBeanPostProcessorTest {
private URI baseUri;
private Server server;
@Before
public void setUp() {
this.server = new Server();
Resource<Actor> actor = new Resource<>(new Actor("Keanu Reaves"));
String actorUri = this.server.mockResourceFor(actor);
Movie movie = new Movie("The Matrix");
Resource<Movie> resource = new Resource<>(movie);
resource.add(new Link(actorUri, "actor"));
this.server.mockResourceFor(resource);
this.server.finishMocking();
this.baseUri = URI.create(this.server.rootResource());
}
@After
public void tearDown() throws IOException {
if (this.server != null) {
this.server.close();
}
}
@Test
public void shouldHandleRootHalDocument() {
withContext(HalConfig.class, context -> {
WebClient webClient = context.getBean(WebClient.class);
webClient
.get().uri(this.baseUri)
.accept(MediaTypes.HAL_JSON)
.retrieve()
.bodyToMono(ResourceSupport.class)
.as(StepVerifier::create)
.expectNextMatches(root -> {
assertThat(root.getLinks()).hasSize(2);
return true;
})
.verifyComplete();
});
}
@Test
public void shouldHandleNavigatingToAResourceObject() {
ParameterizedTypeReference<Resource<Actor>> typeReference = new ResourceType<Actor>() {};
withContext(HalConfig.class, context -> {
WebClient webClient = context.getBean(WebClient.class);
webClient
.get().uri(this.baseUri)
.retrieve()
.bodyToMono(ResourceSupport.class)
.map(resourceSupport -> resourceSupport.getLink("actors").orElseThrow(() -> new RuntimeException("Link not found!")))
.flatMap(link -> webClient
.get().uri(link.expand().getHref())
.retrieve()
.bodyToMono(ResourceSupport.class))
.map(resourceSupport -> resourceSupport.getLinks().get(0))
.flatMap(link -> webClient
.get().uri(link.expand().getHref())
.retrieve()
.bodyToMono(typeReference))
.as(StepVerifier::create)
.expectNext(new Resource<>(new Actor("Keanu Reaves")))
.verifyComplete();
});
}
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL)
static class HalConfig {
@Bean
WebClient webClient() {
return WebClient.create();
}
}
}

View File

@@ -0,0 +1,477 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.config.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.SimpleResourceAssembler;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.mvc.TypeReferences.ResourceType;
import org.springframework.hateoas.mvc.TypeReferences.ResourcesType;
import org.springframework.hateoas.support.Employee;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author Greg Turnquist
*/
public class HypermediaWebFluxConfigurerTest {
ParameterizedTypeReference<Resource<Employee>> resourceEmployeeType = new ResourceType<Employee>() {};
ParameterizedTypeReference<Resources<Resource<Employee>>> resourcesEmployeeType = new ResourcesType<Resource<Employee>>() {};
WebTestClient testClient;
void setUp(Class<?> context) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(context);
ctx.refresh();
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
}
@Test
public void registeringHalShouldServeHal() {
setUp(HalWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
}
@Test
public void registeringHalFormsShouldServeHalForms() {
setUp(HalFormsWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
}
@Test
public void registeringCollectionJsonShouldServerCollectionJson() {
setUp(CollectionJsonWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.COLLECTION_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.COLLECTION_JSON);
}
@Test
public void registeringUberShouldServerUber() {
setUp(UberWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.UBER_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.UBER_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.UBER_JSON);
}
@Test
public void registeringHalAndHalFormsShouldServerHalAndHalForms() {
setUp(AllHalWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyRootUriServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
}
@Test
public void registeringHalAndCollectionJsonShouldServerHalAndCollectionJson() {
setUp(HalAndCollectionJsonWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyRootUriServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyRootUriServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.COLLECTION_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.COLLECTION_JSON);
}
@Test
public void registeringAllHypermediaTypesShouldServerThemAll() {
setUp(AllHypermediaTypesWebFluxConfig.class);
verifyRootUriServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyRootUriServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyRootUriServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.COLLECTION_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyRootUriServesHypermedia(MediaTypes.UBER_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.UBER_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.UBER_JSON);
}
@Test
public void callingForUnregisteredMediaTypeShouldFail() {
setUp(HalWebFluxConfig.class);
this.testClient.get().uri("/")
.accept(MediaTypes.UBER_JSON)
.exchange()
.expectStatus().is4xxClientError()
.returnResult(String.class)
.getResponseBody()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
public void reactorTypesShouldWork() {
setUp(HalWebFluxConfig.class);
this.testClient.get().uri("/reactive")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactlyInAnyOrder(
new Link("/", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")
);
return true;
})
.verifyComplete();
this.testClient.get().uri("/reactive/employees")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(this.resourcesEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resources -> {
assertThat(resources.getContent()).extracting("content").containsExactlyInAnyOrder(
new Employee("Frodo Baggins", "ring bearer"));
assertThat(resources.getContent()).extracting("links").containsExactlyInAnyOrder(
Arrays.asList(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")));
assertThat(resources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", IanaLinkRelation.SELF.value()));
return true;
})
.verifyComplete();
this.testClient.get().uri("/reactive/employees/1")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(this.resourceEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(employee -> {
System.out.println(employee);
assertThat(employee.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(employee.getLinks()).containsExactlyInAnyOrder(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")
);
return true;
})
.verifyComplete();
}
private void verifyRootUriServesHypermedia(MediaType mediaType) {
verifyRootUriServesHypermedia(mediaType, mediaType);
}
private void verifyRootUriServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactlyInAnyOrder(
new Link("/", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"));
return true;
})
.verifyComplete();
}
private void verifyAggregateRootServesHypermedia(MediaType mediaType) {
verifyAggregateRootServesHypermedia(mediaType, mediaType);
}
private void verifyAggregateRootServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/employees")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(this.resourcesEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resources -> {
assertThat(resources.getContent()).hasSize(1);
assertThat(resources.getContent()).extracting("content").containsExactlyInAnyOrder(
new Employee("Frodo Baggins", "ring bearer")
);
assertThat(resources.getContent()).extracting("links").containsExactlyInAnyOrder(
Arrays.asList(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"))
);
assertThat(resources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", IanaLinkRelation.SELF.value())
);
return true;
})
.verifyComplete();
}
private void verifySingleItemResourceServesHypermedia(MediaType mediaType) {
verifySingleItemResourceServesHypermedia(mediaType, mediaType);
}
private void verifySingleItemResourceServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/employees/1")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(this.resourceEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(employeeResource -> {
assertThat(employeeResource.getContent()).isEqualTo(
new Employee("Frodo Baggins", "ring bearer"));
assertThat(employeeResource.getLinks()).containsExactlyInAnyOrder(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"));
return true;
})
.verifyComplete();
}
@Configuration
@EnableWebFlux
static abstract class BaseConfig {
@Bean
TestController testController() {
return new TestController();
}
}
@EnableHypermediaSupport(type = HAL)
static class HalWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = HAL_FORMS)
static class HalFormsWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = COLLECTION_JSON)
static class CollectionJsonWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = UBER)
static class UberWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS})
static class AllHalWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS, COLLECTION_JSON})
static class HalAndCollectionJsonWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS, COLLECTION_JSON, UBER})
static class AllHypermediaTypesWebFluxConfig extends BaseConfig {
}
@RestController
static class TestController {
private List<Employee> employees;
private EmployeeResourceAssembler assembler = new EmployeeResourceAssembler();
TestController() {
this.employees = new ArrayList<>();
this.employees.add(new Employee("Frodo Baggins", "ring bearer"));
}
@GetMapping
ResourceSupport root() {
ResourceSupport root = new ResourceSupport();
root.add(new Link("/").withSelfRel());
root.add(new Link("/employees").withRel("employees"));
return root;
}
@GetMapping("/employees")
Resources<Resource<Employee>> employees() {
return this.assembler.toResources(this.employees);
}
@PostMapping("/employees")
Resource<Employee> newEmployee(@RequestBody Employee newEmployee) {
this.employees.add(newEmployee);
return this.assembler.toResource(newEmployee);
}
@GetMapping("/employees/{id}")
Resource<Employee> employee(@PathVariable String id) {
return this.assembler.toResource(this.employees.get(0));
}
@PutMapping("/employees/{id}")
Resource<Employee> updateEmployee(@RequestBody Employee newEmployee, @PathVariable String id) {
this.employees.add(newEmployee);
return this.assembler.toResource(newEmployee);
}
@GetMapping("/reactive")
Mono<ResourceSupport> reactiveRoot() {
return Mono.just(root());
}
@GetMapping("/reactive/employees")
Mono<Resources<Resource<Employee>>> reactiveEmployees() {
return findAll()
.collectList()
.map(employees -> this.assembler.toResources(employees));
}
@GetMapping("/reactive/employees/{id}")
Mono<Resource<Employee>> reactiveEmployee(@PathVariable String id) {
return findById(0)
.map(this.assembler::toResource);
}
Mono<Employee> findById(int id) {
return Mono.just(this.employees.get(id));
}
Flux<Employee> findAll() {
return Flux.fromIterable(this.employees);
}
}
static class EmployeeResourceAssembler implements SimpleResourceAssembler<Employee> {
@Override
public void addLinks(Resource<Employee> resource) {
resource.add(new Link("/employees/1").withSelfRel());
resource.add(new Link("/employees").withRel("employees"));
}
@Override
public void addLinks(Resources<Resource<Employee>> resources) {
resources.add(new Link("/employees").withSelfRel());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,8 @@
package org.springframework.hateoas.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2018 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 the original author or authors.
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,9 @@
package org.springframework.hateoas.mvc;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
import static org.springframework.util.ReflectionUtils.*;
@@ -50,7 +52,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
/**
* Unit tests for {@link org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler}.
* Unit tests for {@link ResourceProcessorHandlerMethodReturnValueHandler}.
*
* @author Oliver Gierke
* @author Jon Brisbin

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import static org.springframework.hateoas.core.DummyInvocationUtils.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.*;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.reactive.WebClientConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author Greg Turnquist
*/
public class HypermediaWebFilterTest {
WebTestClient testClient;
@Before
public void setUp() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(WebFluxConfig.class);
ctx.refresh();
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
}
@Test
public void webFilterShouldEmbedExchangeIntoContext() {
this.testClient.get().uri("http://example.com/api")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactly(
new Link("http://example.com/api", IanaLinkRelation.SELF.value()));
return true;
})
.verifyComplete();
}
@RestController
@RequestMapping("/api")
static class TestController {
@GetMapping
Mono<ResourceSupport> root() {
return linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.map(ResourceSupport::new);
}
}
@Configuration
@EnableWebFlux
@EnableHypermediaSupport(type = HAL)
static class WebFluxConfig {
@Bean
TestController controller() {
return new TestController();
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.linkTo;
import java.net.URI;
import java.net.URISyntaxException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Greg Turnquist
*/
@RunWith(MockitoJUnitRunner.class)
public class ReactiveLinkBuilderTest {
@Mock ServerWebExchange exchange;
@Mock ServerHttpRequest request;
@Test
public void linkAtSameLevelAsExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
}
@Test
public void linkAtSameLevelAsContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
})
.verifyComplete();
}
@Test
public void shallowLinkFromDeepExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api/employees"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
}
@Test
public void shallowLinkFromDeepContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api/employees"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
})
.verifyComplete();
}
@Test
public void deepLinkFromShallowExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).deep(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
}
@Test
public void deepLinkFromShallowContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).deep())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
return true;
})
.verifyComplete();
}
@Test
public void linkToRouteWithNoMappingShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController2.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/");
return true;
})
.verifyComplete();
}
@Test
public void linkToRouteWithNoExchangeInTheContextShouldFallbackToRelativeUris() throws URISyntaxException {
linkTo(methodOn(TestController2.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("/");
return true;
})
.verifyComplete();
}
@Test
public void linkToRouteWithExplictExchangeBeingNullShouldFallbackToRelativeUris() throws URISyntaxException {
Link link = linkTo(methodOn(TestController2.class).root(), null).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("/");
}
@RestController
@RequestMapping("/api")
static class TestController {
@GetMapping
Mono<Object> root() {
return Mono.empty();
}
@GetMapping("/employees")
Mono<Object> deep() {
return Mono.empty();
}
}
@RestController
static class TestController2 {
@GetMapping
Mono<Object> root() {
return Mono.empty();
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Collections;
import org.assertj.core.api.AssertionsForInterfaceTypes;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Greg Turnquist
*/
public class ReactiveResourceAssemblerUnitTest {
TestAssembler assembler;
TestAssemblerWithCustomResources assemblerWithCustomResources;
Employee employee;
ServerWebExchange exchange;
@Before
public void setUp() {
this.assembler = new TestAssembler();
this.assemblerWithCustomResources = new TestAssemblerWithCustomResources();
this.employee = new Employee("Frodo Baggins");
this.exchange = mock(ServerWebExchange.class);
}
@Test
public void simpleConversionShouldWork() {
this.assembler.toResource(this.employee, this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResource -> {
assertThat(employeeResource.getEmployee()).isEqualTo(new Employee("Frodo Baggins"));
AssertionsForInterfaceTypes.assertThat(employeeResource.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", "employees"));
return true;
})
.verifyComplete();
}
@Test
public void defaultResourcesConversionShouldWork() {
this.assembler.toResources(Flux.just(this.employee), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResources -> {
assertThat(employeeResources.getContent()).extracting("employee").containsExactly(
new Employee("Frodo Baggins"));
assertThat(employeeResources.getContent()).extracting("links").containsExactly(
Collections.singletonList(new Link("/employees", "employees")));
assertThat(employeeResources.getLinks()).isEmpty();
return true;
})
.verifyComplete();
}
@Test
public void customResourcesShouldWork() {
this.assemblerWithCustomResources.toResources(Flux.just(this.employee), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResources -> {
AssertionsForInterfaceTypes.assertThat(employeeResources.getContent()).extracting("employee").containsExactly(
new Employee("Frodo Baggins"));
AssertionsForInterfaceTypes.assertThat(employeeResources.getContent()).extracting("links").containsExactly(
Collections.singletonList(new Link("/employees", "employees")));
AssertionsForInterfaceTypes.assertThat(employeeResources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees").withSelfRel(),
new Link("/", "root")
);
return true;
})
.verifyComplete();
}
class TestAssembler implements ReactiveResourceAssembler<Employee, EmployeeResource> {
@Override
public Mono<EmployeeResource> toResource(Employee entity, ServerWebExchange exchange) {
EmployeeResource employeeResource = new EmployeeResource(entity);
employeeResource.add(new Link("/employees", "employees"));
return Mono.just(employeeResource);
}
}
class TestAssemblerWithCustomResources extends TestAssembler {
@Override
public Mono<Resources<EmployeeResource>> toResources(Flux<? extends Employee> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> {
Resources<EmployeeResource> employeeResources = new Resources<>(listOfResources);
employeeResources.add(new Link("/employees").withSelfRel());
employeeResources.add(new Link("/", "root"));
return employeeResources;
});
}
}
@Data
@AllArgsConstructor
class Employee {
private String name;
}
@Data
@AllArgsConstructor
class EmployeeResource extends ResourceSupport {
private Employee employee;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.reactive;
import static org.assertj.core.api.Assertions.*;
import lombok.Data;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Greg Turnquist
*/
public class SimpleReactiveResourceAssemblerTest {
TestResourceAssemblerSimple testResourceAssembler;
ResourceAssemblerWithCustomLinkSimple resourceAssemblerWithCustomLink;
@Mock ServerWebExchange exchange;
@Before
public void setUp() {
this.testResourceAssembler = new TestResourceAssemblerSimple();
this.resourceAssemblerWithCustomLink = new ResourceAssemblerWithCustomLinkSimple();
}
/**
* @see #
*/
@Test
public void convertingToResourceShouldWork() {
this.testResourceAssembler.toResource(new Employee("Frodo"), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(resource -> {
assertThat(resource.getContent().getName()).isEqualTo("Frodo");
assertThat(resource.getLinks()).isEmpty();
return true;
})
.verifyComplete();
}
/**
* @see #
*/
@Test
public void convertingToResourcesShouldWork() {
this.testResourceAssembler.toResources(Flux.just(new Employee("Frodo")), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(resources -> {
assertThat(resources.getContent()).containsExactly(new Resource<>(new Employee("Frodo")));
assertThat(resources.getLinks()).isEmpty();
return true;
});
}
/**
* @see #
*/
@Test
public void convertingToResourceWithCustomLinksShouldWork() {
this.resourceAssemblerWithCustomLink.toResource(new Employee("Frodo"), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(resource -> {
assertThat(resource.getContent().getName()).isEqualTo("Frodo");
assertThat(resource.getLinks()).containsExactly(new Link("/employees").withRel("employees"));
return true;
})
.verifyComplete();
}
/**
* @see #
*/
@Test
public void convertingToResourcesWithCustomLinksShouldWork() {
this.resourceAssemblerWithCustomLink.toResources(Flux.just(new Employee("Frodo")), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(resources -> {
assertThat(resources.getContent()).containsExactly(
new Resource<>(new Employee("Frodo"), new Link("/employees").withRel("employees")));
assertThat(resources.getLinks()).containsExactly(new Link("/", "root"));
return true;
})
.verifyComplete();
}
class TestResourceAssemblerSimple implements SimpleReactiveResourceAssembler<Employee> {
@Override
public void addLinks(Resource<Employee> resource, ServerWebExchange exchange) {
}
@Override
public void addLinks(Resources<Resource<Employee>> resources, ServerWebExchange exchange) {
}
}
class ResourceAssemblerWithCustomLinkSimple implements SimpleReactiveResourceAssembler<Employee> {
@Override
public void addLinks(Resource<Employee> resource, ServerWebExchange exchange) {
resource.add(new Link("/employees").withRel("employees"));
}
@Override
public void addLinks(Resources<Resource<Employee>> resources, ServerWebExchange exchange) {
resources.add(new Link("/").withRel("root"));
}
}
@Data
class Employee {
private final String name;
}
}

View File

@@ -15,10 +15,9 @@
*/
package org.springframework.hateoas.support;
import net.minidev.json.JSONArray;
import java.util.Iterator;
import net.minidev.json.JSONArray;
import org.springframework.web.client.RestTemplate;
import com.jayway.jsonpath.JsonPath;

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.support;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
/**
* @author Greg Turnquist
*/
public class ContextTester {
public static <E extends Exception> void withContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.register(configuration);
context.refresh();
consumer.accept(context);
}
}
public static <E extends Exception> void withServletContext(Class<?> configuration,
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.register(configuration);
context.setServletContext(new MockServletContext());
context.refresh();
consumer.accept(context);
}
}
public interface ConsumerWithException<T, E extends Exception> {
void accept(T element) throws E;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2018 the original author or authors.
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,14 +15,13 @@
*/
package org.springframework.hateoas.uber;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.core.AbstractLinkDiscovererUnitTest;