/*
* Copyright 2012-2016 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 lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriTemplate;
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
class AnnotatedParametersParameterAccessor {
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<Method, MethodParameters>(
16, ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
/**
* Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(MethodInvocation invocation) {
Assert.notNull(invocation, "MethodInvocation must not be null!");
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<BoundMethodParameter>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
Object value = arguments[parameter.getParameterIndex()];
Object verifiedValue = verifyParameterValue(parameter, value);
if (verifiedValue != null) {
result.add(createParameter(parameter, verifiedValue, attribute));
}
}
return result;
}
/**
* Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and
* {@link AnnotationAttribute}.
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute must not be {@literal null}.
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}
/**
* Callback to verifiy the parameter values given for a dummy invocation. Default implementation rejects
* {@literal null} values as they indicate an invalid dummy call.
*
* @param parameter will never be {@literal null}.
* @param value could be {@literal null}.
* @return the verified value.
*/
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
return value;
}
/**
* Returns the {@link MethodParameters} for the given {@link Method}.
*
* @param method
* @return
*/
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
MethodParameters methodParameters = METHOD_PARAMETERS_CACHE.get(method);
if (methodParameters != null) {
return methodParameters;
}
methodParameters = new MethodParameters(method);
METHOD_PARAMETERS_CACHE.put(method, methodParameters);
return methodParameters;
}
/**
* 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);
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default conversion service setup.
*
* @return
*/
public String asString() {
return value == null ? null
: (String) CONVERSION_SERVICE.convert(value, parameterTypeDecsriptor, STRING_DESCRIPTOR);
}
/**
* Returns whether the given parameter is a required one. Defaults to {@literal true}.
*
* @return
*/
public boolean isRequired() {
return true;
}
}
}