#728 - API polishing.

Renamed LinkBuilder implementations for the different web stacks Web(MVC|Flux)LinkBuilder for symmetry. Introduced dedicated builder types to be used from WebFluxLinkBuilder to more conveniently integrate with reactive flows (see the changes in WebFluxEmployeeController).

Deprecated ControllerLinkBuilder(Factory) in favor of WebMvcLinkBuilder(Factory).

Generally untangled the use of WebMvcLinkBuilder from the reactive examples.
This commit is contained in:
Oliver Drotbohm
2019-02-20 16:45:41 +01:00
parent 576274c1b2
commit 1d9703abbe
25 changed files with 1798 additions and 210 deletions

View File

@@ -18,7 +18,7 @@ package org.springframework.hateoas;
import java.lang.reflect.Method;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.WebMvcLinkBuilder;
/**
* Extension of {@link LinkBuilderFactory} for implementations that also support creating {@link LinkBuilder}s by
@@ -52,9 +52,9 @@ public interface MethodLinkBuilderFactory<T extends LinkBuilder> extends LinkBui
/**
* Returns a {@link LinkBuilder} pointing to the URI mapped to the method the result is handed into this method. Use
* {@link DummyInvocationUtils#methodOn(Class, Object...)} to obtain a dummy instance of a controller to record a
* dummy method invocation on. See {@link ControllerLinkBuilder#linkTo(Object)} for an example.
*
* @see ControllerLinkBuilder#linkTo(Object)
* dummy method invocation on. See {@link WebMvcLinkBuilder#linkTo(Object)} for an example.
*
* @see WebMvcLinkBuilder#linkTo(Object)
* @param methodInvocationResult must not be {@literal null}.
* @return
*/

View File

@@ -22,7 +22,7 @@ import org.springframework.context.annotation.Primary;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.core.ControllerEntityLinksFactoryBean;
import org.springframework.hateoas.core.DelegatingEntityLinks;
import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory;
import org.springframework.hateoas.mvc.WebMvcLinkBuilderFactory;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.plugin.core.support.PluginRegistryFactoryBean;
import org.springframework.stereotype.Controller;
@@ -54,7 +54,7 @@ class EntityLinksConfiguration {
}
@Bean
ControllerEntityLinksFactoryBean controllerEntityLinks(ControllerLinkBuilderFactory controllerLinkBuilderFactory) {
ControllerEntityLinksFactoryBean controllerEntityLinks(WebMvcLinkBuilderFactory controllerLinkBuilderFactory) {
ControllerEntityLinksFactoryBean factory = new ControllerEntityLinksFactoryBean();
factory.setAnnotation(Controller.class);
@@ -64,7 +64,7 @@ class EntityLinksConfiguration {
}
@Bean
ControllerLinkBuilderFactory controllerLinkBuilderFactoryBean() {
return new ControllerLinkBuilderFactory();
WebMvcLinkBuilderFactory webMvcLinkBuilderFactoryBean() {
return new WebMvcLinkBuilderFactory();
}
}

View File

@@ -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 java.util.Map;
@@ -27,7 +27,7 @@ import org.springframework.web.util.UriTemplate;
* @author Michal Stochmialek
* @author Oliver Drotbohm
*/
class UriTemplateFactory {
public class UriTemplateFactory {
private static final Map<String, UriTemplate> CACHE = new ConcurrentReferenceHashMap<String, UriTemplate>();

View File

@@ -42,7 +42,6 @@ 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;
import org.springframework.util.MultiValueMap;
@@ -57,13 +56,14 @@ import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* Utility for taking a method invocation and extracting a {@link ControllerLinkBuilder}.
* Utility for taking a method invocation and extracting a {@link LinkBuilder}.
*
* @author Greg Turnquist
*/
public class WebHandler {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
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 //
@@ -90,7 +90,7 @@ public class WebHandler {
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
UriTemplate template = new UriTemplate(mapping == null ? "/" : mapping);
UriTemplate template = UriTemplateFactory.templateFor(mapping == null ? "/" : mapping);
Map<String, Object> values = new HashMap<>();
Iterator<String> names = template.getVariableNames().iterator();

View File

@@ -29,6 +29,7 @@ import org.springframework.hateoas.core.CachingMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.hateoas.core.UriTemplateFactory;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
@@ -47,7 +48,9 @@ import org.springframework.web.util.UriTemplate;
* @author Andrew Naydyonock
* @author Oliver Trosien
* @author Greg Turnquist
* @deprecated use {@link WebMvcLinkBuilder} instead.
*/
@Deprecated
public class ControllerLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<ControllerLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer

View File

@@ -42,7 +42,9 @@ import org.springframework.hateoas.core.WebHandler;
* @author Kevin Conaway
* @author Andrew Naydyonock
* @author Greg Turnquist
* @deprecated use {@link WebMvcLinkBuilderFactory} instead.
*/
@Deprecated
public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<ControllerLinkBuilder> {
private List<UriComponentsContributor> uriComponentsContributors = new ArrayList<>();

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.hateoas.mvc;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.util.Arrays;

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.hateoas.mvc;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.util.ArrayList;
import java.util.List;

View File

@@ -0,0 +1,265 @@
/*
* 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.mvc;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.Link;
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.MappingDiscoverer;
import org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.hateoas.core.UriTemplateFactory;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.util.DefaultUriTemplateHandler;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriTemplate;
/**
* Builder to ease building {@link Link} instances pointing to Spring MVC controllers.
*
* @author Oliver Gierke
* @author Kamill Sokol
* @author Greg Turnquist
* @author Kevin Conaway
* @author Andrew Naydyonock
* @author Oliver Trosien
* @author Greg Turnquist
*/
public class WebMvcLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<WebMvcLinkBuilder> {
private static final MappingDiscoverer DISCOVERER = CachingMappingDiscoverer
.of(new AnnotationMappingDiscoverer(RequestMapping.class));
private static final WebMvcLinkBuilderFactory FACTORY = new WebMvcLinkBuilderFactory();
private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler();
/**
* Creates a new {@link WebMvcLinkBuilder} using the given {@link UriComponentsBuilder}.
*
* @param builder must not be {@literal null}.
*/
WebMvcLinkBuilder(UriComponentsBuilder builder) {
this(builder, TemplateVariables.NONE, Collections.emptyList());
}
WebMvcLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List<Affordance> affordances) {
super(builder, variables, affordances);
}
WebMvcLinkBuilder(UriComponents uriComponents, TemplateVariables variables, List<Affordance> affordances) {
super(uriComponents, variables, affordances);
}
/**
* Creates a new {@link WebMvcLinkBuilder} with a base of the mapping annotated to the given controller class.
*
* @param controller the class to discover the annotation on, must not be {@literal null}.
* @return
*/
public static WebMvcLinkBuilder linkTo(Class<?> controller) {
return linkTo(controller, new Object[0]);
}
/**
* Creates a new {@link WebMvcLinkBuilder} with a base of the mapping annotated to the given controller class. The
* additional parameters are used to fill up potentially available path variables in the class scop request mapping.
*
* @param controller the class to discover the annotation on, must not be {@literal null}.
* @param parameters additional parameters to bind to the URI template declared in the annotation, must not be
* {@literal null}.
* @return
*/
public static WebMvcLinkBuilder linkTo(Class<?> controller, Object... parameters) {
Assert.notNull(controller, "Controller must not be null!");
Assert.notNull(parameters, "Parameters must not be null!");
String mapping = DISCOVERER.getMapping(controller);
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping);
UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters);
return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uriComponents, true);
}
/**
* Creates a new {@link WebMvcLinkBuilder} with a base of the mapping annotated to the given controller class.
* Parameter map is used to fill up potentially available path variables in the class scope request mapping.
*
* @param controller the class to discover the annotation on, must not be {@literal null}.
* @param parameters additional parameters to bind to the URI template declared in the annotation, must not be
* {@literal null}.
* @return
*/
public static WebMvcLinkBuilder linkTo(Class<?> controller, Map<String, ?> parameters) {
Assert.notNull(controller, "Controller must not be null!");
Assert.notNull(parameters, "Parameters must not be null!");
String mapping = DISCOVERER.getMapping(controller);
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(mapping == null ? "/" : mapping);
UriComponents uriComponents = HANDLER.expandAndEncode(builder, parameters);
return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uriComponents, true);
}
/*
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
*/
public static WebMvcLinkBuilder linkTo(Method method, Object... parameters) {
return linkTo(method.getDeclaringClass(), method, parameters);
}
/*
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Class<?>, Method, Object...)
*/
public static WebMvcLinkBuilder linkTo(Class<?> controller, Method method, Object... parameters) {
Assert.notNull(controller, "Controller type must not be null!");
Assert.notNull(method, "Method must not be null!");
String mapping = DISCOVERER.getMapping(controller, method);
UriTemplate template = UriTemplateFactory.templateFor(mapping);
URI uri = template.expand(parameters);
return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getBuilder()).slash(uri);
}
/**
* Creates a {@link WebMvcLinkBuilder} pointing to a controller method. Hand in a dummy method invocation result you
* can create via {@link #methodOn(Class, Object...)} or {@link DummyInvocationUtils#methodOn(Class, Object...)}.
*
* <pre>
* &#64;RequestMapping("/customers")
* class CustomerController {
*
* &#64;RequestMapping("/{id}/addresses")
* HttpEntity&lt;Addresses&gt; showAddresses(@PathVariable Long id) { … }
* }
*
* Link link = linkTo(methodOn(CustomerController.class).showAddresses(2L)).withRel("addresses");
* </pre>
*
* The resulting {@link Link} instance will point to {@code /customers/2/addresses} and have a rel of
* {@code addresses}. For more details on the method invocation constraints, see
* {@link DummyInvocationUtils#methodOn(Class, Object...)}.
*
* @param invocationValue
* @return
*/
public static WebMvcLinkBuilder linkTo(Object invocationValue) {
return FACTORY.linkTo(invocationValue);
}
/**
* Extract a {@link Link} from the {@link WebMvcLinkBuilder} and look up the related {@link Affordance}. Should only
* be one.
*
* <pre>
* Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel()
* .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)));
* </pre>
*
* This takes a link and adds an {@link Affordance} based on another Spring MVC handler method.
*
* @param invocationValue
* @return
*/
public static Affordance afford(Object invocationValue) {
WebMvcLinkBuilder linkBuilder = linkTo(invocationValue);
Assert.isTrue(linkBuilder.getAffordances().size() == 1, "A base can only have one affordance, itself");
return linkBuilder.getAffordances().get(0);
}
/**
* Wrapper for {@link DummyInvocationUtils#methodOn(Class, Object...)} to be available in case you work with static
* imports of {@link WebMvcLinkBuilder}.
*
* @param controller must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level mapping.
* @return
*/
public static <T> T methodOn(Class<T> controller, Object... parameters) {
return DummyInvocationUtils.methodOn(controller, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.UriComponentsLinkBuilder#getThis()
*/
@Override
protected WebMvcLinkBuilder getThis() {
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables)
*/
@Override
protected WebMvcLinkBuilder createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances,
TemplateVariables variables) {
return new WebMvcLinkBuilder(builder, variables, affordances);
}
/**
* Returns a {@link UriComponentsBuilder} to continue to build the already built URI in a more fine grained way.
*
* @return
*/
public UriComponentsBuilder toUriComponentsBuilder() {
return UriComponentsBuilder.fromUri(toUri());
}
private static class CustomUriTemplateHandler extends DefaultUriTemplateHandler {
public CustomUriTemplateHandler() {
setStrictEncoding(true);
}
/*
* (non-Javadoc)
* @see org.springframework.web.util.DefaultUriTemplateHandler#expandAndEncode(org.springframework.web.util.UriComponentsBuilder, java.util.Map)
*/
@Override
public UriComponents expandAndEncode(UriComponentsBuilder builder, Map<String, ?> uriVariables) {
return super.expandAndEncode(builder, uriVariables);
}
/*
* (non-Javadoc)
* @see org.springframework.web.util.DefaultUriTemplateHandler#expandAndEncode(org.springframework.web.util.UriComponentsBuilder, java.lang.Object[])
*/
@Override
public UriComponents expandAndEncode(UriComponentsBuilder builder, Object[] uriVariables) {
return super.expandAndEncode(builder, uriVariables);
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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.mvc;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.core.MethodParameter;
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.web.util.UriComponentsBuilder;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.
*
* @author Ricardo Gladwell
* @author Oliver Gierke
* @author Dietrich Schulten
* @author Kamill Sokol
* @author Ross Turner
* @author Oemer Yildiz
* @author Kevin Conaway
* @author Andrew Naydyonock
* @author Greg Turnquist
*/
public class WebMvcLinkBuilderFactory implements MethodLinkBuilderFactory<WebMvcLinkBuilder> {
private List<UriComponentsContributor> uriComponentsContributors = new ArrayList<>();
/**
* Configures the {@link UriComponentsContributor} to be used when building {@link Link} instances from method
* invocations.
*
* @see #linkTo(Object)
* @param uriComponentsContributors the uriComponentsContributors to set
*/
public void setUriComponentsContributors(List<? extends UriComponentsContributor> uriComponentsContributors) {
this.uriComponentsContributors = Collections.unmodifiableList(uriComponentsContributors);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkBuilderFactory#linkTo(java.lang.Class)
*/
@Override
public WebMvcLinkBuilder linkTo(Class<?> controller) {
return WebMvcLinkBuilder.linkTo(controller);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkBuilderFactory#linkTo(java.lang.Class, java.lang.Object[])
*/
@Override
public WebMvcLinkBuilder linkTo(Class<?> controller, Object... parameters) {
return WebMvcLinkBuilder.linkTo(controller, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkBuilderFactory#linkTo(java.lang.Class, java.util.Map)
*/
@Override
public WebMvcLinkBuilder linkTo(Class<?> controller, Map<String, ?> parameters) {
return WebMvcLinkBuilder.linkTo(controller, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Class, java.lang.reflect.Method, java.lang.Object[])
*/
@Override
public WebMvcLinkBuilder linkTo(Class<?> controller, Method method, Object... parameters) {
return WebMvcLinkBuilder.linkTo(controller, method, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
*/
@Override
public WebMvcLinkBuilder linkTo(Object invocationValue) {
Function<String, UriComponentsBuilder> builderFactory = mapping -> UriComponentsBuilderFactory.getBuilder()
.path(mapping);
return WebHandler.linkTo(invocationValue, builderFactory, WebMvcLinkBuilder::new, (builder, invocation) -> {
MethodParameters parameters = new MethodParameters(invocation.getMethod());
Iterator<Object> parameterValues = Arrays.asList(invocation.getArguments()).iterator();
for (MethodParameter parameter : parameters.getParameters()) {
Object parameterValue = parameterValues.next();
uriComponentsContributors.stream() //
.filter(it -> it.supportsParameter(parameter)) //
.forEach(it -> it.enhance(builder, parameter, parameterValue));
}
return builder;
});
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.reflect.Method, java.lang.Object[])
*/
@Override
public WebMvcLinkBuilder linkTo(Method method, Object... parameters) {
return WebMvcLinkBuilder.linkTo(method, parameters);
}
}

View File

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

View File

@@ -0,0 +1,250 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.reactive;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility for building reactive {@link Link}s.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
* @since 1.0
*/
public class WebFluxLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<WebFluxLinkBuilder> {
private WebFluxLinkBuilder(UriComponentsBuilder builder, TemplateVariables variables, List<Affordance> affordances) {
super(builder, variables, affordances);
}
private WebFluxLinkBuilder(UriComponents components, TemplateVariables variables, List<Affordance> affordances) {
super(components, variables, affordances);
}
/**
* Create a {@link WebFluxLinkBuilder} 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. Usually used with {@link #methodOn(Class, Object...)} to refer to a method invocation.
*
* @param invocation must not be {@literal null}.
* @see #methodOn(Class, Object...)
*/
public static WebFluxBuilder linkTo(Object invocation) {
Assert.notNull(invocation, "Invocation must not be null!");
return new WebFluxBuilder(linkToInternal(invocation));
}
/**
* Create a {@link WebFluxLinkBuilder} using an explicitly defined {@link ServerWebExchange}. This is possible if your
* WebFlux method includes the exchange and you want to pass it straight in.
*
* @param invocation must not be {@literal null}.
* @param exchange must not be {@literal null}.
*/
public static WebFluxBuilder linkTo(Object invocation, ServerWebExchange exchange) {
return new WebFluxBuilder(linkToInternal(invocation, exchange));
}
/**
* Wrapper for {@link DummyInvocationUtils#methodOn(Class, Object...)} to be available in case you work with static
* imports of {@link WebFluxLinkBuilder}.
*
* @param controller must not be {@literal null}.
* @param parameters parameters to extend template variables in the type level mapping.
* @return
*/
public static <T> T methodOn(Class<T> controller, Object... parameters) {
return DummyInvocationUtils.methodOn(controller, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.TemplateVariableAwareLinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder, java.util.List, org.springframework.hateoas.TemplateVariables)
*/
@Override
protected WebFluxLinkBuilder createNewInstance(UriComponentsBuilder builder, List<Affordance> affordances,
TemplateVariables variables) {
return new WebFluxLinkBuilder(builder, variables, affordances);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.LinkBuilderSupport#getThis()
*/
@Override
protected WebFluxLinkBuilder getThis() {
return this;
}
@RequiredArgsConstructor
public static class WebFluxBuilder {
private final Mono<WebFluxLinkBuilder> builder;
/**
* Creates a new {@link WebFluxLink} for the {@link Link} with the given {@link LinkRelation}
*
* @param relation must not be {@literal null}.
* @return
*/
public WebFluxLink withRel(LinkRelation relation) {
return new WebFluxLink(builder.map(it -> it.withRel(relation)));
}
/**
* Creates a new {@link WebFluxLink} for the {@link Link} with the given link relation.
*
* @param relation must not be {@literal null}.
* @return
*/
public WebFluxLink withRel(String relation) {
return new WebFluxLink(builder.map(it -> it.withRel(relation)));
}
/**
* Creates a new {@link WebFluxLink} for the {@link Link} with the {@link IanaLinkRelations#SELF}.
*
* @return
*/
public WebFluxLink withSelfRel() {
return new WebFluxLink(builder.map(WebFluxLinkBuilder::withSelfRel));
}
/**
* General callback to produce a {@link Link} from the given {@link WebFluxLinkBuilder}.
*
* @param finisher must not be {@literal null}.
* @return
*/
public WebFluxLink toLink(Function<WebFluxLinkBuilder, Mono<Link>> finisher) {
Assert.notNull(finisher, "Finisher must not be null!");
return new WebFluxLink(builder.flatMap(finisher));
}
}
/**
* Intermediate representation of a {@link Link} within a reactive pipeline to easily add {@link Affordance}s from
* method invocations.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public static class WebFluxLink {
private final Mono<Link> link;
/**
* Adds the affordance created by the given virtual method invocation.
*
* @param invocation must not be {@literal null}.
* @return
* @see WebFluxLinkBuilder#methodOn(Class, Object...)
*/
public WebFluxLink andAffordance(Object invocation) {
Assert.notNull(invocation, "Invocation must not be null!");
return new WebFluxLink(link.flatMap(it -> linkToInternal(invocation) //
.flatMapIterable(WebFluxLinkBuilder::getAffordances) //
.singleOrEmpty() //
.map(it::andAffordance)));
}
/**
* Creates a new {@link WebFluxLink} with the current {@link Link} instance transformed using the given mapper.
*
* @param mapper must not be {@literal null}.
* @return
*/
public WebFluxLink map(Function<Link, Link> mapper) {
Assert.notNull(mapper, "Function must not be null!");
return new WebFluxLink(link.map(mapper));
}
/**
* Returns the underlying {@link Mono} of {@link Link} for further handling within a reactive pipeline.
*
* @return
*/
public Mono<Link> toMono() {
return link;
}
/**
* Returns a {@link Mono} of {@link Link} with the current one augmented by the given {@link Function}. Allows
* immediate customization of the {@link Link} instance and immediately return to a general reactive API.
*
* @param finisher must not be {@literal null}.
* @return
*/
public Mono<Link> toMono(Function<Link, Link> finisher) {
Assert.notNull(finisher, "Function must not be null!");
return link.map(finisher);
}
}
/**
* Returns a {@link UriComponentsBuilder} obtained from the {@link ServerWebExchange}.
*
* @param exchange
*/
private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) {
return exchange == null //
? UriComponentsBuilder.fromPath("/") //
: UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation) {
return Mono.subscriberContext() //
.flatMap(context -> linkToInternal(invocation, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
}
private static Mono<WebFluxLinkBuilder> linkToInternal(Object invocation, ServerWebExchange exchange) {
return Mono.just(WebHandler.linkTo(invocation, //
path -> getBuilder(exchange).replacePath(path == null ? "/" : path), //
WebFluxLinkBuilder::new));
}
}

View File

@@ -51,6 +51,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Kamill Sokol
* @author Ross Turner
*/
@Deprecated
public class ControllerLinkBuilderFactoryUnitTest extends TestUtils {
ControllerLinkBuilderFactory factory = new ControllerLinkBuilderFactory();

View File

@@ -28,6 +28,7 @@ import org.springframework.web.context.request.RequestContextHolder;
*
* @author Greg Turnquist
*/
@Deprecated
public class ControllerLinkBuilderOutsideSpringMvcUnitTest {
/**

View File

@@ -56,6 +56,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Oliver Trosien
* @author Greg Turnquist
*/
@Deprecated
public class ControllerLinkBuilderUnitTest extends TestUtils {
public @Rule ExpectedException exception = ExpectedException.none();

View File

@@ -16,11 +16,11 @@
package org.springframework.hateoas.mvc;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import org.junit.Test;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TestUtils;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -35,16 +35,16 @@ public class DummyInvocationUtilsUnitTest extends TestUtils {
@Test
public void pathVariableWithDefaultParameter() {
Link link = ControllerLinkBuilder.linkTo(DummyInvocationUtils.methodOn(SampleController.class).someMethod(1L))
.withSelfRel();
Link link = linkTo(methodOn(SampleController.class).someMethod(1L)).withSelfRel();
assertThat(link.getHref()).isEqualTo("http://localhost/sample/1/foo");
}
@Test
public void pathVariableWithNameParameter() {
Link link = ControllerLinkBuilder.linkTo(DummyInvocationUtils.methodOn(SampleController.class).someOtherMethod(2L))
.withSelfRel();
Link link = linkTo(methodOn(SampleController.class).someOtherMethod(2L)).withSelfRel();
assertThat(link.getHref()).isEqualTo("http://localhost/sample/2/bar");
}

View File

@@ -16,7 +16,7 @@
package org.springframework.hateoas.mvc;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.util.Arrays;
import java.util.List;

View File

@@ -17,7 +17,7 @@ package org.springframework.hateoas.mvc;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

View File

@@ -0,0 +1,208 @@
/*
* 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.mvc;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TestUtils;
import org.springframework.hateoas.mvc.ControllerLinkBuilderUnitTest.ControllerWithMethods;
import org.springframework.hateoas.mvc.ControllerLinkBuilderUnitTest.PersonControllerImpl;
import org.springframework.hateoas.mvc.ControllerLinkBuilderUnitTest.PersonsAddressesController;
import org.springframework.http.HttpEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Unit tests for {@link WebMvcLinkBuilderFactory}.
*
* @author Ricardo Gladwell
* @author Oliver Gierke
* @author Kamill Sokol
* @author Ross Turner
*/
public class WebMvcLinkBuilderFactoryUnitTest extends TestUtils {
WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory();
@Test
public void createsLinkToControllerRoot() {
Link link = factory.linkTo(PersonControllerImpl.class).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people");
}
@Test
public void createsLinkToParameterizedControllerRoot() {
Link link = factory.linkTo(PersonsAddressesController.class, 15).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/15/addresses");
}
@Test
public void appliesParameterValueIfContributorConfigured() {
WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory();
factory.setUriComponentsContributors(Collections.singletonList(new SampleUriComponentsContributor()));
SpecialType specialType = new SpecialType();
specialType.parameterValue = "value";
Link link = factory.linkTo(methodOn(SampleController.class).sampleMethod(1L, specialType)).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getHref()).endsWith("/sample/1?foo=value");
}
/**
* @see #57
*/
@Test
public void usesDateTimeFormatForUriBinding() {
DateTime now = DateTime.now();
WebMvcLinkBuilderFactory factory = new WebMvcLinkBuilderFactory();
Link link = factory.linkTo(methodOn(SampleController.class).sampleMethod(now)).withSelfRel();
assertThat(link.getHref()).endsWith("/sample/" + ISODateTimeFormat.date().print(now));
}
/**
* @see #96
*/
@Test
public void linksToMethodWithPathVariableContainingBlank() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("with blank")).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/something/with%20blank/foo");
}
/**
* @see #96
*/
@Test
public void createsLinkToParameterizedControllerRootContainingBlank() {
Link link = factory.linkTo(PersonsAddressesController.class, "with blank").withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/with%20blank/addresses");
}
/**
* @see #209
*/
@Test
public void createsLinkToControllerMethodWithMapRequestParam() {
Map<String, String> queryParams = new LinkedHashMap<>();
queryParams.put("firstKey", "firstValue");
queryParams.put("secondKey", "secondValue");
Link link = factory.linkTo(methodOn(SampleController.class).sampleMethodWithMap(queryParams)).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/sample/mapsupport?firstKey=firstValue&secondKey=secondValue");
}
/**
* @see #209
*/
@Test
public void createsLinkToControllerMethodWithMultiValueMapRequestParam() {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.put("key1", Arrays.asList("value1a", "value1b"));
queryParams.put("key2", Arrays.asList("value2a", "value2b"));
Link link = factory.linkTo(methodOn(SampleController.class).sampleMethodWithMap(queryParams)).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()) //
.endsWith("/sample/multivaluemapsupport?key1=value1a&key1=value1b&key2=value2a&key2=value2b");
}
/**
* @see #372
*/
@Test
public void createsLinkToParameterizedControllerRootWithParameterMap() {
Link link = factory.linkTo(PersonsAddressesController.class, Collections.singletonMap("id", "17")).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/17/addresses");
}
interface SampleController {
@RequestMapping("/sample/{id}")
HttpEntity<?> sampleMethod(@PathVariable("id") Long id, SpecialType parameter);
@RequestMapping("/sample/{time}")
HttpEntity<?> sampleMethod(@PathVariable("time") @DateTimeFormat(iso = ISO.DATE) DateTime time);
@RequestMapping("/sample/mapsupport")
HttpEntity<?> sampleMethodWithMap(@RequestParam Map<String, String> queryParams);
@RequestMapping("/sample/multivaluemapsupport")
HttpEntity<?> sampleMethodWithMap(@RequestParam MultiValueMap<String, String> queryParams);
}
static class SampleUriComponentsContributor implements UriComponentsContributor {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return SpecialType.class.equals(parameter.getParameterType());
}
@Override
public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) {
builder.queryParam("foo", ((SpecialType) value).parameterValue);
}
}
static class SpecialType {
String parameterValue;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.hateoas.Link;
import org.springframework.web.context.request.RequestContextHolder;
/**
* Test cases for {@link ControllerLinkBuilder} that are NOT inside an existing Spring MVC request
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
public class WebMvcLinkBuilderOutsideSpringMvcUnitTest {
/**
* Clear out any existing request attributes left behind by other tests
*/
@Before
public void setUp() {
RequestContextHolder.setRequestAttributes(null);
}
/**
* @see #408
*/
@Test
public void requestingLinkOutsideWebRequest() {
Link link = linkTo(
methodOn(WebMvcLinkBuilderUnitTest.PersonsAddressesController.class, 15).getAddressesForCountry("DE"))
.withSelfRel();
assertThat(link).isEqualTo(new Link("/people/15/addresses/DE").withSelfRel());
}
}

View File

@@ -0,0 +1,739 @@
/*
* 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.mvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariable.VariableType;
import org.springframework.hateoas.TestUtils;
import org.springframework.http.HttpEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Unit tests for {@link ControllerLinkBuilder}.
*
* @author Oliver Gierke
* @author Dietrich Schulten
* @author Kamill Sokol
* @author Oemer Yildiz
* @author Greg Turnquist
* @author Kevin Conaway
* @author Oliver Trosien
* @author Greg Turnquist
*/
public class WebMvcLinkBuilderUnitTest extends TestUtils {
public @Rule ExpectedException exception = ExpectedException.none();
@Test
public void createsLinkToControllerRoot() {
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people");
}
@Test
public void createsLinkToParameterizedControllerRoot() {
Link link = linkTo(PersonsAddressesController.class, 15).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/15/addresses");
}
/**
* @see #70
*/
@Test
public void createsLinkToMethodOnParameterizedControllerRoot() {
Link link = linkTo(methodOn(PersonsAddressesController.class, 15).getAddressesForCountry("DE")).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/15/addresses/DE");
}
@Test
public void createsLinkToSubResource() {
Link link = linkTo(PersonControllerImpl.class).slash("something").withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/people/something");
}
@Test
public void createsLinkWithCustomRel() {
Link link = linkTo(PersonControllerImpl.class).withRel(IanaLinkRelations.NEXT);
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.NEXT);
assertThat(link.getHref()).endsWith("/people");
}
/**
* @see #186
*/
@Test
public void usesFirstMappingInCaseMultipleOnesAreDefined() {
assertThat(linkTo(InvalidController.class).withSelfRel().getHref()).endsWith("/persons");
}
@Test
public void createsLinkToUnmappedController() {
Link link = linkTo(UnmappedController.class).withSelfRel();
assertThat(link.getHref()).isEqualTo("http://localhost");
}
@Test
@SuppressWarnings("unchecked")
public void usesIdOfIdentifyableForPathSegment() {
Identifiable<Long> identifyable = Mockito.mock(Identifiable.class);
Mockito.when(identifyable.getId()).thenReturn(Optional.of(10L));
Link link = linkTo(PersonControllerImpl.class).slash(identifyable).withSelfRel();
assertThat(link.getHref()).endsWith("/people/10");
}
@Test
public void appendingNullIsANoOp() {
Link link = linkTo(PersonControllerImpl.class).slash(null).withSelfRel();
assertThat(link.getHref()).endsWith("/people");
link = linkTo(PersonControllerImpl.class).slash((Object) null).withSelfRel();
assertThat(link.getHref()).endsWith("/people");
}
@Test
public void linksToMethod() {
Link link = linkTo(methodOn(ControllerWithMethods.class).myMethod(null)).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getHref()).endsWith("/something/else");
}
@Test
public void linksToMethodWithPathVariable() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("1")).withSelfRel();
assertPointsToMockServer(link);
assertThat(link.getHref()).endsWith("/something/1/foo");
}
/**
* @see #33
*/
@Test
public void usesForwardedHostAsHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://somethingDifferent"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslIfHeaderIsSet() {
request.addHeader("X-Forwarded-Ssl", "on");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("https://"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslIfHeaderIsSetOff() {
request.addHeader("X-Forwarded-Ssl", "off");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://"));
}
/**
* @see #112
*/
@Test
public void usesForwardedSslAndHostIfHeaderIsSet() {
request.addHeader("X-Forwarded-Host", "somethingDifferent");
request.addHeader("X-Forwarded-Ssl", "on");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("https://somethingDifferent"));
}
/**
* @see #26, #39
*/
@Test
public void addsRequestParametersHandedIntoSlashCorrectly() {
Link link = linkTo(PersonController.class).slash("?foo=bar").withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getQuery()).isEqualTo("foo=bar");
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndRequestParams() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, 5)).withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getPath()).isEqualTo("/something/1/foo");
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("offset"), contains("10"));
}
/**
* @see #26, #39
*/
@Test
public void linksToMethodWithPathVariableAndMultiValueRequestParams() {
Link link = linkTo(
methodOn(ControllerWithMethods.class).methodWithMultiValueRequestParams("1", Arrays.asList(3, 7), 5))
.withSelfRel();
UriComponents components = toComponents(link);
assertThat(components.getPath()).isEqualTo("/something/1/foo");
MultiValueMap<String, String> queryParams = components.getQueryParams();
assertThat(queryParams.get("limit"), contains("5"));
assertThat(queryParams.get("items"), containsInAnyOrder("3", "7"));
}
/**
* @see #26, #39
*/
@Test
public void returnsUriComponentsBuilder() {
UriComponents components = linkTo(PersonController.class).slash("something?foo=bar").toUriComponentsBuilder()
.build();
assertThat(components.getPath()).isEqualTo("/people/something");
assertThat(components.getQuery()).isEqualTo("foo=bar");
}
/**
* @see #90
*/
@Test
public void usesForwardedHostAndPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobar:8088");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobar:8088"));
}
/**
* @see #90
*/
@Test
public void usesFirstHostOfXForwardedHost() {
request.addHeader("X-Forwarded-Host", "barfoo:8888, localhost:8088");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://barfoo:8888"));
}
/**
* @see #122, #169
*/
@Test
public void appendsOptionalParameterVariableForUnsetParameter() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForOptionalNextPage(null)).withSelfRel();
assertThat(link.getVariables(), contains(new TemplateVariable("offset", VariableType.REQUEST_PARAM)));
assertThat(link.expand().getHref()).endsWith("/foo");
}
/**
* @see #122, #169
*/
@Test
public void rejectsMissingPathVariable() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable(null))//
.withSelfRel();
exception.expect(IllegalArgumentException.class);
link.expand();
}
/**
* @see #122, #169
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsMissingRequiredRequestParam() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithRequestParam(null)).withSelfRel();
assertThat(link.getVariableNames(), contains("id"));
link.expand();
}
/**
* @see #170
*/
@Test
public void usesForwardedPortFromHeader() {
request.addHeader("X-Forwarded-Host", "foobarhost");
request.addHeader("X-Forwarded-Port", "9090");
request.setServerPort(8080);
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobarhost:9090/"));
}
/**
* @see #170
*/
@Test
public void usesForwardedHostFromHeaderWithDefaultPort() {
request.addHeader("X-Forwarded-Host", "foobarhost");
request.setServerPort(8080);
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("http://foobarhost/"));
}
/**
* @see #114
*/
@Test
public void discoversParentClassTypeMappingForInvocation() {
Link link = linkTo(methodOn(ChildController.class).myMethod()).withSelfRel();
assertThat(link.getHref()).endsWith("/parent/child");
}
/**
* @see #114
*/
@Test
public void includesTypeMappingFromChildClass() {
Link link = linkTo(methodOn(ChildWithTypeMapping.class).myMethod()).withSelfRel();
assertThat(link.getHref()).endsWith("/child/parent");
}
/**
* @see #96
*/
@Test
public void linksToMethodWithPathVariableContainingBlank() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithPathVariable("with blank")).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/something/with%20blank/foo");
}
/**
* @see #192
*/
@Test
public void usesRootMappingOfTargetClassForMethodsOfParentClass() {
Link link = linkTo(methodOn(ChildControllerWithRootMapping.class) //
.someEmptyMappedMethod()) //
.withSelfRel();
assertThat(link.getHref()).endsWith("/root");
}
/**
* @see #192
*/
@Test
public void usesRootMappingOfTargetClassForMethodsOfParent() throws Exception {
Method method = ParentControllerWithoutRootMapping.class.getMethod("someEmptyMappedMethod");
Link link = linkTo(ChildControllerWithRootMapping.class, method).withSelfRel();
assertThat(link.getHref()).endsWith("/root");
}
/**
* @see #257, #107
*/
@Test
public void usesXForwardedProtoHeaderAsLinkSchema() {
for (String proto : Arrays.asList("http", "https")) {
setUp();
request.addHeader("X-Forwarded-Proto", proto);
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith(proto + "://"));
}
}
/**
* @see #257, #107
*/
@Test
public void usesProtoValueFromForwardedHeaderAsLinkSchema() {
for (String proto : Arrays.asList("http", "https")) {
setUp();
request.addHeader("Forwarded", new String[] { "proto=" + proto });
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith(proto.concat("://")));
}
}
/**
* @see #257, #107
*/
@Test
public void favorsStandardForwardHeaderOverXForwardedProto() {
request.addHeader("X-Forwarded-Proto", "foo");
request.addHeader("Forwarded", "proto=bar");
adaptRequestFromForwardedHeaders();
Link link = linkTo(PersonControllerImpl.class).withSelfRel();
assertThat(link.getHref(), startsWith("bar://"));
}
/**
* @see #331
*/
@Test
public void linksToMethodWithRequestParamImplicitlySetToFalse() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForOptionalSizeWithDefaultValue(null)).withSelfRel();
assertThat(link.getHref()).endsWith("/bar");
}
/**
* @see #398
*/
@Test
public void encodesRequestParameterWithSpecialValue() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithRequestParam("Spring#\n")).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).endsWith("/something/foo?id=Spring%23%0A");
}
/**
* @see #169
*/
@Test
public void createsPartiallyExpandedLink() {
Link link = linkTo(methodOn(PersonsAddressesController.class, "some id").getAddressesForCountry(null))
.withSelfRel();
assertThat(link.isTemplated()).isTrue();
assertThat(link.getHref()).contains("some%20id");
}
/**
* @see #169
*/
@Test
public void addsRequestParameterVariablesForMissingRequiredParameter() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", 10, null)).withSelfRel();
assertThat(link.getVariableNames(), contains("limit"));
exception.expect(IllegalArgumentException.class);
exception.expectMessage("limit");
link.expand();
}
/**
* @see #169
*/
@Test
public void addsOptionalRequestParameterTemplateForMissingValue() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodForNextPage("1", null, 5)).withSelfRel();
assertThat(link.getVariables(), contains(new TemplateVariable("offset", VariableType.REQUEST_PARAM_CONTINUED)));
UriComponents components = toComponents(link);
assertThat(components.getQueryParams().get("query")).isNull();
}
/**
* @see #509
*/
@Test
public void supportsTwoProxiesAddingXForwardedPort() {
request.addHeader("X-Forwarded-Port", "1443,8443");
request.addHeader("X-Forwarded-Host", "proxy1,proxy2");
adaptRequestFromForwardedHeaders();
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://proxy1:1443"));
}
/**
* @see #509
*/
@Test
public void resolvesAmbiguousXForwardedHeaders() {
request.addHeader("X-Forwarded-Proto", "http");
request.addHeader("X-Forwarded-Ssl", "on");
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref(), startsWith("http://"));
}
/**
* @see #527
*/
@Test
public void createsLinkRelativeToContextRoot() {
request.setContextPath("/ctx");
request.setServletPath("/foo");
request.setRequestURI("/ctx/foo");
assertThat(linkTo(PersonControllerImpl.class).withSelfRel().getHref()).endsWith("/ctx/people");
}
/**
* @see #639
*/
@Test
public void considersEmptyOptionalMethodParameterOptional() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional.empty())).withSelfRel();
assertThat(link.isTemplated()).isTrue();
assertThat(link.getVariableNames(), contains("value"));
}
/**
* @see #639
*/
@Test
public void considersOptionalWithValueMethodParameterOptional() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithJdk8Optional(Optional.of(1))).withSelfRel();
assertThat(link.isTemplated()).isFalse();
assertThat(link.getHref()).endsWith("?value=1");
}
/**
* @see #617
*/
@Test
public void alternativePathVariableParameter() {
Link link = linkTo(methodOn(ControllerWithMethods.class).methodWithAlternatePathVariable("bar")).withSelfRel();
assertThat(link.getHref()).isEqualTo("http://localhost/something/bar/foo");
}
private static UriComponents toComponents(Link link) {
return UriComponentsBuilder.fromUriString(link.expand().getHref()).build();
}
static class Person implements Identifiable<Long> {
Long id;
@Override
public Optional<Long> getId() {
return Optional.ofNullable(id);
}
}
@RequestMapping("/people")
interface PersonController {}
class PersonControllerImpl implements PersonController {}
@RequestMapping("/people/{id}/addresses")
static class PersonsAddressesController {
@RequestMapping("/{country}")
public HttpEntity<Void> getAddressesForCountry(@PathVariable String country) {
return null;
}
}
@RequestMapping({ "/persons", "/people" })
class InvalidController {
}
class UnmappedController {
}
@RequestMapping("/something")
static class ControllerWithMethods {
@RequestMapping("/else")
HttpEntity<Void> myMethod(@RequestBody Object payload) {
return null;
}
@RequestMapping("/{id}/foo")
HttpEntity<Void> methodWithPathVariable(@PathVariable String id) {
return null;
}
@RequestMapping("/foo")
HttpEntity<Void> methodWithRequestParam(@RequestParam String id) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodForNextPage(@PathVariable String id, @RequestParam(required = false) Integer offset,
@RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodWithMultiValueRequestParams(@PathVariable String id, @RequestParam List<Integer> items,
@RequestParam Integer limit) {
return null;
}
@RequestMapping(value = "/{id}/foo")
HttpEntity<Void> methodWithAlternatePathVariable(@PathVariable(name = "id") String otherId) {
return null;
}
@RequestMapping(value = "/foo")
HttpEntity<Void> methodForOptionalNextPage(@RequestParam(required = false) Integer offset) {
return null;
}
@RequestMapping(value = "/bar")
HttpEntity<Void> methodForOptionalSizeWithDefaultValue(@RequestParam(defaultValue = "10") Integer size) {
return null;
}
@RequestMapping
HttpEntity<Void> methodWithJdk8Optional(@RequestParam Optional<Integer> value) {
return null;
}
}
@RequestMapping("/parent")
interface ParentController {}
interface ChildController extends ParentController {
@RequestMapping("/child")
Object myMethod();
}
interface ParentWithMethod {
@RequestMapping("/parent")
Object myMethod();
}
@RequestMapping("/child")
interface ChildWithTypeMapping extends ParentWithMethod {}
interface ParentControllerWithoutRootMapping {
@RequestMapping
Object someEmptyMappedMethod();
}
@RequestMapping("/root")
interface ChildControllerWithRootMapping extends ParentControllerWithoutRootMapping {
}
}

View File

@@ -17,8 +17,7 @@ package org.springframework.hateoas.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import static org.springframework.hateoas.core.DummyInvocationUtils.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.*;
import static org.springframework.hateoas.reactive.WebFluxLinkBuilder.*;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -89,9 +88,7 @@ public class HypermediaWebFilterTest {
@GetMapping
Mono<ResourceSupport> root() {
return linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.map(ResourceSupport::new);
return linkTo(methodOn(TestController.class).root()).withSelfRel().toMono().map(ResourceSupport::new);
}
}

View File

@@ -17,9 +17,9 @@ package org.springframework.hateoas.reactive;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.methodOn;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.linkTo;
import static org.springframework.hateoas.reactive.WebFluxLinkBuilder.linkTo;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -33,7 +33,6 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
@@ -42,10 +41,13 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
/**
* Unit tests for {@link WebFluxLinkBuilder}.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
@RunWith(MockitoJUnitRunner.class)
public class ReactiveLinkBuilderTest {
public class WebFluxLinkBuilderTest {
@Mock ServerWebExchange exchange;
@Mock ServerHttpRequest request;
@@ -57,14 +59,18 @@ public class ReactiveLinkBuilderTest {
public void linkAtSameLevelAsExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel().toMono() //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
}).verifyComplete();
}
/**
@@ -74,13 +80,12 @@ public class ReactiveLinkBuilderTest {
public void linkAtSameLevelAsContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).root()).withSelfRel().toMono() //
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)) //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
@@ -100,10 +105,16 @@ public class ReactiveLinkBuilderTest {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api/employees"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel().toMono() //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
}).verifyComplete();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
}
/**
@@ -113,18 +124,18 @@ public class ReactiveLinkBuilderTest {
public void shallowLinkFromDeepContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api/employees"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).root()).withSelfRel().toMono() //
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)) //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
}).verifyComplete();
}
@@ -135,14 +146,18 @@ public class ReactiveLinkBuilderTest {
public void deepLinkFromShallowExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).deep(), this.exchange).withSelfRel();
linkTo(methodOn(TestController.class).deep(), this.exchange).withSelfRel().toMono() //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
return true;
}).verifyComplete();
}
/**
@@ -152,18 +167,18 @@ public class ReactiveLinkBuilderTest {
public void deepLinkFromShallowContextProvidedServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).deep()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).deep()).withSelfRel().toMono() //
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)) //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
return true;
}).verifyComplete();
}
@@ -174,18 +189,18 @@ public class ReactiveLinkBuilderTest {
public void linkToRouteWithNoMappingShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController2.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController2.class).root()).withSelfRel().toMono() //
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)) //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/");
return true;
}).verifyComplete();
}
@@ -195,15 +210,14 @@ public class ReactiveLinkBuilderTest {
@Test
public void linkToRouteWithNoExchangeInTheContextShouldFallbackToRelativeUris() throws URISyntaxException {
linkTo(methodOn(TestController2.class).root())//
.map(ReactiveLinkBuilder::withSelfRel) //
.as(StepVerifier::create) //
.expectNextMatches(link -> {
linkTo(methodOn(TestController2.class).root()).withSelfRel().toMono() //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("/");
return true;
}).verifyComplete();
}
@@ -213,10 +227,16 @@ public class ReactiveLinkBuilderTest {
@Test
public void linkToRouteWithExplictExchangeBeingNullShouldFallbackToRelativeUris() throws URISyntaxException {
Link link = linkTo(methodOn(TestController2.class).root(), null).withSelfRel();
linkTo(methodOn(TestController2.class).root(), null).withSelfRel().toMono() //
.as(StepVerifier::create).expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("/");
return true;
}).verifyComplete();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("/");
}
@RestController

View File

@@ -15,8 +15,7 @@
*/
package org.springframework.hateoas.support;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.linkTo;
import static org.springframework.hateoas.reactive.WebFluxLinkBuilder.*;
import static reactor.function.TupleUtils.*;
import reactor.core.publisher.Flux;
@@ -27,10 +26,13 @@ import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.reactive.ReactiveLinkBuilder;
import org.springframework.hateoas.reactive.WebFluxLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
@@ -42,7 +44,10 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Sample controller using {@link WebFluxLinkBuilder} to create {@link Affordance}s.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
@RestController
public class WebFluxEmployeeController {
@@ -60,44 +65,61 @@ public class WebFluxEmployeeController {
@GetMapping("/employees")
public Mono<Resources<Resource<Employee>>> all() {
return Flux.fromIterable(EMPLOYEES.keySet()).flatMap(id -> findOne(id)).collectList().flatMap(
resources -> linkTo(methodOn(WebFluxEmployeeController.class).all()).map(ReactiveLinkBuilder::withSelfRel)
.map(link -> link.andAffordance(afford(methodOn(WebFluxEmployeeController.class).newEmployee(null)))
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).search(null, null))))
Class<WebFluxEmployeeController> controller = WebFluxEmployeeController.class;
return Flux.fromIterable(EMPLOYEES.keySet()) //
.flatMap(id -> findOne(id)) //
.collectList() //
.flatMap(resources -> linkTo(methodOn(controller).all()).withSelfRel() //
.andAffordance(methodOn(controller).newEmployee(null)) //
.andAffordance(methodOn(controller).search(null, null)) //
.toMono() //
.map(selfLink -> new Resources<>(resources, selfLink)));
}
@GetMapping("/employees/search")
public Mono<Resources<Resource<Employee>>> search(@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "role", required = false) String role) {
public Mono<Resources<Resource<Employee>>> search( //
@RequestParam Optional<String> name, //
@RequestParam Optional<String> role) {
return Flux.fromIterable(EMPLOYEES.keySet()).flatMap(id -> findOne(id)).filter(resource -> {
return Flux.fromIterable(EMPLOYEES.keySet()) //
.flatMap(id -> findOne(id)) //
.filter(resource -> {
boolean nameMatches = Optional.ofNullable(name).map(s -> resource.getContent().getName().contains(s))
.orElse(true);
boolean nameMatches = name //
.map(s -> resource.getContent().getName().contains(s)) //
.orElse(true);
boolean roleMatches = Optional.ofNullable(role).map(s -> resource.getContent().getRole().contains(s))
.orElse(true);
boolean roleMatches = name.map(s -> resource.getContent().getRole().contains(s)) //
.orElse(true);
return nameMatches && roleMatches;
}).collectList().flatMap(
resources -> linkTo(methodOn(WebFluxEmployeeController.class).all()).map(ReactiveLinkBuilder::withSelfRel)
.map(link -> link.andAffordance(afford(methodOn(WebFluxEmployeeController.class).newEmployee(null)))
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).search(null, null))))
.map(selfLink -> new Resources<>(resources, selfLink)));
return nameMatches && roleMatches;
}).collectList().flatMap(resources -> {
Class<WebFluxEmployeeController> controller = WebFluxEmployeeController.class;
return linkTo(methodOn(controller).all()).withSelfRel() //
.andAffordance(methodOn(controller).newEmployee(null)) //
.andAffordance(methodOn(controller).search(null, null)) //
.toMono() //
.map(selfLink -> new Resources<>(resources, selfLink));
});
}
@GetMapping("/employees/{id}")
public Mono<Resource<Employee>> findOne(@PathVariable Integer id) {
return linkTo(methodOn(WebFluxEmployeeController.class).findOne(id)) //
.map(ReactiveLinkBuilder::withSelfRel) //
.zipWith(linkTo(methodOn(WebFluxEmployeeController.class).all()) //
.map(reactiveLinkBuilder -> reactiveLinkBuilder.withRel("employees")))
.map(function((selfLink, employeesLink) -> new Resource<>(EMPLOYEES.get(id),
selfLink.andAffordance(afford(methodOn(WebFluxEmployeeController.class).updateEmployee(null, id))) //
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).partiallyUpdateEmployee(null, id))),
employeesLink)));
Mono<Link> selfLink = linkTo(methodOn(WebFluxEmployeeController.class).findOne(id)).withSelfRel() //
.andAffordance(methodOn(WebFluxEmployeeController.class).updateEmployee(null, id)) //
.andAffordance(methodOn(WebFluxEmployeeController.class).partiallyUpdateEmployee(null, id)) //
.toMono();
Mono<Link> employeesLink = linkTo(methodOn(WebFluxEmployeeController.class).all()).withRel("employees") //
.toMono();
return selfLink.zipWith(employeesLink) //
.map(function((left, right) -> Links.of(left, right))) //
.map(links -> new Resource<>(EMPLOYEES.get(id), links));
}
@PostMapping("/employees")

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.hateoas.support;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.mvc.WebMvcLinkBuilder.*;
import java.net.URI;
import java.net.URISyntaxException;
@@ -24,6 +24,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
@@ -58,21 +60,15 @@ public class WebMvcEmployeeController {
@GetMapping("/employees")
public Resources<Resource<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
employees.add(findOne(i));
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(WebMvcEmployeeController.class).all()).withSelfRel() //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).newEmployee(null))) //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).search(null, null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
return IntStream.range(0, EMPLOYEES.size()) //
.mapToObj(this::findOne) //
.collect(Collectors.collectingAndThen(Collectors.toList(), it -> new Resources<>(it, selfLink)));
}
@GetMapping("/employees/search")