Spring 4.3.0.RELEASE does not inject Model and Request to #Controller - spring

The code below was working fine with Spring 4.2.6.REALESE. Now, after migration something was changed and I started to get nulls in my #Controller Is it a bug?
#Controller
#RequestMapping("/client")
public class ClientController {
#RequestMapping(value = "test", method = RequestMethod.GET)
public String test(Model model) {
// model is null at this point
return "client/test";
}
}

I share your experience with the model object being null. The code below worked well prior 4.3.0-RELEASE. Maybe we are missing some new configuration?
#Controller
#RequestMapping("/admin/pricelists")
public class PriceListsController {
#RequestMapping(value = "/")
public String index(final Model model) {
// Model is null in 4.3.0-RELEASE.
...
}
}

Short answer: 4.3.0.RELEASE requires Servlet 3.0+
Longer proof:
Model is null because a method to find a proper ArgumentResolver for org.springframework.ui.Model finds the first one on the list which is org.springframework.web.method.annotation.RequestParamMethodArgumentResolver
instead of org.springframework.web.method.annotation.ModelMethodProcessor
That is the invalid part of code form HandlerMethodArgumentResolverComposite
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
parameter.getGenericParameterType() + "]");
}
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
RequestParamMethodArgumentResolver is here first on the list and its
RequestParamMethodArgumentResolver.supportsParameter(MethodParameter parameter) returns true because of this line:
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
and then in MultipartResolutionDelegate:
public static boolean isMultipartArgument(MethodParameter parameter) {
Class<?> paramType = parameter.getNestedParameterType();
return (MultipartFile.class == paramType || isMultipartFileCollection(parameter) ||
isMultipartFileArray(parameter) || servletPartClass == paramType ||
isPartCollection(parameter) || isPartArray(parameter));
}
isPartCollection returns true because of this strange comparison where both sides of the equation are nulls !!
private static boolean isPartCollection(MethodParameter methodParam) {
return (servletPartClass == getCollectionParameterType(methodParam));
}
servletPartClass is null because of this
private static Class<?> servletPartClass = null;
static {
try {
servletPartClass = ClassUtils.forName("javax.servlet.http.Part",
MultipartResolutionDelegate.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 3.0 javax.servlet.http.Part type not available -
// Part references simply not supported then.
}
}

This is fixed in 4.3.1
Spring Framework / SPR-14358
Failure to resolve #RequestMapping method arguments in Servlet 2.5 environments

Related

SpringBoot rest validation does not fail on wrong enum input

I have a SpringBoot rest POST endpoint where in body I POST an enum value. This call does not fail on wrong value input. I would like the rest call to fail instead of returning null for a value which can not be deserialised.
I have tried with the following custom ObjectMapper configuration, but any wrong input i put as enum deserialises to null.
#Bean
#Primary
public ObjectMapper customJsonObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
ObjectMapper objectMapper = builder.build();
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, false);
SimpleModule module = new SimpleModule();
objectMapper.registerModule(module);
return objectMapper;
}
For example if i have the enum:
public enum CouponOddType {
BACK("back"),
LAY("lay");
private String value;
CouponOddType(String value) {
this.value = value;
}
#Override
#JsonValue
public String toString() {
return String.valueOf(value);
}
#JsonCreator
public static CouponOddType fromValue(String text) {
for (CouponOddType b : CouponOddType.values()) {
if (String.valueOf(b.value).equals(text)) {
return b;
}
}
return null;
}
}
the dto where the request is mapped to:
#ApiModel(description = "Filter used to query coupons. Filter properties are combined with AND operator")
#Validated
#javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2020-07-07T13:12:58.487+02:00[Europe/Ljubljana]")
public class CouponQueryFilter {
#JsonProperty("statuses")
#Valid
private List<CouponStatus> statuses = null;
#JsonProperty("oddTypes")
#Valid
private List<CouponOddType> oddTypes = null;
public CouponQueryFilter statuses(List<CouponStatus> statuses) {
this.statuses = statuses;
return this;
}
public CouponQueryFilter addStatusesItem(CouponStatus statusesItem) {
if (this.statuses == null) {
this.statuses = new ArrayList<>();
}
this.statuses.add(statusesItem);
return this;
}
/**
* Get statuses
* #return statuses
**/
#ApiModelProperty(value = "")
#Valid
public List<CouponStatus> getStatuses() {
return statuses;
}
public void setStatuses(List<CouponStatus> statuses) {
this.statuses = statuses;
}
public CouponQueryFilter oddTypes(List<CouponOddType> oddTypes) {
this.oddTypes = oddTypes;
return this;
}
public CouponQueryFilter addOddTypesItem(CouponOddType oddTypesItem) {
if (this.oddTypes == null) {
this.oddTypes = new ArrayList<>();
}
this.oddTypes.add(oddTypesItem);
return this;
}
/**
* Get oddTypes
* #return oddTypes
**/
#ApiModelProperty(value = "")
#Valid
public List<CouponOddType> getOddTypes() {
return oddTypes;
}
public void setOddTypes(List<CouponOddType> oddTypes) {
this.oddTypes = oddTypes;
}
}
and in the POST request i put the enum value in json array:
{
"statuses": [
"wrong value"
],
"oddTypes": [
"wrong value"
]
}
I would like that this type of request results in an HTTP 404 error, instead of deserialising into null.
In this case, Jackson is actually behaving as intended and there is an issue in your deserialization logic. Ultimately, you want bad enum values to throw an error and return that error to the user. This is infact the default behaviour of spring and jackso, and will result in a HTTP 400 BAD REQUEST error. IMO This is the appropriate error to return (not 404) since the user has supplied bad input.
Unless there is a specific reason for you to implement a custom #JsonCreator in your enum class, I would get rid of it. What is happening here is that Jackson is being told to use this method for converting a string into an enum value instead from the defualt method. When a text is passed that is not a valid value of your enum, you are returning null which results into that values deserializing to null.
A quick fix, would be to delete the JsonCreator and allow jackson to use its default behaviour for handling enums. The extra properties methods you have added are unnecessary in most cases
ublic enum CouponOddType {
BACK("back"),
LAY("lay");
private String value;
CouponOddType(String value) {
this.value = value;
}
}
If you need to perserve the creator for some other reason, then you will need to add business logic to determine if any of the enum values in the arrays evaluated to null.
private Response someSpringRestEndpoint(#RequestBody CouponQueryFilter filter){
if (filter.getOddTypes() != null && filter.getOddTypes().contains(null){
throw new CustomException()
}
if (filter.getStatuses() != null && filter.getStatuses().contains(null){
throw new CustomException()
}
//... other business logic
}

Why does Spring #Cacheable not pass the annotated method's result type to its deserializer?

This is sample code with Kotlin.
#Configuration
#Bean("cacheManager1hour")
fun cacheManager1hour(#Qualifier("cacheConfig") cacheConfiguration: RedisCacheConfiguration, redisConnectionFactory: RedisConnectionFactory): CacheManager {
cacheConfiguration.entryTtl(Duration.ofSeconds(60 * 60))
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build()
}
#Bean("cacheConfig")
fun cacheConfig(objectMapper:ObjectMapper): RedisCacheConfiguration {
return RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith { cacheName -> "yaya:$cacheName:" }
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
}
#RestController
#Cacheable(value = "book", key = "#root.methodName", cacheManager = "cacheManager1hour")
fun getBook(): Book {
return Book()
}
class Book {
var asdasd:String? = "TEST"
var expires_in = 123
}
The GenericJackson2JsonRedisSerializer cannot process the "kotlin class" and we need to add '#class as property' to the Redis cache entry.
Anyway, why do we need the #class? The Spring context is aware of the result's type, why doesn't it get passed? We would have two benefits:
less memory
easy for the Serializer, i.e. objectMapper.readValue(str, T)
Annotated Spring code for illustration
// org.springframework.cache.interceptor.CacheAspectSupport
#Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
// --> maybe we can pass the context.method.returnType to doGet
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" +
cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
// org.springframework.data.redis.cache.RedisCache
#Override
protected Object lookup(Object key) {
// -> there will get the deserialized type can pass to Jackson
byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
if (value == null) {
return null;
}
return deserializeCacheValue(value);
}
Your return type could be:
some abstract class
some interface
In those cases your return type is almost useless to deserialize the object. Encoding the actual class always works .

Pass method argument in Aspect of custom annotation

I'm trying to use something similar to org.springframework.cache.annotation.Cacheable :
Custom annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
}
Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
}
Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(key = "#id")
public Entity getEntity(Long id) {
return new Entity(id);
}
}
My IDE (IntelliJ) doesn't see anything special with the key = "#id" usage in contrast to similar usages for Cacheable where it's shown with different color than plain text. I'm mentioning the IDE part just as a hint in case it helps, it looks like the IDE is aware in advance about these annotations or it just realizes some connection which doesn't exist in my example.
The value in the checkEntity.key is '#id' instead of an expected number.
I tried using ExpressionParser but possibly not in the right way.
The only way to get parameter value inside the checkEntity annotation is by accessing the arguments array which is not what I want because this annotation could be used also in methods with more than one argument.
Any idea?
Adding another simpler way of doing it using Spring Expression. Refer below:
Your Annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String keyPath() default "";
}
Your Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(keyPath = "[0]")
public Entity getEntity(Long id) {
return new Entity(id);
}
#CheckEntity(keyPath = "[1].otherId")
public Entity methodWithMoreThanOneArguments(String message, CustomClassForExample object) {
return new Entity(object.otherId);
}
}
class CustomClassForExample {
Long otherId;
}
Your Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
Object[] args = joinPoint.getArgs();
ExpressionParser elParser = new SpelExpressionParser();
Expression expression = elParser.parseExpression(checkEntity.keyPath());
Long id = (Long) expression.getValue(args);
// Do whatever you want to do with this id
// This works for both the service methods provided above and can be re-used for any number of similar methods
}
}
PS: I am adding this solution because I feel this is a simpler/clearner approach as compared to other answers and this might be helpful for someone.
Thanks to #StéphaneNicoll I managed to create a first version of a working solution:
The Aspect
#Component
#Aspect
public class CheckEntityAspect {
protected final Log logger = LogFactory.getLog(getClass());
private ExpressionEvaluator<Long> evaluator = new ExpressionEvaluator<>();
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntity checkEntity) {
Long result = getValue(joinPoint, checkEntity.key());
logger.info("result: " + result);
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
private Long getValue(JoinPoint joinPoint, String condition) {
return getValue(joinPoint.getTarget(), joinPoint.getArgs(),
joinPoint.getTarget().getClass(),
((MethodSignature) joinPoint.getSignature()).getMethod(), condition);
}
private Long getValue(Object object, Object[] args, Class clazz, Method method, String condition) {
if (args == null) {
return null;
}
EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
return evaluator.condition(condition, methodKey, evaluationContext, Long.class);
}
}
The Expression Evaluator
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
/**
* Create the suitable {#link EvaluationContext} for the specified event handling
* on the specified method.
*/
public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
Method targetMethod = getTargetMethod(targetClass, method);
ExpressionRootObject root = new ExpressionRootObject(object, args);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
/**
* Specify if the condition defined by the specified expression matches.
*/
public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod == null) {
targetMethod = method;
}
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
}
The Root Object
public class ExpressionRootObject {
private final Object object;
private final Object[] args;
public ExpressionRootObject(Object object, Object[] args) {
this.object = object;
this.args = args;
}
public Object getObject() {
return object;
}
public Object[] getArgs() {
return args;
}
}
I think you probably misunderstand what the framework is supposed to do for you vs. what you have to do.
SpEL support has no way to be triggered automagically so that you can access the actual (resolved) value instead of the expression itself. Why? Because there is a context and as a developer you have to provide this context.
The support in Intellij is the same thing. Currently Jetbrains devs track the places where SpEL is used and mark them for SpEL support. We don't have any way to conduct the fact that the value is an actual SpEL expression (this is a raw java.lang.String on the annotation type after all).
As of 4.2, we have extracted some of the utilities that the cache abstraction uses internally. You may want to benefit from that stuff (typically CachedExpressionEvaluator and MethodBasedEvaluationContext).
The new #EventListener is using that stuff so you have more code you can look at as examples for the thing you're trying to do: EventExpressionEvaluator.
In summary, your custom interceptor needs to do something based on the #id value. This code snippet is an example of such processing and it does not depend on the cache abstraction at all.
Spring uses internally an ExpressionEvaluator to evaluate the Spring Expression Language in the key parameter (see CacheAspectSupport)
If you want to emulate the same behaviour, have a look at how CacheAspectSupport is doing it. Here is an snippet of the code:
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
/**
* Compute the key for the given caching operation.
* #return the generated key, or {#code null} if none can be generated
*/
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext(
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result);
}
I don't know which IDE you are using, but it must deal with the #Cacheable annotation in a different way than with the others in order to highlight the params.
Your annotation can be used with methods with more than 1 parameter, but that doesn't mean you can't use the arguments array. Here's a sollution:
First we have to find the index of the "id" parameter. This you can do like so:
private Integer getParameterIdx(ProceedingJoinPoint joinPoint, String paramName) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
String parameterName = parameterNames[i];
if (paramName.equals(parameterName)) {
return i;
}
}
return -1;
}
where "paramName" = your "id" param
Next you can get the actual id value from the arguments like so:
Integer parameterIdx = getParameterIdx(joinPoint, "id");
Long id = joinPoint.getArgs()[parameterIdx];
Of course this assumes that you always name that parameter "id". One fix there could be to allow to specify the parameter name on the annotation, something like
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
String paramName() default "id";
}

Converter from #PathVariable DomainObject to String? (using ControllerLinkBuilder.methodOn)

I'm trying to call Spring's ControllerLinkBuilder.methodOn() with a non-String type, which always fails. And I don't know which kind of Converter to use and where to register it.
Here's my Controller:
#RestController
#RequestMapping("/companies")
class CompanyController {
#RequestMapping(value="/{c}", method=RequestMethod.GET)
void getIt(#PathVariable Company c) {
System.out.println(c);
Link link = linkTo(methodOn(getClass()).getIt(c));
}
}
The System.out.println(c) works well. My Company Domain object get's fetched from DB. (I'm using DomainClassConverter)
But the other way doesn't work: ConverterNotFoundException: No converter found capable of converting from type #PathVariable Company to type String
Do I just need a Converter<Company, String>? And where should I register it? I tried something within the addFormatters(FormatterRegistry registry) method of WebMvcConfigurationSupport, but it did just display the same error. But after all I'm not sure what exactly I tried...
I had the same issue, it is a bug. If you don't want to do copy & paste on every controller you can try something like this in your WebMvcConfigurationSupport. It works for me.
#Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
try {
Class<?> clazz = Class.forName("org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor$BoundMethodParameter");
Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
field.setAccessible(true);
DefaultFormattingConversionService service = (DefaultFormattingConversionService) field.get(null);
for (Converter<?, ?> converter : beanFactory.getBeansOfType(Converter.class).values()) {
service.addConverter(converter);
}
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
Found a "solution". It requires a lot copy & paste from Spring's classes, but at least it works!
Basically I had to copy org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor and change two lines:
class AnnotatedParametersParameterAccessor {
...
static class BoundMethodParameter {
// OLD: (with this one you can't call addConverter())
// private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
// NEW:
private static final FormattingConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
...
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
...
// ADD:
CONVERSION_SERVICE.addConverter(new MyNewConverter());
}
...
}
This class get's used by ControllerLinkBuilderFactory. So I had to copy & paste that, too.
And this one get's used by ControllerLinkBuilder. Also copy & paste.
My Converter just does myDomainObject.getId().toString():
public class MyNewConverter implements Converter<Company, String> {
#Override
public String convert(Company source) {
return source.getId().toString();
}
}
Now you can use the copy&pasted ControllerLinkBuilder inside the controller and it works as expected!
I developed a framework to render links in spring hateoas and it supports annotated parameters (#PathVariable and #RequestParam) and arbitrary parameters types.
In order to render these arbitrary types you have to create a spring bean that implements com.github.osvaldopina.linkbuilder.argumentresolver.ArgumentResolver interface.
The interface has 3 methods:
public boolean resolveFor(MethodParameter methodParameter)
Is used to determine if the ArgumentResolver can be used to deal with the methodParameter. For example:
public boolean resolveFor(MethodParameter methodParameter) {
return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}
Defines that this ArgumentResover will be used for UserDefinedType.
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter)
Is used to include in the uriTemplate associated with the method the proper template parts. For example:
#Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
uriTemplateAugmenter.addToQuery("value1");
uriTemplateAugmenter.addToQuery("value2");
}
adds 2 query parameters (value1 and value2) to the uri template.
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames)
Sets in the template the values for the template variables. For example:
#Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
template.set("value1", ((UserDefinedType) parameter).getValue1());
}
else {
template.set("value1", "null-value");
}
if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
template.set("value2", ((UserDefinedType) parameter).getValue2());
}
else {
template.set("value2", "null-value");
}
}
gets the UserDefinedType instance and use it to sets the templates variables value1 and value2 defined in augmentTemplate method.
A ArgumentResolver complete example would be:
#Component
public class UserDefinedTypeArgumentResolver implements ArgumentResolver {
#Override
public boolean resolveFor(MethodParameter methodParameter) {
return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}
#Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
uriTemplateAugmenter.addToQuery("value1");
uriTemplateAugmenter.addToQuery("value2");
}
#Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
template.set("value1", ((UserDefinedType) parameter).getValue1());
}
else {
template.set("value1", "null-value");
}
if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
template.set("value2", ((UserDefinedType) parameter).getValue2());
}
else {
template.set("value2", "null-value");
}
}
}
and for the following link builder:
linksBuilder.link()
.withRel("user-type")
.fromControllerCall(RootRestController.class)
.queryParameterForUserDefinedType(new UserDefinedType("v1", "v2"));
to the following method:
#RequestMapping("/user-defined-type")
#EnableSelfFromCurrentCall
public void queryParameterForUserDefinedType(UserDefinedType userDefinedType) {
}
would generate the following link:
{
...
"_links": {
"user-type": {
"href": "http://localhost:8080/user-defined-type?value1=v1&value2=v2"
}
...
}
}
full config in spring boot. same as Franco Gotusso's answer just provide more detail.
```
/**
* This configuration file is to fix bug of Spring Hateoas.
* please check https://github.com/spring-projects/spring-hateoas/issues/118.
*/
#Component
public class MvcConfig extends WebMvcConfigurerAdapter {
#Autowired
private ApplicationContext applicationContext;
#Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
try {
Class<?> clazz = Class.forName("org.springframework.hateoas.mvc."
+ "AnnotatedParametersParameterAccessor$BoundMethodParameter");
Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
field.setAccessible(true);
DefaultFormattingConversionService service =
(DefaultFormattingConversionService) field.get(null);
for (Formatter<?> formatter : applicationContext
.getBeansOfType(Formatter.class).values()) {
service.addFormatter(formatter);
}
for (Converter<?, ?> converter : applicationContext
.getBeansOfType(Converter.class).values()) {
service.addConverter(converter);
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
```

Spring formatting with Formatting from String to String and spring:eval does not work

I am running a spring 4.0 mvc webapp and want to make use of the formater SPI with a IbanFormatter:
public class IbanFormatter implements Formatter<String>
{
#Override
public String print ( String iban, Locale locale )
{
StringBuilder sb = new StringBuilder(iban);
for (int i = 4; i < sb.length(); i += 5)
{
sb.insert(i, ' ');
}
return sb.toString();
}
#Override
public String parse ( String iban, Locale locale ) throws ParseException
{
return iban.replaceAll("\\s+", "");
}
}
Additionally I have a AnnotationFormaterFactory:
public final class IbanFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<IbanFormat>
{
#Override
public Printer<?> getPrinter ( IbanFormat annotation, Class<?> fieldType )
{
return new IbanFormatter();
}
#Override
public Parser<?> getParser ( IbanFormat annotation, Class<?> fieldType )
{
return new IbanFormatter();
}
#Override
public Set<Class<?>> getFieldTypes ( )
{
HashSet<Class<?>> hashSet = new HashSet<Class<?>>();
hashSet.add(String.class);
return hashSet;
}
}
And an annotation of course:
#Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface IbanFormat
{}
With an Object annotated like this:
public class DirectDebitDTO
{
#IbanFormat
private String iban;
}
I registerd this formatter like this:
#Override
protected void addFormatters ( FormatterRegistry registry )
{
registry.addFormatterForFieldAnnotation(new IsoFallbackJodaDateTimeFormatAnnotationFormatterFactory());
registry.addFormatterForFieldAnnotation(new IbanFormatAnnotationFormatterFactory());
}
My DateFormatter seen above works like a charm.
In my jsp I want to show the formatted iban:
IBAN: <spring:eval expression="directDebit.iban" />
It does not work. It just shows unformatted iban. I debugged it and found the reason in line 65 in ExpressionUtils
public static <T> T convertTypedValue(EvaluationContext context, TypedValue typedValue, Class<T> targetType) {
Object value = typedValue.getValue();
if ((targetType == null) || (value != null && ClassUtils.isAssignableValue(targetType, value))) {
return (T) value;
}
if (context != null) {
return (T) context.getTypeConverter().convertValue(value, typedValue.getTypeDescriptor(), TypeDescriptor.valueOf(targetType));
}
throw new EvaluationException("Cannot convert value '" + value + "' to type '" + targetType.getName() + "'");
}
The first if statement is triggered because both values are Strings. No conversion and thus no formatting takes place. So it wouldn't even help to add a Converter
Of course I could change the type of iban from String to an Iban.class just to get type conversion.
For me it looks like a Bug, but havn't found anything about this.
Is this intended behaviour or a bug?

Resources