#728 - Add support for Spring WebFlux.

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

View File

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

View File

@@ -1,207 +0,0 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config;
import static org.springframework.hateoas.MediaTypes.*;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Configuration
@RequiredArgsConstructor
class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectProvider<ObjectMapper> mapper;
private final ObjectProvider<DelegatingRelProvider> relProvider;
private final ObjectProvider<CurieProvider> curieProvider;
private final ObjectProvider<HalConfiguration> halConfiguration;
private final ObjectProvider<HalFormsConfiguration> halFormsConfiguration;
private BeanFactory beanFactory;
private Collection<HypermediaType> hypermediaTypes;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* @param hyperMediaTypes the hyperMediaTypes to set
*/
public void setHypermediaTypes(Collection<HypermediaType> hyperMediaTypes) {
this.hypermediaTypes = hyperMediaTypes;
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List)
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
ObjectMapper objectMapper = mapper.getIfAvailable(ObjectMapper::new);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
CurieProvider curieProvider = this.curieProvider.getIfAvailable();
RelProvider relProvider = this.relProvider.getObject();
if (hypermediaTypes.contains(HypermediaType.HAL)) {
converters.add(0, createHalConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
}
if (hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
}
}
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
}
if (hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, createUberJsonConverter(objectMapper));
}
}
/**
* @param objectMapper
* @return
*/
protected MappingJackson2HttpMessageConverter createUberJsonConverter(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
// mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), mapper);
}
/**
* @param objectMapper
* @param linkRelationMessageSource
* @return
*/
protected MappingJackson2HttpMessageConverter createCollectionJsonConverter(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(COLLECTION_JSON), mapper);
}
/**
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @return
*/
private MappingJackson2HttpMessageConverter createHalFormsConverter(ObjectMapper objectMapper,
CurieProvider curieProvider, RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
true, this.halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(HAL_FORMS_JSON), mapper);
}
/**
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @return
*/
private MappingJackson2HttpMessageConverter createHalConverter(ObjectMapper objectMapper, CurieProvider curieProvider,
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
this.halConfiguration.getIfAvailable(HalConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(HAL_JSON, HAL_JSON_UTF8), mapper);
}
}

View File

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

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Utility class for create {@link ObjectMapper} instances of all hypermedia types.
*
* @author Greg Turnquist
* @since 1.0
*/
public final class HypermediaObjectMapperCreator {
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @param halConfiguration
* @return
*/
public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalConfiguration halConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, halConfiguration));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_FORMS_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param curieProvider
* @param relProvider
* @param linkRelationMessageSource
* @param halFormsConfiguration
* @return properly configured objectMapper
*/
public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalFormsConfiguration halFormsConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(
relProvider, curieProvider, linkRelationMessageSource, true,
halFormsConfiguration));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#COLLECTION_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @param linkRelationMessageSource
* @return properly configured objectMapper
*/
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
return mapper;
}
/**
* Create a {@link org.springframework.hateoas.MediaTypes#UBER_JSON}-based {@link ObjectMapper}.
*
* @param objectMapper
* @return properly configured objectMapper
*/
public static ObjectMapper createUberObjectMapper(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return mapper;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config.mvc;
import static org.springframework.hateoas.MediaTypes.*;
import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Configuration
@RequiredArgsConstructor
public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private final ObjectMapper mapper;
private final DelegatingRelProvider relProvider;
private final CurieProvider curieProvider;
private final HalConfiguration halConfiguration;
private final HalFormsConfiguration halFormsConfiguration;
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List)
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_JSON, HAL_JSON_UTF8),
createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halConfiguration)));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_FORMS_JSON),
createHalFormsObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halFormsConfiguration)));
}
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(COLLECTION_JSON),
createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource)));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(UBER_JSON), createUberObjectMapper(this.mapper)));
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config.mvc;
import java.util.Collection;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring MVC HATEOAS Configuration
*
* @author Greg Turnquist
*/
@Configuration
public class WebMvcHateoasConfiguration {
@Bean
HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new HypermediaWebMvcConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new),
hypermediaTypes);
}
@Bean
HypermediaRestTemplateBeanPostProcessor restTemplateBeanPostProcessor(HypermediaWebMvcConfigurer configurer) {
return new HypermediaRestTemplateBeanPostProcessor(configurer);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config.reactive;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link BeanPostProcessor} to register the proper handlers in {@link WebClient} instances found in the
* application context.
*
* @author Greg Turnquist
* @since 1.0
*/
@RequiredArgsConstructor
public class HypermediaWebClientBeanPostProcessor implements BeanPostProcessor {
private final WebClientConfigurer configurer;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebClient) {
return this.configurer.registerHypermediaTypes((WebClient) bean);
}
return bean;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config.reactive;
import java.util.Collection;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.reactive.HypermediaWebFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring WebFlux HATEOAS configuration.
*
* @author Greg Turnquist
* @since 1.0
*/
@Configuration
public class WebFluxHateoasConfiguration {
@Bean
WebClientConfigurer webClientConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new WebClientConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new),
hypermediaTypes);
}
@Bean
HypermediaWebClientBeanPostProcessor webClientBeanPostProcessor(WebClientConfigurer configurer) {
return new HypermediaWebClientBeanPostProcessor(configurer);
}
@Bean
HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider<ObjectMapper> mapper,
DelegatingRelProvider relProvider,
ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
Collection<HypermediaType> hypermediaTypes) {
return new HypermediaWebFluxConfigurer(
mapper.getIfAvailable(ObjectMapper::new),
relProvider,
curieProvider.getIfAvailable(),
halConfiguration.getIfAvailable(HalConfiguration::new),
halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new), hypermediaTypes);
}
/**
* TODO: Replace with Spring Framework filter when https://github.com/spring-projects/spring-framework/issues/21746 is completed.
*/
@Bean
HypermediaWebFilter hypermediaWebFilter() {
return new HypermediaWebFilter();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
import java.util.Iterator;
/**
* @author Oliver Drotbohm
* @author Greg Turnquist
*/
public interface LastInvocationAware {
Iterator<Object> getObjectParameters();
MethodInvocation getLastInvocation();
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
import java.lang.reflect.Method;
/**
* @author Oliver Drotbohm
* @author Greg Turnquist
*/
public interface MethodInvocation {
Object[] getArguments();
Method getMethod();
Class<?> getTargetType();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.reactive;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
/**
* {@link WebFilter} that ensures a copy of the {@link ServerWebExchange} is added to the Reactor {@link Context}.
*
* @author Greg Turnquist
* @since 1.0
*/
public class HypermediaWebFilter implements WebFilter {
public static final String SERVER_WEB_EXCHANGE = "serverWebExchange";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, exchange));
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.reactive;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility for building reactive {@link Link}s.
*
* @author Greg Turnquist
* @since 1.0
*/
public class ReactiveLinkBuilder {
private final ControllerLinkBuilder controllerLinkBuilder;
private ReactiveLinkBuilder(ControllerLinkBuilder controllerLinkBuilder) {
this.controllerLinkBuilder = controllerLinkBuilder;
}
/**
* Create a {@link ReactiveLinkBuilder} by checking if the Reactor Context contains a {@link ServerWebExchange}
* and using that combined with the Spring Web annotations to build a full URI.
*
* If there is no exchange, then fall back to relative URIs.
*
* @param invocationValue
*/
public static Mono<ReactiveLinkBuilder> linkTo(Object invocationValue) {
return Mono.subscriberContext()
.map(context -> linkTo(invocationValue, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
}
/**
* Create a {@link ReactiveLinkBuilder} using an explicitly defined {@link ServerWebExchange}. This is possible if
* your WebFlux method includes the exchange and you want to pass it straight in.
*
* @param invocationValue
* @param exchange
*/
public static ReactiveLinkBuilder linkTo(Object invocationValue, ServerWebExchange exchange) {
ControllerLinkBuilder controllerLinkBuilder = WebHandler.linkTo(invocationValue, path -> {
UriComponentsBuilder builder = getBuilder(exchange);
if (path == null) {
builder.replacePath("/");
} else {
builder.replacePath(path);
}
return builder;
}, null);
return new ReactiveLinkBuilder(controllerLinkBuilder);
}
/**
* Utility method to transform {@link ReactiveLinkBuilder} into a {@link Link}.
*/
public Link withSelfRel() {
return this.controllerLinkBuilder.withSelfRel();
}
/**
* Utility method to transform {@link ReactiveLinkBuilder} into a {@link Link}.
*
* @param rel
*/
public Link withRel(String rel) {
return this.controllerLinkBuilder.withRel(rel);
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the {@link ServerWebExchange}.
*
* @param exchange
*/
private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) {
if (exchange == null) {
return UriComponentsBuilder.fromPath("/");
}
return UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.SimpleResourceAssembler;
import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
* @author Greg Turnquist
* @since 1.0
*/
public interface ReactiveResourceAssembler<T, D extends ResourceSupport> {
/**
* Converts the given entity into a {@code D}, which extends {@link ResourceSupport}.
*
* @param entity
* @return
*/
Mono<D> toResource(T entity, ServerWebExchange exchange);
/**
* Converts an {@link Iterable} or {@code T}s into an {@link Iterable} of {@link ResourceSupport} and wraps
* them in a {@link Resources} instance.
*
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@code D}.
*/
default Mono<Resources<D>> toResources(Flux<? extends T> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> new Resources<>(listOfResources));
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.SimpleResourceAssembler;
import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
* @author Greg Turnquist
* @since 1.0
*/
public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAssembler<T, Resource<T>> {
/**
* Converts the given entity into a {@link Resource} wrapped in a {@link Mono}.
*
* @param entity
* @return
*/
default Mono<Resource<T>> toResource(T entity, ServerWebExchange exchange) {
Resource<T> resource = new Resource<>(entity);
addLinks(resource, exchange);
return Mono.just(resource);
}
/**
* Define links to add to every individual {@link Resource}.
*
* @param resource
*/
void addLinks(Resource<T> resource, ServerWebExchange exchange);
/**
* Converts all given entities into resources and wraps the collection as a resource as well.
*
* @see #toResource(Object, ServerWebExchange)
* @param entities must not be {@literal null}.
* @return {@link Resources} containing {@link Resource} of {@code T}.
*/
default Mono<Resources<Resource<T>>> toResources(Flux<? extends T> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> {
Resources<Resource<T>> resources = new Resources<>(listOfResources);
addLinks(resources, exchange);
return resources;
});
}
/**
* Define links to add to the {@link Resources} collection.
*
* @param resources
*/
void addLinks(Resources<Resource<T>> resources, ServerWebExchange exchange);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.support;
import org.springframework.util.ClassUtils;
/**
* Utility to glean what web stack is currently available.
*
* @author Greg Turnquist
*/
public enum WebStack {
WEBMVC("org.springframework.web.servlet.DispatcherServlet"),
WEBFLUX("org.springframework.web.reactive.DispatcherHandler");
private final boolean isAvailable;
/**
* Initialize the {@link #isAvailable} based upon a defined signature class.
*/
WebStack(String signatureClass) {
this.isAvailable = ClassUtils.isPresent(signatureClass, null);
}
/**
* Is this web stack on the classpath?
*/
public boolean isAvailable() {
return this.isAvailable;
}
}