#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:
committed by
Oliver Drotbohm
parent
0ec193fa26
commit
3f0dcc7cb4
43
pom.xml
43
pom.xml
@@ -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>
|
||||
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
246
src/main/java/org/springframework/hateoas/core/WebHandler.java
Normal file
246
src/main/java/org/springframework/hateoas/core/WebHandler.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() //
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.*;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user