Introduced MvcUriComponentsBuilder to create URIs pointing to controller methods.
MvcUriComponentsBuilder allows creating URIs that point to Spring MVC controller methods annotated with @RequestMapping. It builds them by exposing a mock method invocation API similar to Mockito, records the method invocations and thus builds up the URI by inspecting the mapping annotations and the parameters handed into the method invocations. Introduced a new SPI UriComponentsContributor that should be implemented by HandlerMethodArgumentResolvers that actually contribute path segments or query parameters to a URI. While the newly introduced MvcUriComponentsBuilder looks up those UriComponentsContributor instances from the MVC configuration. The MvcUriComponentsBuilderFactory (name to be discussed - MvcUris maybe?) prevents the multiple lookups by keeping the UriComponentsBuilder instances in an instance variable. So an instance of the factory could be exposed as Spring bean or through a HandlerMethodArgumentResolver to be injected into Controller methods. Issue: SPR-10665, SPR-8826
This commit is contained in:
committed by
Rossen Stoyanchev
parent
92a48b72d7
commit
4fd27b12fc
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.AnnotationAttribute;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.MethodParameters;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
|
||||
import org.springframework.web.util.UriTemplate;
|
||||
|
||||
/**
|
||||
* Value object to allow accessing {@link RecordedMethodInvocation} parameters with the
|
||||
* configured {@link AnnotationAttribute}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class AnnotatedParametersParameterAccessor {
|
||||
|
||||
private final AnnotationAttribute attribute;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AnnotatedParametersParameterAccessor} using the given
|
||||
* {@link AnnotationAttribute}.
|
||||
*
|
||||
* @param attribute must not be {@literal null}.
|
||||
*/
|
||||
public AnnotatedParametersParameterAccessor(AnnotationAttribute attribute) {
|
||||
|
||||
Assert.notNull(attribute);
|
||||
this.attribute = attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link BoundMethodParameter}s contained in the given
|
||||
* {@link RecordedMethodInvocation}.
|
||||
*
|
||||
* @param invocation must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public List<BoundMethodParameter> getBoundParameters(RecordedMethodInvocation invocation) {
|
||||
|
||||
Assert.notNull(invocation, "RecordedMethodInvocation must not be null!");
|
||||
|
||||
MethodParameters parameters = new MethodParameters(invocation.getMethod());
|
||||
Object[] arguments = invocation.getArguments();
|
||||
List<BoundMethodParameter> result = new ArrayList<BoundMethodParameter>();
|
||||
|
||||
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
|
||||
result.add(new BoundMethodParameter(parameter,
|
||||
arguments[parameter.getParameterIndex()], attribute));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a {@link MethodParameter} alongside the value it has been bound to.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class BoundMethodParameter {
|
||||
|
||||
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
|
||||
|
||||
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
|
||||
private final MethodParameter parameter;
|
||||
|
||||
private final Object value;
|
||||
|
||||
private final AnnotationAttribute attribute;
|
||||
|
||||
private final TypeDescriptor parameterTypeDecsriptor;
|
||||
|
||||
/**
|
||||
* Creates a new {@link BoundMethodParameter}
|
||||
*
|
||||
* @param parameter
|
||||
* @param value
|
||||
* @param attribute
|
||||
*/
|
||||
public BoundMethodParameter(MethodParameter parameter, Object value,
|
||||
AnnotationAttribute attribute) {
|
||||
|
||||
Assert.notNull(parameter, "MethodParameter must not be null!");
|
||||
|
||||
this.parameter = parameter;
|
||||
this.value = value;
|
||||
this.attribute = attribute;
|
||||
this.parameterTypeDecsriptor = TypeDescriptor.nested(parameter, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the {@link UriTemplate} variable to be bound. The name will
|
||||
* be derived from the configured {@link AnnotationAttribute} or the
|
||||
* {@link MethodParameter} name as fallback.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getVariableName() {
|
||||
|
||||
if (attribute == null) {
|
||||
return parameter.getParameterName();
|
||||
}
|
||||
|
||||
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
|
||||
String annotationAttributeValue = attribute.getValueFrom(annotation).toString();
|
||||
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue
|
||||
: parameter.getParameterName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw value bound to the {@link MethodParameter}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bound value converted into a {@link String} based on default
|
||||
* conversion service setup.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String asString() {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) CONVERSION_SERVICE.convert(value, parameterTypeDecsriptor,
|
||||
STRING_DESCRIPTOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.core.AnnotationAttribute;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link MappingDiscoverer} implementation that inspects mappings from a particular
|
||||
* annotation.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class AnnotationMappingDiscoverer {
|
||||
|
||||
private final AnnotationAttribute attribute;
|
||||
|
||||
/**
|
||||
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type. Will
|
||||
* lookup the {@code value} attribute by default.
|
||||
*
|
||||
* @param annotation must not be {@literal null}.
|
||||
*/
|
||||
public AnnotationMappingDiscoverer(Class<? extends Annotation> annotation) {
|
||||
this(new AnnotationAttribute(annotation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link AnnotationMappingDiscoverer} for the given annotation type and
|
||||
* attribute name.
|
||||
*
|
||||
* @param annotation must not be {@literal null}.
|
||||
* @param mappingAttributeName if {@literal null}, it defaults to {@code value}.
|
||||
*/
|
||||
public AnnotationMappingDiscoverer(AnnotationAttribute attribute) {
|
||||
|
||||
Assert.notNull(attribute);
|
||||
this.attribute = attribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mapping associated with the given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return the type-level mapping or {@literal null} in case none is present.
|
||||
*/
|
||||
public String getMapping(Class<?> type) {
|
||||
|
||||
String[] mapping = getMappingFrom(attribute.findValueOn(type));
|
||||
|
||||
if (mapping.length > 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Multiple class level mappings defined on class %s!", type.getName()));
|
||||
}
|
||||
|
||||
return mapping.length == 0 ? null : mapping[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mapping associated with the given {@link Method}. This will include the
|
||||
* type-level mapping.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return the method mapping including the type-level one or {@literal null} if
|
||||
* neither of them present.
|
||||
*/
|
||||
public String getMapping(Method method) {
|
||||
|
||||
String[] mapping = getMappingFrom(attribute.findValueOn(method));
|
||||
|
||||
if (mapping.length > 1) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Multiple method level mappings defined on method %s!",
|
||||
method.toString()));
|
||||
}
|
||||
|
||||
String typeMapping = getMapping(method.getDeclaringClass());
|
||||
|
||||
if (mapping == null || mapping.length == 0) {
|
||||
return typeMapping;
|
||||
}
|
||||
|
||||
return typeMapping == null || "/".equals(typeMapping) ? mapping[0] : typeMapping
|
||||
+ mapping[0];
|
||||
}
|
||||
|
||||
private String[] getMappingFrom(Object annotationValue) {
|
||||
|
||||
if (annotationValue instanceof String) {
|
||||
return new String[] { (String) annotationValue };
|
||||
}
|
||||
else if (annotationValue instanceof String[]) {
|
||||
return (String[]) annotationValue;
|
||||
}
|
||||
else if (annotationValue == null) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Unsupported type for the mapping attribute! Support String and String[] but got %s!",
|
||||
annotationValue.getClass()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.AnnotationAttribute;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.MethodParameters;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.hypermedia.AnnotatedParametersParameterAccessor.BoundMethodParameter;
|
||||
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.LastInvocationAware;
|
||||
import org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.RecordedMethodInvocation;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
//import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.util.UriTemplate;
|
||||
|
||||
import static org.springframework.web.servlet.hypermedia.RecordedInvocationUtils.*;
|
||||
|
||||
/**
|
||||
* Builder to ease building {@link URI} instances pointing to Spring MVC controllers.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class MvcUriComponentsBuilder extends UriComponentsBuilder {
|
||||
|
||||
private static final AnnotationMappingDiscoverer 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 AnnotatedParametersParameterAccessor(
|
||||
new AnnotationAttribute(RequestParam.class));
|
||||
|
||||
private final List<UriComponentsContributor> contributors;
|
||||
|
||||
@Autowired(required = false)
|
||||
private RequestMappingHandlerAdapter adapter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link LinkBuilderSupport} to grab the
|
||||
* {@link UriComponentsContributor}s registered in the
|
||||
* {@link RequestMappingHandlerAdapter}.
|
||||
*
|
||||
* @param builder must not be {@literal null}.
|
||||
*/
|
||||
MvcUriComponentsBuilder() {
|
||||
|
||||
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
|
||||
List<UriComponentsContributor> contributors = new ArrayList<UriComponentsContributor>();
|
||||
|
||||
if (adapter != null) {
|
||||
for (HandlerMethodArgumentResolver resolver : adapter.getArgumentResolvers()) {
|
||||
if (resolver instanceof UriComponentsContributor) {
|
||||
contributors.add((UriComponentsContributor) resolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.contributors = contributors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MvcUriComponentsBuilder} 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 UriComponentsBuilder from(Class<?> controller) {
|
||||
return from(controller, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MvcUriComponentsBuilder} 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 UriComponentsBuilder from(Class<?> controller, Object... parameters) {
|
||||
|
||||
Assert.notNull(controller);
|
||||
|
||||
String mapping = DISCOVERER.getMapping(controller);
|
||||
UriTemplate template = new UriTemplate(mapping == null ? "/" : mapping);
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
|
||||
return getRootBuilder().with(builder);
|
||||
}
|
||||
|
||||
public static UriComponentsBuilder from(Method method, Object... parameters) {
|
||||
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
|
||||
return from(method, parameters, builder.contributors);
|
||||
}
|
||||
|
||||
static UriComponentsBuilder from(Method method, Object[] parameters,
|
||||
List<UriComponentsContributor> contributors) {
|
||||
|
||||
UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method));
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(template.expand(parameters));
|
||||
|
||||
RecordedMethodInvocation invocation = getInvocation(method, parameters);
|
||||
UriComponentsBuilder appender = applyUriComponentsContributer(invocation,
|
||||
builder, contributors);
|
||||
|
||||
return getRootBuilder().with(appender);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MvcUriComponentsBuilder} pointing to a controller method. Hand in
|
||||
* a dummy method invocation result you can create via
|
||||
* {@link #methodOn(Class, Object...)} or
|
||||
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
|
||||
*
|
||||
* <pre>
|
||||
* @RequestMapping("/customers")
|
||||
* class CustomerController {
|
||||
*
|
||||
* @RequestMapping("/{id}/addresses")
|
||||
* HttpEntity<Addresses> showAddresses(@PathVariable Long id) { … }
|
||||
* }
|
||||
*
|
||||
* URI uri = linkTo(methodOn(CustomerController.class).showAddresses(2L)).toURI();
|
||||
* </pre>
|
||||
*
|
||||
* The resulting {@link URI} instance will point to {@code /customers/2/addresses}.
|
||||
* For more details on the method invocation constraints, see
|
||||
* {@link RecordedInvocationUtils#methodOn(Class, Object...)}.
|
||||
*
|
||||
* @param invocationValue
|
||||
* @return
|
||||
*/
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.Object)
|
||||
*/
|
||||
public static UriComponentsBuilder from(Object invocationValue) {
|
||||
|
||||
MvcUriComponentsBuilder builder = new MvcUriComponentsBuilder();
|
||||
return from(invocationValue, builder.contributors);
|
||||
}
|
||||
|
||||
static UriComponentsBuilder from(Object invocationValue,
|
||||
List<? extends UriComponentsContributor> contributors) {
|
||||
|
||||
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
|
||||
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
|
||||
|
||||
RecordedMethodInvocation invocation = invocations.getLastInvocation();
|
||||
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
|
||||
Method method = invocation.getMethod();
|
||||
|
||||
String mapping = DISCOVERER.getMapping(method);
|
||||
UriComponentsBuilder builder = getRootBuilder().path(mapping);
|
||||
|
||||
UriTemplate template = new UriTemplate(mapping);
|
||||
Map<String, Object> values = new HashMap<String, Object>();
|
||||
|
||||
Iterator<String> names = template.getVariableNames().iterator();
|
||||
while (classMappingParameters.hasNext()) {
|
||||
values.put(names.next(), classMappingParameters.next());
|
||||
}
|
||||
|
||||
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
|
||||
values.put(parameter.getVariableName(), parameter.asString());
|
||||
}
|
||||
|
||||
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
|
||||
|
||||
Object value = parameter.getValue();
|
||||
String key = parameter.getVariableName();
|
||||
|
||||
if (value instanceof Collection) {
|
||||
for (Object element : (Collection<?>) value) {
|
||||
builder.queryParam(key, element);
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.queryParam(key, parameter.asString());
|
||||
}
|
||||
}
|
||||
|
||||
UriComponents components = applyUriComponentsContributer(invocation, builder,
|
||||
contributors).buildAndExpand(values);
|
||||
return UriComponentsBuilder.fromUri(components.toUri());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link RecordedInvocationUtils#methodOn(Class, Object...)} to be
|
||||
* available in case you work with static imports of {@link MvcUriComponentsBuilder}.
|
||||
*
|
||||
* @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 RecordedInvocationUtils.methodOn(controller, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link UriComponentsBuilder} obtained from the current servlet mapping
|
||||
* with the host tweaked in case the request contains an {@code X-Forwarded-Host}
|
||||
* header.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
static UriComponentsBuilder getRootBuilder() {
|
||||
|
||||
HttpServletRequest request = getCurrentRequest();
|
||||
UriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);
|
||||
|
||||
String header = request.getHeader("X-Forwarded-Host");
|
||||
|
||||
if (!StringUtils.hasText(header)) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
String[] hosts = StringUtils.commaDelimitedListToStringArray(header);
|
||||
String hostToUse = hosts[0];
|
||||
|
||||
if (hostToUse.contains(":")) {
|
||||
|
||||
String[] hostAndPort = StringUtils.split(hostToUse, ":");
|
||||
|
||||
builder.host(hostAndPort[0]);
|
||||
builder.port(Integer.parseInt(hostAndPort[1]));
|
||||
|
||||
}
|
||||
else {
|
||||
builder.host(hostToUse);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the configured {@link UriComponentsContributor}s to the given
|
||||
* {@link UriComponentsBuilder}.
|
||||
*
|
||||
* @param builder will never be {@literal null}.
|
||||
* @param invocation will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static UriComponentsBuilder applyUriComponentsContributer(
|
||||
RecordedMethodInvocation invocation, UriComponentsBuilder builder,
|
||||
Collection<? extends UriComponentsContributor> contributors) {
|
||||
|
||||
if (contributors.isEmpty()) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
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 : contributors) {
|
||||
if (contributor.supportsParameter(parameter)) {
|
||||
contributor.enhance(builder, parameter, parameterValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of {@link ServletUriComponentsBuilder#getCurrentRequest()} until SPR-10110
|
||||
* gets fixed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private static HttpServletRequest getCurrentRequest() {
|
||||
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
Assert.state(requestAttributes != null,
|
||||
"Could not find current request via RequestContextHolder");
|
||||
Assert.isInstanceOf(ServletRequestAttributes.class, requestAttributes);
|
||||
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
|
||||
Assert.state(servletRequest != null, "Could not find current HttpServletRequest");
|
||||
return servletRequest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author olivergierke
|
||||
*/
|
||||
public class MvcUriComponentsBuilderFactory implements MvcUris {
|
||||
|
||||
private final List<? extends UriComponentsContributor> contributors;
|
||||
|
||||
/**
|
||||
* @param contributors
|
||||
*/
|
||||
public MvcUriComponentsBuilderFactory(
|
||||
List<? extends UriComponentsContributor> contributors) {
|
||||
this.contributors = contributors;
|
||||
}
|
||||
|
||||
public UriComponentsBuilder from(Class<?> controller) {
|
||||
return from(controller, new Object[0]);
|
||||
}
|
||||
|
||||
public UriComponentsBuilder from(Class<?> controller, Object... parameters) {
|
||||
return MvcUriComponentsBuilder.from(controller, parameters);
|
||||
}
|
||||
|
||||
public UriComponentsBuilder from(Object invocationValue) {
|
||||
return MvcUriComponentsBuilder.from(invocationValue, contributors);
|
||||
}
|
||||
|
||||
public UriComponentsBuilder from(Method method, Object... parameters) {
|
||||
return MvcUriComponentsBuilder.from(method, parameters, contributors);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author olivergierke
|
||||
*/
|
||||
public interface MvcUris {
|
||||
|
||||
UriComponentsBuilder from(Class<?> controller);
|
||||
|
||||
UriComponentsBuilder from(Class<?> controller, Object... parameters);
|
||||
|
||||
UriComponentsBuilder from(Object invocationValue);
|
||||
|
||||
UriComponentsBuilder from(Method method, Object... parameters);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2012-2013 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.web.servlet.hypermedia;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.target.EmptyTargetSource;
|
||||
import org.springframework.cglib.proxy.Callback;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.Factory;
|
||||
import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.objenesis.ObjenesisStd;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Utility methods to capture dummy method invocations.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class RecordedInvocationUtils {
|
||||
|
||||
private static ObjenesisStd OBJENESIS = new ObjenesisStd(true);
|
||||
|
||||
public interface LastInvocationAware {
|
||||
|
||||
Iterator<Object> getObjectParameters();
|
||||
|
||||
RecordedMethodInvocation getLastInvocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method interceptor that records the last method invocation and creates a proxy for
|
||||
* the return value that exposes the method invocation.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class InvocationRecordingMethodInterceptor implements
|
||||
MethodInterceptor, LastInvocationAware,
|
||||
org.springframework.cglib.proxy.MethodInterceptor {
|
||||
|
||||
private static final Method GET_INVOCATIONS;
|
||||
|
||||
private static final Method GET_OBJECT_PARAMETERS;
|
||||
|
||||
private final Object[] objectParameters;
|
||||
|
||||
private RecordedMethodInvocation invocation;
|
||||
|
||||
static {
|
||||
GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class,
|
||||
"getLastInvocation");
|
||||
GET_OBJECT_PARAMETERS = ReflectionUtils.findMethod(LastInvocationAware.class,
|
||||
"getObjectParameters");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given
|
||||
* parameters forward that might be needed to populate the class level mapping.
|
||||
*
|
||||
* @param parameters
|
||||
*/
|
||||
public InvocationRecordingMethodInterceptor(Object... parameters) {
|
||||
this.objectParameters = parameters.clone();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.springframework.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
|
||||
* java.lang.reflect.Method, java.lang.Object[],
|
||||
* org.springframework.cglib.proxy.MethodProxy)
|
||||
*/
|
||||
public Object intercept(Object obj, Method method, Object[] args,
|
||||
MethodProxy proxy) {
|
||||
|
||||
if (GET_INVOCATIONS.equals(method)) {
|
||||
return getLastInvocation();
|
||||
}
|
||||
else if (GET_OBJECT_PARAMETERS.equals(method)) {
|
||||
return getObjectParameters();
|
||||
}
|
||||
else if (ReflectionUtils.isObjectMethod(method)) {
|
||||
return ReflectionUtils.invokeMethod(method, obj, args);
|
||||
}
|
||||
|
||||
this.invocation = new SimpleRecordedMethodInvocation(method, args);
|
||||
|
||||
Class<?> returnType = method.getReturnType();
|
||||
return returnType.cast(getProxyWithInterceptor(returnType, this));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept
|
||||
* .MethodInvocation)
|
||||
*/
|
||||
@Override
|
||||
public Object invoke(org.aopalliance.intercept.MethodInvocation invocation)
|
||||
throws Throwable {
|
||||
return intercept(invocation.getThis(), invocation.getMethod(),
|
||||
invocation.getArguments(), null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
|
||||
* getLastInvocation()
|
||||
*/
|
||||
@Override
|
||||
public RecordedMethodInvocation getLastInvocation() {
|
||||
return invocation;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#
|
||||
* getObjectParameters()
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Object> getObjectParameters() {
|
||||
return Arrays.asList(objectParameters).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a proxy of the given type, backed by an {@link EmptyTargetSource} to simply
|
||||
* drop method invocations but equips it with an
|
||||
* {@link InvocationRecordingMethodInterceptor}. The interceptor records the last
|
||||
* invocation and returns a proxy of the return type that also implements
|
||||
* {@link LastInvocationAware} so that the last method invocation can be inspected.
|
||||
* Parameters passed to the subsequent method invocation are generally neglected
|
||||
* except the ones that might be mapped into the URI translation eventually, e.g.
|
||||
* {@linke PathVariable} in the case of Spring MVC.
|
||||
*
|
||||
* @param type 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> type, Object... parameters) {
|
||||
|
||||
Assert.notNull(type, "Given type must not be null!");
|
||||
|
||||
InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(
|
||||
parameters);
|
||||
return getProxyWithInterceptor(type, interceptor);
|
||||
}
|
||||
|
||||
static RecordedMethodInvocation getInvocation(Method method, Object[] parameters) {
|
||||
return new SimpleRecordedMethodInvocation(method, parameters);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getProxyWithInterceptor(Class<?> type,
|
||||
InvocationRecordingMethodInterceptor interceptor) {
|
||||
|
||||
if (type.isInterface()) {
|
||||
|
||||
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
|
||||
factory.addInterface(type);
|
||||
factory.addInterface(LastInvocationAware.class);
|
||||
factory.addAdvice(interceptor);
|
||||
|
||||
return (T) factory.getProxy();
|
||||
}
|
||||
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(type);
|
||||
enhancer.setInterfaces(new Class<?>[] { LastInvocationAware.class });
|
||||
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
|
||||
|
||||
Factory factory = (Factory) OBJENESIS.newInstance(enhancer.createClass());
|
||||
factory.setCallbacks(new Callback[] { interceptor });
|
||||
return (T) factory;
|
||||
}
|
||||
|
||||
public interface RecordedMethodInvocation {
|
||||
|
||||
Object[] getArguments();
|
||||
|
||||
Method getMethod();
|
||||
}
|
||||
|
||||
static class SimpleRecordedMethodInvocation implements RecordedMethodInvocation {
|
||||
|
||||
private final Method method;
|
||||
|
||||
private final Object[] arguments;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SimpleRecordedMethodInvocation} for the given
|
||||
* {@link Method} and arguments.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param arguments must not be {@literal null}.
|
||||
*/
|
||||
private SimpleRecordedMethodInvocation(Method method, Object[] arguments) {
|
||||
|
||||
Assert.notNull(method, "Method must not be null!");
|
||||
Assert.notNull(arguments, "Arguments must not be null!");
|
||||
|
||||
this.arguments = arguments;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2013 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.web.servlet.hypermedia;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* SPI callback to enhance a {@link UriComponentsBuilder} when referring to a method
|
||||
* through a dummy method invocation. Will usually be implemented in implementations of
|
||||
* {@link HandlerMethodArgumentResolver} as they represent exactly the same functionality
|
||||
* inverted.
|
||||
*
|
||||
* @see MvcUriComponentsBuilderFactory#from(Object)
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public interface UriComponentsContributor {
|
||||
|
||||
/**
|
||||
* Returns whether the {@link UriComponentsBuilder} supports the given
|
||||
* {@link MethodParameter}.
|
||||
*
|
||||
* @param parameter will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
boolean supportsParameter(MethodParameter parameter);
|
||||
|
||||
/**
|
||||
* Enhance the given {@link UriComponentsBuilder} with the given value.
|
||||
*
|
||||
* @param builder will never be {@literal null}.
|
||||
* @param parameter will never be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value);
|
||||
}
|
||||
Reference in New Issue
Block a user