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.
+ * 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);
+ }
+}
diff --git a/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java
new file mode 100644
index 00000000..65ea44b2
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java
@@ -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 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> encoders = new ArrayList<>();
+ List> 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();
+ }
+}
+
diff --git a/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java
new file mode 100644
index 00000000..8c1e6d0f
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java
@@ -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 mapper,
+ DelegatingRelProvider relProvider,
+ ObjectProvider curieProvider,
+ ObjectProvider halConfiguration,
+ ObjectProvider halFormsConfiguration,
+ Collection 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 mapper,
+ DelegatingRelProvider relProvider,
+ ObjectProvider curieProvider,
+ ObjectProvider halConfiguration,
+ ObjectProvider halFormsConfiguration,
+ Collection 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();
+ }
+}
diff --git a/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java b/src/main/java/org/springframework/hateoas/core/AnnotatedParametersParameterAccessor.java
similarity index 94%
rename from src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java
rename to src/main/java/org/springframework/hateoas/core/AnnotatedParametersParameterAccessor.java
index d4f7aeb3..aaacae01 100644
--- a/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java
+++ b/src/main/java/org/springframework/hateoas/core/AnnotatedParametersParameterAccessor.java
@@ -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);
}
diff --git a/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java
index 49d4a145..7057a37d 100644
--- a/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java
+++ b/src/main/java/org/springframework/hateoas/core/DummyInvocationUtils.java
@@ -47,13 +47,6 @@ public class DummyInvocationUtils {
private static final Map, Class>> CLASS_CACHE = new ConcurrentReferenceHashMap<>(16,
ReferenceType.WEAK);
- public interface LastInvocationAware {
-
- Iterator