#1588 - WebMvcLinkBuilder.linkTo(Method, Object[]) now properly handles request parameters.

We now route calls to WebMvcLinkBuilder.linkTo(Method, Object[]) through the same infrastructure that linkTo(…) calls with a dummy method invocation are routed through. We construct a fake LastInvocationAware pointing to the given Method and parameters.
This commit is contained in:
Oliver Drotbohm
2021-07-21 09:46:48 +02:00
parent a85952ebc1
commit ce71b144a1
4 changed files with 217 additions and 79 deletions

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2021 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
*
* https://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.server.core;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Simple {@link MethodInvocation} implementation that can also be directly used as {@link LastInvocationAware}.
*
* @author Oliver Drotbohm
*/
class DefaultMethodInvocation implements MethodInvocation, LastInvocationAware {
private final Class<?> type;
private final Method method;
private final Object[] arguments;
/**
* Creates a new {@link DefaultMethodInvocation} for the given type, method and parameters.
*
* @param type must not be {@literal null}.
* @param method must not be {@literal null}.
* @param arguments must not be {@literal null}.
*/
public DefaultMethodInvocation(Class<?> type, Method method, Object[] arguments) {
Assert.notNull(type, "targetType must not be null!");
Assert.notNull(method, "method must not be null!");
Assert.notNull(arguments, "arguments must not be null!");
this.type = type;
this.method = method;
this.arguments = arguments;
}
public DefaultMethodInvocation(Method method, Object[] arguments) {
this(method.getDeclaringClass(), method, arguments);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.server.core.MethodInvocation#getTargetType()
*/
public Class<?> getTargetType() {
return this.type;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.server.core.MethodInvocation#getMethod()
*/
public Method getMethod() {
return this.method;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.server.core.MethodInvocation#getArguments()
*/
public Object[] getArguments() {
return this.arguments;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.server.core.LastInvocationAware#getLastInvocation()
*/
@Override
public MethodInvocation getLastInvocation() {
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.server.core.LastInvocationAware#getObjectParameters()
*/
@Override
public Iterator<Object> getObjectParameters() {
return Collections.emptyIterator();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof DefaultMethodInvocation)) {
return false;
}
DefaultMethodInvocation that = (DefaultMethodInvocation) o;
return Objects.equals(this.type, that.type) //
&& Objects.equals(this.method, that.method) //
&& Arrays.equals(this.arguments, that.arguments);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = Objects.hash(this.type, this.method);
result = 31 * result + Arrays.hashCode(this.arguments);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "DefaultMethodInvocation(targetType=" + this.type //
+ ", method=" + this.method //
+ ", arguments=" + Arrays.deepToString(this.arguments) + ")";
}
}

View File

@@ -82,7 +82,7 @@ public class DummyInvocationUtils {
return ReflectionUtils.invokeMethod(method, invocation.getThis(), invocation.getArguments());
}
this.invocation = new SimpleMethodInvocation(targetType, method, invocation.getArguments());
this.invocation = new DefaultMethodInvocation(targetType, method, invocation.getArguments());
Class<?> returnType = method.getReturnType();
ClassLoader classLoader = method.getDeclaringClass().getClassLoader();
@@ -144,7 +144,35 @@ public class DummyInvocationUtils {
*/
@Nullable
public static LastInvocationAware getLastInvocationAware(Object source) {
return (LastInvocationAware) ((Advised) source).getAdvisors()[0].getAdvice();
return (LastInvocationAware) (Advised.class.isInstance(source)
? ((Advised) source).getAdvisors()[0].getAdvice()
: source);
}
/**
* Creates a simple {@link LastInvocationAware} for the given method and parameters.
*
* @param method must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @return will never be {@literal null}.
* @since 1.3.4
*/
public static LastInvocationAware getLastInvocationAware(Method method, Object[] parameters) {
return getLastInvocationAware(method.getDeclaringClass(), method, parameters);
}
/**
* Creates a simple {@link LastInvocationAware} from the given type, method and parameters.
*
* @param type must not be {@literal null}.
* @param method must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @return will never be {@literal null}.
* @since 1.3.4
*/
public static LastInvocationAware getLastInvocationAware(Class<?> type, Method method, Object[] parameters) {
return new DefaultMethodInvocation(type, method, parameters);
}
@SuppressWarnings("unchecked")
@@ -180,25 +208,31 @@ public class DummyInvocationUtils {
return new CacheKey<T>(type, arguments);
}
public Class<T> getType() {
return this.type;
}
public Object[] getArguments() {
return this.arguments;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object o) {
if (this == o)
if (this == o) {
return true;
if (!(o instanceof CacheKey))
}
if (!(o instanceof CacheKey)) {
return false;
}
CacheKey<?> cacheKey = (CacheKey<?>) o;
return Objects.equals(this.type, cacheKey.type) && Arrays.equals(this.arguments, cacheKey.arguments);
return Objects.equals(this.type, cacheKey.type) //
&& Arrays.equals(this.arguments, cacheKey.arguments);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
@@ -207,66 +241,16 @@ public class DummyInvocationUtils {
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "DummyInvocationUtils.CacheKey(type=" + this.type + ", arguments=" + Arrays.deepToString(this.arguments)
return "DummyInvocationUtils.CacheKey(type=" + this.type //
+ ", arguments=" + Arrays.deepToString(this.arguments) //
+ ")";
}
}
private static final class SimpleMethodInvocation implements MethodInvocation {
private final Class<?> targetType;
private final Method method;
private final Object[] arguments;
public SimpleMethodInvocation(Class<?> targetType, Method method, Object[] arguments) {
Assert.notNull(targetType, "targetType must not be null!");
Assert.notNull(method, "method must not be null!");
Assert.notNull(arguments, "arguments must not be null!");
this.targetType = targetType;
this.method = method;
this.arguments = arguments;
}
public Class<?> getTargetType() {
return this.targetType;
}
public Method getMethod() {
return this.method;
}
public Object[] getArguments() {
return this.arguments;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof SimpleMethodInvocation))
return false;
SimpleMethodInvocation that = (SimpleMethodInvocation) o;
return Objects.equals(this.targetType, that.targetType) && Objects.equals(this.method, that.method)
&& Arrays.equals(this.arguments, that.arguments);
}
@Override
public int hashCode() {
int result = Objects.hash(this.targetType, this.method);
result = 31 * result + Arrays.hashCode(this.arguments);
return result;
}
public String toString() {
return "DummyInvocationUtils.SimpleMethodInvocation(targetType=" + this.targetType + ", method=" + this.method
+ ", arguments=" + Arrays.deepToString(this.arguments) + ")";
}
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.hateoas.server.mvc;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -27,12 +26,10 @@ import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.server.core.DummyInvocationUtils;
import org.springframework.hateoas.server.core.SpringAffordanceBuilder;
import org.springframework.hateoas.server.core.TemplateVariableAwareLinkBuilderSupport;
import org.springframework.hateoas.server.core.UriTemplateFactory;
import org.springframework.util.Assert;
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.
@@ -123,6 +120,10 @@ public class WebMvcLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<W
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(Method, Object...)
*/
public static WebMvcLinkBuilder linkTo(Method method, Object... parameters) {
Assert.notNull(method, "Method must not be null!");
Assert.notNull(parameters, "Parameters must not be null!");
return linkTo(method.getDeclaringClass(), method, parameters);
}
@@ -133,12 +134,9 @@ public class WebMvcLinkBuilder extends TemplateVariableAwareLinkBuilderSupport<W
Assert.notNull(controller, "Controller type must not be null!");
Assert.notNull(method, "Method must not be null!");
Assert.notNull(parameters, "Parameters must not be null!");
String mapping = SpringAffordanceBuilder.DISCOVERER.getMapping(controller, method);
UriTemplate template = UriTemplateFactory.templateFor(mapping);
URI uri = template.expand(parameters);
return new WebMvcLinkBuilder(UriComponentsBuilderFactory.getComponents()).slash(uri);
return linkTo(DummyInvocationUtils.getLastInvocationAware(controller, method, parameters));
}
/**

View File

@@ -618,6 +618,15 @@ class WebMvcLinkBuilderUnitTest extends TestUtils {
}
}
@Test // #1588, #1589
void buildsLinkFromMethodAndParameters() throws Exception {
Method method = ControllerWithMethods.class.getDeclaredMethod("methodWithRequestParam", String.class);
assertThat(linkTo(method, "someString").withSelfRel().getHref()).endsWith("?id=someString");
assertThat(linkTo(method, new Object[] { null }).withSelfRel().getHref()).endsWith("?id={id}");
}
private static UriComponents toComponents(Link link) {
return UriComponentsBuilder.fromUriString(link.expand().getHref()).build();
}