#728 - Polishing.

Adapt to API changes in master branch. Fixed formatting.
This commit is contained in:
Oliver Drotbohm
2019-02-15 13:13:22 +01:00
parent 3f0dcc7cb4
commit 68c605c7ff
29 changed files with 1748 additions and 1030 deletions

View File

@@ -13,13 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
package org.springframework.hateoas;
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 lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -30,13 +35,21 @@ 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.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.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.LastInvocationAware;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -47,52 +60,45 @@ 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));
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) {
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);
}
UriTemplate template = new UriTemplate(mapping == null ? "/" : 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)) {
for (AnnotatedParametersParameterAccessor.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)) {
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
bindRequestParameters(builder, parameter);
@@ -112,12 +118,9 @@ public class WebHandler {
}
}
UriComponents components;
if (additionalUriHandler == null) {
components = builder.buildAndExpand(values);
} else {
components = additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
}
UriComponents components = additionalUriHandler == null //
? builder.buildAndExpand(values)
: additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
TemplateVariables variables = NONE;
@@ -125,22 +128,22 @@ public class WebHandler {
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
TemplateVariable variable = new TemplateVariable(parameter,
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
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}.
* {@link AnnotatedParametersParameterAccessor.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) {
private static void bindRequestParameters(UriComponentsBuilder builder, AnnotatedParametersParameterAccessor.BoundMethodParameter parameter) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
@@ -198,7 +201,7 @@ public class WebHandler {
*/
@Override
protected BoundMethodParameter createParameter(final MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute) {
@@ -243,4 +246,162 @@ public class WebHandler {
return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null;
}
}
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
private static class AnnotatedParametersParameterAccessor {
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(
16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
/**
* Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(MethodInvocation invocation) {
Assert.notNull(invocation, "MethodInvocation must not be null!");
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
Object value = arguments[parameter.getParameterIndex()];
Object verifiedValue = verifyParameterValue(parameter, value);
if (verifiedValue != null) {
result.add(createParameter(parameter, verifiedValue, attribute));
}
}
return result;
}
/**
* Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and
* {@link AnnotationAttribute}.
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute must not be {@literal null}.
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}
/**
* Callback to verify the parameter values given for a dummy invocation. Default implementation rejects
* {@literal null} values as they indicate an invalid dummy call.
*
* @param parameter will never be {@literal null}.
* @param value could be {@literal null}.
* @return the verified value.
*/
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
return value;
}
/**
* Returns the {@link MethodParameters} for the given {@link Method}.
*
* @param method
* @return
*/
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
return METHOD_PARAMETERS_CACHE.computeIfAbsent(method, MethodParameters::new);
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDescriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute can be {@literal null}.
*/
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, parameter.isOptional() ? 1 : 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will be derived from the configured
* {@link AnnotationAttribute} or the {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation);
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default conversion service setup.
*
* @return
*/
public String asString() {
return value == null //
? null //
: (String) CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR);
}
/**
* Returns whether the given parameter is a required one. Defaults to {@literal true}.
*
* @return
*/
public boolean isRequired() {
return true;
}
}
}
}

View File

@@ -95,15 +95,5 @@ public @interface EnableHypermediaSupport {
UBER;
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
/**
* Is this {@literal HAL} or one of its derivatives?
*
* @param hypermediaType
* @return
*/
public static boolean isHalBasedMediaType(HypermediaType hypermediaType) {
return HAL_BASED_MEDIATYPES.contains(hypermediaType);
}
}
}

View File

@@ -18,7 +18,6 @@ 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;
@@ -27,14 +26,13 @@ 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
*/
@@ -42,7 +40,7 @@ public final class HypermediaObjectMapperCreator {
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_JSON}-based {@link ObjectMapper}.
*
*
* @param objectMapper
* @param curieProvider
* @param relProvider
@@ -50,18 +48,15 @@ public final class HypermediaObjectMapperCreator {
* @param halConfiguration
* @return
*/
public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalConfiguration halConfiguration) {
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));
mapper.setHandlerInstantiator(
new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource, halConfiguration));
return mapper;
}
@@ -76,19 +71,16 @@ public final class HypermediaObjectMapperCreator {
* @param halFormsConfiguration
* @return properly configured objectMapper
*/
public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalFormsConfiguration halFormsConfiguration) {
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));
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
true, halFormsConfiguration));
return mapper;
}
@@ -100,14 +92,12 @@ public final class HypermediaObjectMapperCreator {
* @param linkRelationMessageSource
* @return properly configured objectMapper
*/
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
return mapper;
}
@@ -124,7 +114,6 @@ public final class HypermediaObjectMapperCreator {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return mapper;
}

View File

@@ -79,11 +79,11 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance)
.filter(AbstractJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
@@ -91,36 +91,32 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
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)));
}
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,
Collections.singletonList(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)));
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(COLLECTION_JSON), createCollectionJsonObjectMapper(this.mapper)));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(UBER_JSON), createUberObjectMapper(this.mapper)));
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), createUberObjectMapper(this.mapper)));
}
}
}

View File

@@ -26,6 +26,7 @@ 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.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
@@ -44,7 +45,7 @@ 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
*/
@@ -62,7 +63,7 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -70,8 +71,8 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
/**
* 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.
* <p>
* The configured readers and writers will be used for both annotated controllers and functional endpoints.
*
* @param configurer the configurer to use
*/
@@ -79,67 +80,59 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
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)) {
.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);
MessageSourceAccessor.class);
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
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));
}
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)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
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));
}
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);
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper);
customCodecs.encoder(
new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
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.encoder(new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON));
customCodecs.decoder(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
customCodecs.encoder(CharSequenceEncoder.allMimeTypes());
customCodecs.decoder(StringDecoder.allMimeTypes());
configurer.registerDefaults(false);

View File

@@ -28,6 +28,7 @@ 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.CharSequenceEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
@@ -46,7 +47,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Assembles {@link ExchangeStrategies} needed to wire a {@link WebClient} with hypermedia support.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -64,7 +65,7 @@ public class WebClientConfigurer implements BeanFactoryAware {
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -72,40 +73,38 @@ public class WebClientConfigurer implements BeanFactoryAware {
/**
* 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);
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)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
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));
}
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)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
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));
}
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);
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper);
encoders.add(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
decoders.add(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
@@ -119,18 +118,16 @@ public class WebClientConfigurer implements BeanFactoryAware {
decoders.add(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
// Include ability to decode into a plain string
encoders.add(CharSequenceEncoder.allMimeTypes());
decoders.add(StringDecoder.allMimeTypes());
return ExchangeStrategies.builder()
.codecs(clientCodecConfigurer -> {
return ExchangeStrategies.builder().codecs(clientCodecConfigurer -> {
encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder));
decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder));
encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder));
decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder));
clientCodecConfigurer.registerDefaults(false);
})
.build();
clientCodecConfigurer.registerDefaults(false);
}).build();
}
/**
@@ -141,9 +138,6 @@ public class WebClientConfigurer implements BeanFactoryAware {
*/
public WebClient registerHypermediaTypes(WebClient webClient) {
return webClient.mutate()
.exchangeStrategies(hypermediaExchangeStrategies())
.build();
return webClient.mutate().exchangeStrategies(hypermediaExchangeStrategies()).build();
}
}

View File

@@ -1,193 +0,0 @@
/*
* 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.
* 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 lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriTemplate;
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
class AnnotatedParametersParameterAccessor {
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(
16, ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
/**
* Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(MethodInvocation invocation) {
Assert.notNull(invocation, "MethodInvocation must not be null!");
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
Object value = arguments[parameter.getParameterIndex()];
Object verifiedValue = verifyParameterValue(parameter, value);
if (verifiedValue != null) {
result.add(createParameter(parameter, verifiedValue, attribute));
}
}
return result;
}
/**
* Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and
* {@link AnnotationAttribute}.
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute must not be {@literal null}.
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}
/**
* Callback to verify the parameter values given for a dummy invocation. Default implementation rejects
* {@literal null} values as they indicate an invalid dummy call.
*
* @param parameter will never be {@literal null}.
* @param value could be {@literal null}.
* @return the verified value.
*/
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
return value;
}
/**
* Returns the {@link MethodParameters} for the given {@link Method}.
*
* @param method
* @return
*/
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
return METHOD_PARAMETERS_CACHE.computeIfAbsent(method, MethodParameters::new);
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDescriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute can be {@literal null}.
*/
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, parameter.isOptional() ? 1 : 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will be derived from the configured
* {@link AnnotationAttribute} or the {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation);
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default conversion service setup.
*
* @return
*/
public String asString() {
return value == null //
? null //
: (String) CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR);
}
/**
* Returns whether the given parameter is a required one. Defaults to {@literal true}.
*
* @return
*/
public boolean isRequired() {
return true;
}
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.hateoas.WebHandler;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.

View File

@@ -18,9 +18,10 @@ 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.WebHandler;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
@@ -37,19 +38,18 @@ public class ReactiveLinkBuilder {
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.
* 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)));
.map(context -> linkTo(invocationValue, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
}
/**
@@ -61,19 +61,9 @@ public class ReactiveLinkBuilder {
*/
public static ReactiveLinkBuilder linkTo(Object invocationValue, ServerWebExchange exchange) {
ControllerLinkBuilder controllerLinkBuilder = WebHandler.linkTo(invocationValue, path -> {
ControllerLinkBuilder controllerLinkBuilder = WebHandler //
.linkTo(invocationValue, path -> getBuilder(exchange).replacePath(path == null ? "/" : path), null);
UriComponentsBuilder builder = getBuilder(exchange);
if (path == null) {
builder.replacePath("/");
} else {
builder.replacePath(path);
}
return builder;
}, null);
return new ReactiveLinkBuilder(controllerLinkBuilder);
}
@@ -100,10 +90,8 @@ public class ReactiveLinkBuilder {
*/
private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) {
if (exchange == null) {
return UriComponentsBuilder.fromPath("/");
}
return UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
return exchange == null //
? UriComponentsBuilder.fromPath("/") //
: UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
}

View File

@@ -17,6 +17,7 @@ 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;
@@ -25,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -60,14 +61,14 @@ public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAsse
*/
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;
});
return entities //
.flatMap(entity -> toResource(entity, exchange)) //
.collectList() //
.map(listOfResources -> {
Resources<Resource<T>> resources = new Resources<>(listOfResources);
addLinks(resources, exchange);
return resources;
});
}
/**
@@ -76,5 +77,4 @@ public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAsse
* @param resources
*/
void addLinks(Resources<Resource<T>> resources, ServerWebExchange exchange);
}

View File

@@ -30,6 +30,8 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
@@ -73,6 +75,13 @@ public class PropertyUtils {
public static List<String> findPropertyNames(ResolvableType resolvableType) {
if (WebStack.WEBFLUX.isAvailable()) {
if (Mono.class.equals(resolvableType.getRawClass()) || Flux.class.equals(resolvableType.getRawClass())) {
ResolvableType generic = resolvableType.getGeneric(0);
return findPropertyNames(generic);
}
}
if (resolvableType.getRawClass().equals(Resource.class)) {
return findPropertyNames(resolvableType.resolveGeneric(0));
} else {