#728 - Fix dependency cycles in WebFlux / WebMVC code

WebHandler is now independent of a particular web stack as the final LinkBuilder creation is externalized through a SAM type that client code now uses to pass a constructor. Refactored the ReactiveLinkBuilder to avoid a dependency on ControllerLinkBuilder (read: the WebMVC stack). With that change, WebHandler is now moved back to the core package.

Refactored LinkBuilderSupport to keep a UriComponentsBuilder instance around instead of UriComponents. It creates a defensive copy of the builder instance on state modifying invocations and returns a new LinkBuilderSupport instance.

Moved SpringMvcAffordanceBuilder to the core package and removed Mvc segment from its name to make obvious it's not tied to SpringMVC. Introduced TemplateVariableAwareLinkBuilderSupport as common base class between ControllerLinkBuilder and ReactiveLinkBuilder. It extracts the TemplateVariable behavior from ControllerLinkBuilder.

Extracted import of web stack specific configuration into dedicated ImportSelector and make use of String based class names to avoid cyclic dependencies between the config packages.

Removed BasicLinkBuilder.
This commit is contained in:
Oliver Drotbohm
2019-02-11 10:16:57 +01:00
parent 68c605c7ff
commit 576274c1b2
24 changed files with 454 additions and 382 deletions

View File

@@ -20,8 +20,6 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.EnumSet;
import java.util.Set;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
@@ -37,7 +35,7 @@ import org.springframework.hateoas.LinkDiscoverer;
* <li>{@link LinkDiscoverer}</li>
* <li>a Jackson 2 module to correctly marshal the resource model classes into the appropriate representation.
* </ul>
*
*
* @see LinkDiscoverer
* @see EntityLinks
* @author Oliver Gierke
@@ -47,19 +45,19 @@ import org.springframework.hateoas.LinkDiscoverer;
@Target(ElementType.TYPE)
@Documented
@EnableEntityLinks
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class, WebStackImportSelector.class })
public @interface EnableHypermediaSupport {
/**
* The hypermedia type to be supported.
*
*
* @return
*/
HypermediaType[] type();
/**
* Hypermedia representation types supported.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -67,7 +65,7 @@ public @interface EnableHypermediaSupport {
/**
* HAL - Hypermedia Application Language.
*
*
* @see http://stateless.co/hal_specification.html
* @see http://tools.ietf.org/html/draft-kelly-json-hal-05
*/
@@ -75,7 +73,7 @@ public @interface EnableHypermediaSupport {
/**
* HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support
*
*
* @see https://rwcbook.github.io/hal-forms/
*/
HAL_FORMS,
@@ -93,7 +91,5 @@ public @interface EnableHypermediaSupport {
* @see http://uberhypermedia.org/
*/
UBER;
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
}
}

View File

@@ -35,11 +35,8 @@ 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;
@@ -47,7 +44,7 @@ import org.springframework.util.ClassUtils;
* {@link ImportBeanDefinitionRegistrar} implementation to activate hypermedia support based on the configured
* hypermedia type. Activates {@link EntityLinks} support as well (essentially as if {@link EnableEntityLinks} was
* activated as well).
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -70,7 +67,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
* collection using the application context.
*/
for (HypermediaType type : types) {
BeanDefinitionBuilder hypermediaTypeBeanDefinition = genericBeanDefinition(HypermediaType.class, () -> type);
registerSourcedBeanDefinition(hypermediaTypeBeanDefinition, metadata, registry);
}
@@ -87,30 +84,11 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
BeanDefinitionReaderUtils.generateBeanName(linkDiscovererBeanDefinition, registry)), 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);
}
}
/**
* Returns a {@link LinkDiscoverer} {@link BeanDefinition} suitable for the given {@link HypermediaType}.
*
*
* @param type
* @return
*/

View File

@@ -0,0 +1,51 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.hateoas.support.WebStack;
/**
* {@link ImportSelector} to include web stack specific configuration.
*
* @author Oliver Drotbohm
*/
class WebStackImportSelector implements ImportSelector {
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata)
*/
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> imports = new ArrayList<>();
if (WebStack.WEBMVC.isAvailable()) {
imports.add("org.springframework.hateoas.config.mvc.WebMvcHateoasConfiguration");
}
if (WebStack.WEBFLUX.isAvailable()) {
imports.add("org.springframework.hateoas.config.reactive.WebFluxHateoasConfiguration");
}
return imports.toArray(new String[imports.size()]);
}
}

View File

@@ -22,6 +22,7 @@ import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeansException;
@@ -101,10 +102,10 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
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)));
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)) {
@@ -115,8 +116,8 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), createUberObjectMapper(this.mapper)));
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Collections.singletonList(UBER_JSON), createUberObjectMapper(this.mapper)));
}
}
}

View File

@@ -30,7 +30,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring MVC HATEOAS Configuration
*
*
* @author Greg Turnquist
*/
@Configuration
@@ -38,19 +38,13 @@ 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);
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

View File

@@ -31,7 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Spring WebFlux HATEOAS configuration.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -39,45 +39,34 @@ import com.fasterxml.jackson.databind.ObjectMapper;
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);
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) {
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);
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.
* TODO: Replace with Spring Framework filter when https://github.com/spring-projects/spring-framework/issues/21746 is
* completed.
*/
@Bean
HypermediaWebFilter hypermediaWebFilter() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -16,15 +16,16 @@
package org.springframework.hateoas.core;
import static org.springframework.hateoas.core.EncodingUtils.*;
import static org.springframework.web.util.UriComponentsBuilder.*;
import lombok.Getter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.IanaLinkRelations;
@@ -48,32 +49,36 @@ import org.springframework.web.util.UriComponentsBuilder;
*/
public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkBuilder {
private final UriComponents uriComponents;
private @Getter final List<Affordance> affordances;
/**
* Creates a new {@link LinkBuilderSupport} using the given {@link UriComponentsBuilder}.
*
* @param builder must not be {@literal null}.
*/
public LinkBuilderSupport(UriComponentsBuilder builder) {
Assert.notNull(builder, "UriComponentsBuilder must not be null!");
this.uriComponents = builder.build();
this.affordances = new ArrayList<>();
}
private final UriComponentsBuilder builder;
private final @Getter List<Affordance> affordances;
/**
* Creates a new {@link LinkBuilderSupport} using the given {@link UriComponents}.
*
* @param uriComponents must not be {@literal null}.
* @param builder must not be {@literal null}.
*/
public LinkBuilderSupport(UriComponents uriComponents) {
protected LinkBuilderSupport(UriComponentsBuilder builder) {
this(builder, Collections.emptyList());
}
Assert.notNull(uriComponents, "UriComponents must not be null!");
this.uriComponents = uriComponents;
this.affordances = new ArrayList<>();
protected LinkBuilderSupport(UriComponentsBuilder builder, List<Affordance> affordances) {
Assert.notNull(builder, "UriComponents must not be null!");
Assert.notNull(affordances, "Affordances must not be null!");
this.builder = builder.cloneBuilder();
this.affordances = affordances;
}
protected LinkBuilderSupport(UriComponents components, List<Affordance> affordances) {
String uriString = components.toUriString();
UriComponentsBuilder builder = uriString.isEmpty() //
? UriComponentsBuilder.fromUri(components.toUri()) //
: UriComponentsBuilder.fromUriString(uriString);
this.builder = builder;
this.affordances = affordances;
}
/*
@@ -109,20 +114,20 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
protected T slash(UriComponents components, boolean encoded) {
String uriString = uriComponents.toUriString();
UriComponentsBuilder builder = uriString.isEmpty() ? fromUri(uriComponents.toUri()) : fromUriString(uriString);
return withFreshBuilder(builder -> {
for (String pathSegment : components.getPathSegments()) {
builder.pathSegment(encoded ? pathSegment : encodePath(pathSegment));
}
for (String pathSegment : components.getPathSegments()) {
builder.pathSegment(encoded ? pathSegment : encodePath(pathSegment));
}
String fragment = components.getFragment();
String fragment = components.getFragment();
if (StringUtils.hasText(fragment)) {
builder.fragment(encoded ? fragment : encodeFragment(fragment));
}
if (StringUtils.hasText(fragment)) {
builder.fragment(encoded ? fragment : encodeFragment(fragment));
}
return createNewInstance(builder.query(components.getQuery()));
return createNewInstance(builder.query(components.getQuery()), affordances);
});
}
/*
@@ -143,13 +148,16 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
* @see org.springframework.hateoas.LinkBuilder#toUri()
*/
public URI toUri() {
return uriComponents.encode().toUri().normalize();
return builder.build().toUri().normalize();
}
public T addAffordances(Collection<Affordance> affordances) {
this.affordances.addAll(affordances);
return getThis();
List<Affordance> newAffordances = new ArrayList<>();
newAffordances.addAll(this.affordances);
newAffordances.addAll(affordances);
return createNewInstance(builder, newAffordances);
}
/*
@@ -176,7 +184,20 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
*/
@Override
public String toString() {
return uriComponents.toUriString();
return builder.build().toUriString();
}
/**
* Executes the given {@link Function} using a freshly cloned {@link UriComponentsBuilder}.
*
* @param function must not be {@literal null}.
* @return
*/
protected <S> S withFreshBuilder(Function<UriComponentsBuilder, S> function) {
Assert.notNull(function, "Function must not be null!");
return function.apply(builder.cloneBuilder());
}
/**
@@ -192,5 +213,5 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
* @param builder will never be {@literal null}.
* @return
*/
protected abstract T createNewInstance(UriComponentsBuilder builder);
protected abstract T createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances);
}

View File

@@ -13,10 +13,9 @@
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -26,9 +25,6 @@ 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.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;
import org.springframework.web.bind.annotation.RequestParam;
@@ -38,8 +34,9 @@ import org.springframework.web.util.UriComponents;
* Extract information needed to assemble an {@link Affordance} from a Spring MVC web method.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
class SpringMvcAffordanceBuilder {
public class SpringAffordanceBuilder {
/**
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
@@ -50,7 +47,7 @@ class SpringMvcAffordanceBuilder {
* @param components
* @return
*/
public static Collection<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
public static List<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
UriComponents components) {
List<Affordance> affordances = new ArrayList<>();

View File

@@ -0,0 +1,83 @@
/*
* 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.List;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@link LinkBuilderSupport} extension that can keep a list of {@link TemplateVariables} around.
*
* @author Oliver Gierke
*/
public abstract class TemplateVariableAwareLinkBuilderSupport<T extends TemplateVariableAwareLinkBuilderSupport<T>>
extends LinkBuilderSupport<T> {
private final TemplateVariables variables;
protected TemplateVariableAwareLinkBuilderSupport(UriComponentsBuilder builder, TemplateVariables variables,
List<Affordance> affordances) {
super(builder, affordances);
this.variables = variables;
}
protected TemplateVariableAwareLinkBuilderSupport(UriComponents components, TemplateVariables variables,
List<Affordance> affordances) {
super(components, affordances);
this.variables = variables;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List)
*/
@Override
protected final T createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances) {
return createNewInstance(builder, affordances, variables);
}
protected abstract T createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances,
TemplateVariables variables);
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderSupport#toString()
*/
@Override
public String toString() {
String result = super.toString();
if (variables == TemplateVariables.NONE) {
return result;
}
if (!result.contains("#")) {
return result.concat(variables.toString());
}
String[] parts = result.split("#");
return parts[0].concat(variables.toString()).concat("#").concat(parts[0]);
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas;
package org.springframework.hateoas.core;
import static org.springframework.hateoas.TemplateVariable.VariableType.*;
import static org.springframework.hateoas.TemplateVariables.*;
@@ -38,12 +38,10 @@ 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.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.Affordance;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -59,19 +57,29 @@ import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* Utility for taking a a method invocation and extracting a {@link ControllerLinkBuilder}.
* Utility for taking 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();
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,
public interface LinkBuilderCreator<T extends LinkBuilder> {
T createBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances);
}
public static <T extends LinkBuilder> T linkTo(Object invocationValue,
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder, LinkBuilderCreator<T> creator) {
return linkTo(invocationValue, mappingToUriComponentsBuilder, creator, null);
}
public static <T extends LinkBuilder> T linkTo(Object invocationValue,
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder, LinkBuilderCreator<T> creator,
BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
@@ -92,13 +100,15 @@ public class WebHandler {
values.put(names.next(), encodePath(classMappingParameters.next()));
}
for (AnnotatedParametersParameterAccessor.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 (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR
.getBoundParameters(invocation)) {
bindRequestParameters(builder, parameter);
@@ -132,7 +142,9 @@ public class WebHandler {
variables = variables.concat(variable);
}
return new ControllerLinkBuilder(components, variables, invocation);
List<Affordance> affordances = SpringAffordanceBuilder.create(invocation, DISCOVERER, components);
return creator.createBuilder(components, variables, affordances);
}
/**
@@ -143,7 +155,8 @@ public class WebHandler {
* @param parameter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private static void bindRequestParameters(UriComponentsBuilder builder, AnnotatedParametersParameterAccessor.BoundMethodParameter parameter) {
private static void bindRequestParameters(UriComponentsBuilder builder,
AnnotatedParametersParameterAccessor.BoundMethodParameter parameter) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
@@ -248,15 +261,16 @@ public class WebHandler {
}
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
* 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 static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(16,
ConcurrentReferenceHashMap.ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
@@ -297,7 +311,7 @@ public class WebHandler {
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2012 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.mvc;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Simples {@link LinkBuilder} implementation possible. Exposes a link to the current servlet mapping only.
*
* @author Oliver Gierke
*/
public class BasicLinkBuilder extends LinkBuilderSupport<BasicLinkBuilder> {
/**
* Creates a new {@link BasicLinkBuilder} using the given {@link UriComponentsBuilder}.
*
* @param builder must not be {@literal null}.
*/
private BasicLinkBuilder(UriComponentsBuilder builder) {
super(builder);
}
/**
* Creates a new {@link BasicLinkBuilder} to link to the current servlet mapping.
*
* @return
*/
public static BasicLinkBuilder linkToCurrentMapping() {
return new BasicLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder)
*/
@Override
protected BasicLinkBuilder createNewInstance(UriComponentsBuilder builder) {
return new BasicLinkBuilder(builder);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.mvc.LinkBuilderSupport#getThis()
*/
@Override
protected BasicLinkBuilder getThis() {
return this;
}
}

View File

@@ -17,7 +17,8 @@ package org.springframework.hateoas.mvc;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Affordance;
@@ -26,9 +27,8 @@ 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.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodInvocation;
import org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
@@ -48,42 +48,28 @@ import org.springframework.web.util.UriTemplate;
* @author Oliver Trosien
* @author Greg Turnquist
*/
public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuilder> {
public class ControllerLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<ControllerLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer
.of(new AnnotationMappingDiscoverer(RequestMapping.class));
private static final ControllerLinkBuilderFactory FACTORY = new ControllerLinkBuilderFactory();
private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler();
private final TemplateVariables variables;
/**
* Creates a new {@link ControllerLinkBuilder} using the given {@link UriComponentsBuilder}.
*
* @param builder must not be {@literal null}.
*/
ControllerLinkBuilder(UriComponentsBuilder builder) {
super(builder);
this.variables = TemplateVariables.NONE;
this(builder, TemplateVariables.NONE, Collections.emptyList());
}
/**
* Creates a new {@link ControllerLinkBuilder} using the given {@link UriComponents}.
*
* @param uriComponents must not be {@literal null}.
*/
ControllerLinkBuilder(UriComponents uriComponents) {
this(uriComponents, TemplateVariables.NONE, null);
ControllerLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List<Affordance> affordances) {
super(builder, variables, affordances);
}
public ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, MethodInvocation invocation) {
super(uriComponents);
this.variables = variables;
this.addAffordances(findAffordances(invocation, uriComponents));
ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, List<Affordance> affordances) {
super(uriComponents, variables, affordances);
}
/**
@@ -233,11 +219,12 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
/*
* (non-Javadoc)
* @see org.springframework.hateoas.UriComponentsLinkBuilder#createNewInstance(org.springframework.web.util.UriComponentsBuilder)
* @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables)
*/
@Override
protected ControllerLinkBuilder createNewInstance(UriComponentsBuilder builder) {
return new ControllerLinkBuilder(builder);
protected ControllerLinkBuilder createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances,
TemplateVariables variables) {
return new ControllerLinkBuilder(builder, variables, affordances);
}
/**
@@ -249,27 +236,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
return UriComponentsBuilder.fromUri(toUri());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderSupport#toString()
*/
@Override
public String toString() {
String result = super.toString();
if (variables == TemplateVariables.NONE) {
return result;
}
if (!result.contains("#")) {
return result.concat(variables.toString());
}
String[] parts = result.split("#");
return parts[0].concat(variables.toString()).concat("#").concat(parts[0]);
}
/**
* 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.
@@ -280,17 +246,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
return UriComponentsBuilderFactory.getBuilder();
}
/**
* Look up {@link Affordance}s based on the {@link MethodInvocation} and {@link UriComponents}.
*
* @param invocation
* @param components
* @return
*/
private static Collection<Affordance> findAffordances(MethodInvocation invocation, UriComponents components) {
return SpringMvcAffordanceBuilder.create(invocation, DISCOVERER, components);
}
private static class CustomUriTemplateHandler extends DefaultUriTemplateHandler {
public CustomUriTemplateHandler() {

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.WebHandler;
import org.springframework.hateoas.core.WebHandler;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.
@@ -45,12 +45,6 @@ import org.springframework.hateoas.WebHandler;
*/
public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<ControllerLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer
.of(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();
private List<UriComponentsContributor> uriComponentsContributors = new ArrayList<>();
/**
@@ -107,26 +101,25 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
@Override
public ControllerLinkBuilder linkTo(Object 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();
return WebHandler.linkTo(invocationValue, mapping -> ControllerLinkBuilder.getBuilder().path(mapping),
ControllerLinkBuilder::new, (builder, invocation) -> {
for (MethodParameter parameter : parameters.getParameters()) {
Object parameterValue = parameterValues.next();
for (UriComponentsContributor contributor : this.uriComponentsContributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
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 : this.uriComponentsContributors) {
if (contributor.supportsParameter(parameter)) {
contributor.enhance(builder, parameter, parameterValue);
}
}
}
}
return builder;
});
return builder;
});
}
/*

View File

@@ -19,10 +19,15 @@ import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import reactor.core.publisher.Mono;
import java.util.List;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.WebHandler;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
@@ -31,12 +36,14 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Greg Turnquist
* @since 1.0
*/
public class ReactiveLinkBuilder {
public class ReactiveLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<ReactiveLinkBuilder> {
private final ControllerLinkBuilder controllerLinkBuilder;
private ReactiveLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List<Affordance> affordances) {
super(builder, variables, affordances);
}
private ReactiveLinkBuilder(ControllerLinkBuilder controllerLinkBuilder) {
this.controllerLinkBuilder = controllerLinkBuilder;
private ReactiveLinkBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances) {
super(components, variables, affordances);
}
/**
@@ -61,26 +68,9 @@ public class ReactiveLinkBuilder {
*/
public static ReactiveLinkBuilder linkTo(Object invocationValue, ServerWebExchange exchange) {
ControllerLinkBuilder controllerLinkBuilder = WebHandler //
.linkTo(invocationValue, path -> getBuilder(exchange).replacePath(path == null ? "/" : path), 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);
return WebHandler.linkTo(invocationValue, //
path -> getBuilder(exchange).replacePath(path == null ? "/" : path), //
ReactiveLinkBuilder::new);
}
/**
@@ -94,4 +84,23 @@ public class ReactiveLinkBuilder {
? UriComponentsBuilder.fromPath("/") //
: UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables)
*/
@Override
protected ReactiveLinkBuilder createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances,
TemplateVariables variables) {
return new ReactiveLinkBuilder(builder, variables, affordances);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderSupport#getThis()
*/
@Override
protected ReactiveLinkBuilder getThis() {
return this;
}
}