#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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user