Another thing we encounter regularly, is that we want to issue a
POST
request to a URL to execute an action on a domain object. As we prefer
not to repeat the code to retrieve the domain object, we’ve created an
annotation @PathEntity
so we can do this:
1
2
3
4
5
|
@RequestMapping(value = "/", method = RequestMethod.POST, params = "action=approve")
@ResponseBody
public Invoice approveInvoice(@PathEntity("invoiceId") Invoice invoice) {
// ...
}
|
Granted, we could also do this by overriding the Spring
ConversionService, but as we’ll see in the next example this approach
has it’s advantages. The code for the
@PathEntity
annotation and the PathEntityMethodArgumentResolver
to handle it is this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package com.jay.demo;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate a method parameter should be bound to a URL template, and retrieved as a domain entity.
* Used for annotated methods.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathEntity {
/**
* The name of the URL template that contains the
* .
*
* @return the name of the URL template that is the entity id
*/
String value();
}
|
And this is the class to resolve parameters annotated with
@PathEntity
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
package com.jay.demo;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import nl.t42.generic.EntityNotFoundException;
import nl.t42.generic.DomainEntity;
/**
* A that converts a HTTP request body to a , provided that
* the handler method argument is annotated with .
*/
public class PathEntityMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* A conversion service to convert primary key values.
*/
@Autowired
private ConversionService conversionService;
/**
* The entity manager used to retrieve the entities.
*/
@Autowired
private EntityManager entityManager;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(PathEntity.class) && isADomainEntity(parameter);
}
/**
* Determine if a method parameter is a DomainEntity.
*
* @param parameter a method parameter
* @return if the type of the parameter is (a subclass of) DomainEntity
*/
protected boolean isADomainEntity(MethodParameter parameter) {
return DomainEntity.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public DomainEntity resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String primaryKeyName = getPrimaryKeyName(parameter);
Long entityId = findPrimaryKeyValue(webRequest, primaryKeyName);
return findEntity(parameter, entityId);
}
/**
* Get the nam of the URL template that holds the primary key.
*
* @param parameter the method parameter
* @return the name of a URL template
*/
protected String getPrimaryKeyName(MethodParameter parameter) {
return parameter.getParameterAnnotation(PathEntity.class).value();
}
private Long findPrimaryKeyValue(NativeWebRequest webRequest, String primaryKeyName) {
Map<String, String> pathVariables = getPathVariables(webRequest);
String primaryKeyValue = pathVariables.get(primaryKeyName);
if (primaryKeyValue == null) {
throw new IllegalStateException(String.format("The path variable %s cannot be resolved.", primaryKeyName));
}
return conversionService.convert(primaryKeyValue, Long.class);
}
private Map<String, String> getPathVariables(NativeWebRequest webRequest) {
HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
return (Map<String, String>) httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private DomainEntity findEntity(MethodParameter parameter, Long entityId) {
Class<? extends DomainEntity> entityType = (Class<? extends DomainEntity>) parameter.getParameterType();
DomainEntity entity = entityManager.find(entityType, entityId);
if (entity == null) {
throw new EntityNotFoundException(
String.format("Cannot find a %s with id=%d", entityType.getSimpleName(), entityId));
}
return entity;
}
}
|
Registering this
MethodHandlerArgumentResolver
is as usual:
1
2
3
4
5
|
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="nl.t42.spring31.PathEntityMethodArgumentResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
|
With these two classes in place, we can identify our domain objects
with our REST resources (URL’s). When the controller method is invoked,
we have an instance of the domain entity the URL represents. Note that
outside a transaction, this entity is a detached entity (this is
prescribed by the JPA specification). So we still need to call
EntityManager.merge(Object)
to persist any changes.
No comments:
Post a Comment