Spring HATEOAS Resource Assembler and Resource Links with many variables - java

I'm working on REST API with Spring HATEOAS and the Spring stack, and i have some problems with links into resources.
Here is my code :
the Controller :
#RestController
#RequestMapping("/apporteurs/{idInt}/ribs")
public class RibController {
#Autowired
private RibResourceAssembler ribResourceAssembler;
#Autowired
private RibRepository ribRepository;
/**
* Methode GET permettant d'obtenir un Rib par son ID
*
* #param idRib ID du Rib
* #return RibResource
*/
#RequestMapping(value = "/{idRib}", method = RequestMethod.GET)
#ResponseBody
public RibResource getRibById(#PathVariable Long idInt, #PathVariable Long idRib) {
CurrentUserUtils.checkAuthorizationByApporteur(idInt);
return ribResourceAssembler.toResource(ribRepository.getRibById(idRib));
}
}
The Assembler :
#Component
public class RibResourceAssembler extends ResourceAssemblerSupport<Rib, RibResource> {
public static final long TMP_IDS = 1234L;
#Autowired
private RibResourceMapper ribResourceMapper;
public RibResourceAssembler() {
super(RibController.class, RibResource.class);
}
#Override
public RibResource toResource(Rib rib) {
return createResourceWithId(rib.getId(), rib);
}
/**
* TODO : mettre le lien vers l'editique Mandat
*
* #param rib Rib à instancier en Resource.
* #return RibResource
*/
#Override
protected RibResource instantiateResource(Rib rib) {
RibResource ribResource = ribResourceMapper.fromRib(rib, rib.getLastMandat());
ribResource.removeLinks();
CustomUserDetails user = CurrentUserUtils.getCurrentUser();
UriComponentsBuilder uriBuilderMandat = linkTo(RibController.class).toUriComponentsBuilder();
String uri = uriBuilderMandat.path("/{idRib}/mandats/{idMandat}").buildAndExpand(user.getIdInt(), rib.getId(), TMP_IDS).toUriString();
Link linkEditiqueMandat = new Link(uri).withRel("editiqueMandat");
UriComponentsBuilder uriBuilderRib = linkTo(RibController.class).toUriComponentsBuilder();
String uriSelf = uriBuilderRib.path("/{idRib}").buildAndExpand(user.getIdInt(), rib.getId()).toUriString();
Link linkUriSelf = new Link(uriSelf).withSelfRel();
ribResource.add(linkEditiqueMandat);
ribResource.add(linkUriSelf);
return ribResource;
}
}
The Resource :
public class RibResource extends ResourceSupport {
private Long idRib;
private String rum;
private String iban;
private String libelle;
private String dateFin;
private String dateCreation;
private String dateModification;
private String codeOperateurCreation;
private String dateRegulationMandat;
private boolean actif;
private boolean reactivable;
private CodeValueResource modeReglement;
/*Gzetter & setters, etc*/
}
As you can see, my controller have some parameters in the URI : idInt and idRib.
So for make a SelfLink, i have to know this parameters for make something like "/apporteurs/1234/ribs/1234", but i think the Assembler want only one parameter, and a "simple" URI.
I have this stack trace :
2014-11-25 12:02:09.365 ERROR 20860 --- [nio-9080-exec-1] w.s.m.m.a.ResponseEntityExceptionHandler : Not enough variable values available to expand 'idInt'
So i'm looking for an elegant solution to do this, because i didn't find anything ^^
I saw something with ResourceProcessor, but i'm not using Spring Data Rest.
Can you help me ? Thank you in advance ;)
EDIT :
The result should be :
_links": {
"editiqueMandat": {
"href": "http://localhost:9080/apporteurs/6797/mandats/5822"
},
"self": {
"href": "http://localhost:9080/apporteurs/6797/ribs/1234"
}
}

#Override
public RibResource toResource(Rib rib) {
return createResourceWithId(rib.getId(), rib);
}
createResourceWithId() internally creates a self link based on the controller's URL. In your case that contains the placeholder {idInt} so you would have to provide a parameter for that:
CustomUserDetails user = CurrentUserUtils.getCurrentUser();
return createResourceWithId(rib.getId(), rib, user.getIdInt());
The better choice would be not to call createResourceWithId() at all. Just move everything you now have in instantiateResource() to toResource().

Related

JUnit Superclass mocked method still called

I'm trying to write my Unit Tests for a class which extends another. I'm calling a superclass method in another child class method :
The child class
#RestController
#RequestMapping(PUBLIC_BASE_URL)
public class RequeteSASRestController extends BaseController {
private static final Logger LOGGER = LogManager.getLogger();
private final RequeteSASService requeteSASService;
#Inject
public RequeteSASRestController(
final RequeteSASService pRequeteSASJdbcService) {
requeteSASService = pRequeteSASJdbcService;
}
#ApiOperation(value = "Lecture d'une liste des requetes SAS", responseContainer = "List")
#RequestMapping(value = "/statistiques/requeteSAS", method = RequestMethod.GET)
public ResponseEntity<SimpleServiceResponse<List<RequeteSAS>>> readListeRequeteSas() {
LOGGER.debug(" readListeRequeteSas() ->");
String remoteUser=super.getCurrentUserLogin();
return this.ok(requeteSASService.readListeRequeteSas(remoteUser));
}
The superclass
public class BaseController {
public String getCurrentUserLogin() {
//Some things
}
And finally here's my test class :
public class RequeteSASRestTest extends Mockito{
private RequeteSAS requeteSASJdbc1;
private RequeteSAS requeteSASJdbc2;
private RequeteSAS requeteSASJdbc1suppr;
/** Service RequeteSAS */
private RequeteSASService mockRequeteService;
/** Rest Controller. */
private RequeteSASRestController restController;
// Before : mock des services
#Before
public void beforeTests() throws FileNotFoundException {
clearSecurityContext();
mockRequeteService = mock(RequeteSASService.class);
restController = Mockito.spy(new RequeteSASRestController(mockRequeteService));
requeteSASJdbc1 = RequeteSASTestUtil.createRequeteSAS(//instance constructor);
requeteSASJdbc2 = RequeteSASTestUtil.createRequeteSAS(//instance constructor);
}
#Test
public void testGetRequetes() {
String mockRemoteUser = "STATT-00016-0102035";
List<RequeteSAS> listReqTest = new ArrayList<RequeteSAS>();
listReqTest.add(requeteSASJdbc1);
listReqTest.add(requeteSASJdbc2);
Mockito.doReturn(listReqTest).when(mockRequeteService).readListeRequeteSas(mockRemoteUser);
Mockito.doReturn(mockRemoteUser).when((BaseController)restController).getCurrentUserLogin();
ResponseEntity<SimpleServiceResponse<List<RequeteSAS>>> repReq = restController.readListeRequeteSas();
List<RequeteSAS> listReq = repReq.getBody().getValue();
//Assert List
}
As I said, with this, the method "getCurrentUserLogin()" from BaseController is still called, it does not return the mockRemoteUser. I've read many thread on this exact subject i.e. mocking a superclass method, I followed the answer, but still my test fails because "getCurrentUserLogin()" returns null instead of the mocked user.
Does somebody see where the problem is? Thank you.

In Roo 2.0 I want to use JPA Query Method

I plan on writing a query method like
/**
* TODO Auto-generated method documentation
*
* #param entity
* #return EventExecute
*/
#Transactional
#Autowired
public EventExecute save(EventExecute entity) {
String eventKey = entity.getEventKey();
StepDefinitionRepository sdRepository;
List<StepDefinition> stepDefinitions = sdRepository.findByEventKeyAllIgnoreCaseOrderBySequenceAsc(eventKey);
return getEventExecuteRepository().save(entity);
}
I want to lookup the StepDefintions that match an event key.
I tried following the example in the JPA Documentation...
public class SomeClient {
#Autowired
private PersonRepository repository;
public void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
But my sdRepository complains that is it not initialized. I found the getStepDefintionRepository() in the ..ServiceImpl.aj but can't call it.
Is there an example out there?
Ok I figured out my error...here is what works.
#Autowired
private StepDefinitionRepository sdRepository;
/**
* Overridden save method to intercept and process the dynamic steps before saving
*
* #param entity
* #return EventExecute
*/
#Transactional
public EventExecute save(EventExecute entity) {
boolean keepGoing = true;
String eventKey = entity.getEventKey();
Set<NameValuePair> messageVariables = null;
String eventArguments = entity.getEventArguments();
List<NameValuePair> eventVariables = null;
if (!eventArguments.isEmpty()){
eventVariables = _ExtractEventVariables(eventArguments);
}
List<StepDefinition> stepDefinitions = sdRepository.findByEventKeyAllIgnoreCaseOrderBySequenceAsc(eventKey);
for (Iterator<StepDefinition> iterator = stepDefinitions.iterator(); iterator.hasNext();) {

How to detach logic from controllers and put it to some handler using annotations?

In our project we already have some necessary controllers and my task is to make it easier. It should work like this: I just put an annotation under a controller and a handler do all its job. What I already have:
/**
* This annotation marks collector methods
*/
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Collect {
}
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface Collectors {
/**
* An array of subclasses that can load and provide values for
* generating a ModelMap.
*/
Class<?>[] value();
}
I can't find any example how to do it. Controller example:
#Controller
public class TestController {
private final SchoolService schoolService;
private final TeacherService teacherService;
public TestController(SchoolService schoolService, TeacherService teacherService) {
this.schoolService = schoolService;
this.teacherService = teacherService;
}
/**
* Saves the static list of users in model and renders it
* via freemarker template.
*
* #param model
* #return The index view (FTL)
*/
#Collectors(SchoolCollector.class)
#RequestMapping(value = "freemarker/freemarkertest", method = RequestMethod.GET)
public String index(#ModelAttribute("model") ModelMap model) {
List<SchoolDTO> schoolList = new ArrayList<SchoolDTO>();
schoolList = schoolService.findAll();
model.addAttribute("schoolList", schoolList);
return "freemarkertest";
}
/**
* Add a new School
*
* #param schoolDTO
* #return Redirect back to same /freemarkertest page to display school list, if successful
*/
#Collectors(SchoolCollector.class)
#RequestMapping(value = "freemarker/freemarkertest/add", method = RequestMethod.POST)
public String add(#ModelAttribute("schoolDTO") SchoolDTO schoolDTO) {
if(schoolDTO.getName() != null && !schoolDTO.getName().isEmpty() &&
schoolDTO.getEnabled() != null) {
schoolService.save(schoolDTO);
return "redirect:";
} else {
return "redirect:error"; //TODO: create error page
}
}
/**
* Get list of teachers.
*
* #param model
* #param schoolId
* #return The index view (FTL)
*/
#Collectors(SchoolCollector.class)
#RequestMapping(value = "freemarker/teachers/{schoolId}", method = RequestMethod.GET)
public String index(#ModelAttribute("model") ModelMap model, #PathVariable Long schoolId) {
List<TeacherDTO> teachers = teacherService.getAllBySchoolId(schoolId);
model.addAttribute("teachersList", teachers);
model.addAttribute("schoolId", schoolId);
return "teachers";
}
}
What you need are HandlerInterceptorAdapters. Those have to be defined in your SpringMvc-servlet.xml, than those will intercept all your requests.
public class CollectorHandler extends HandlerInterceptorAdapter {
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (handler instanceof HandlerMethod && modelAndView != null) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.getMethodAnnotation(Collectors.class) == null) {
return;
}
/**
* Your logic here
*/
}
}
}
For further reading:
http://www.baeldung.com/spring-mvc-handlerinterceptor
https://www.mkyong.com/spring-mvc/spring-mvc-handler-interceptors-example/
PS: I also want to add, that your code will not work, as you have some "errors" in it... returning Strings instead of objects as an example

Adding applicationproperties in Jhipster

I'm using jhipster microservices app for my development. Based on jhipster documentation for adding application-specific is here:
application-dev.yml and
ApplicationProperties.java
I did this by adding this
application:
mycom:
sgADIpAddress: 172.x.x.xxx
and this my applicationconfig class
package com.mbb.ias.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties specific to JHipster.
*
* <p>
* Properties are configured in the application.yml file.
* </p>
*/
#ConfigurationProperties(prefix = "application", ignoreUnknownFields = false)
public class ApplicationProperties {
private final Mycom mycom= new Mycom();
public Mycom getMycom () {
return mycom;
}
public static class Mycom {
String sgADIpAddress ="";
public String getSgADIpAddress() {
return sgADIpAddress;
}
public void setSgADIpAddress(String sgADIpAddress) {
this.sgADIpAddress = sgADIpAddress;
}
}
}
I've call this by using same like jhipster properties which are
#Inject
private ApplicationProperties applicationProperties;
in classes which are need this AD IP address.
it will throw null value
java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
please help me guys, SIT going to be started, I need to create a profile for maven build like jhipster created
I have the same problem and spent a couple of hours to figure it out...Jhipster has its preconfigured property class that users can customize their own properteis:
Quote from Jhipster website:
Your generated application can also have its own Spring Boot properties. This is highly recommended, as it allows type-safe configuration of the application, as well as auto-completion and documentation within an IDE.
JHipster has generated a ApplicationProperties class in the config package, which is already preconfigured, and it is already documented at the bottom the application.yml, application-dev.yml and application-prod.yml files. All you need to do is code your own specific properties.
In my case, I have set the properties in all yml files.
application:
redis:
host: vnode1
pool:
max-active: 8
max-idle: 8
max-wait: -1
min-idle: 0
port: 6379
In ApplicationProperties class:
#ConfigurationProperties(prefix = "application", ignoreUnknownFields = false)
public class ApplicationProperties {
public final Redis redis = new Redis();
public Redis getRedis() {
return redis;
}
public static class Redis {
private String host = "127.0.0.1";
private int port = 0;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
private Pool pool = new Pool();
public void setPool(Pool pool) {
this.pool = pool;
}
public Pool getPool() {
return this.pool;
}
public static class Pool {
private int maxActive = 8;
private int maxWait = -1;
private int maxIdle = 8;
private int minIdle = 0;
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMaxActive() {
return maxActive;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public int getMaxWait() {
return maxWait;
}
public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}
}
}
}
Then I use it as:
private final ApplicationProperties.Redis redis;
public RedisConfiguration(ApplicationProperties applicationProperties){
redis = applicationProperties.getRedis();
}
For instance use max-wait and host:
this.redis.getPool().getMaxWait();
this.redis.getHost();
refering to this thread
Spring annotation #Inject doesn't work
i remove my new operator for all classes which is calling my applicationproperties.java
#Service
public class ADAuthenticatorService {
private static final Logger log = LoggerFactory.getLogger(ADAuthenticatorService.class);
private final static long DIFF_NET_JAVA_FOR_DATE_AND_TIMES = 11644473600000L;
#Inject
ADContext adContext;
/**
* AD authentication
*
* #param UserID,
* AD User ID
* #param Password,
* AD Password
* #return ADProfile
*/
#Inject
ApplicationProperties applicationProperties;
public ADProfile authenticate(String UserID, String Password) throws Exception {
ADContext context = adContext.getDefaultContext(applicationProperties);
return authenticate(context, UserID, Password);
}
in my ADContext i put #component on the top of my Class name, and added #Sevice annotation on the top of ADAuthenticatorService
then my
#Inject
ApplicationProperties applicationProperties;
is working flawlessly
just posting this answer so any noob like me at outside can benefit this lol

How to customize parameter names when binding Spring MVC command objects?

I have a command object:
public class Job {
private String jobType;
private String location;
}
Which is bound by spring-mvc:
#RequestMapping("/foo")
public String doSomethingWithJob(Job job) {
...
}
Which works fine for http://example.com/foo?jobType=permanent&location=Stockholm. But now I need to make it work for the following url instead:
http://example.com/foo?jt=permanent&loc=Stockholm
Obviously, I don't want to change my command object, because the field names have to remain long (as they are used in the code). How can I customize that? Is there an option to do something like this:
public class Job {
#RequestParam("jt")
private String jobType;
#RequestParam("loc")
private String location;
}
This doesn't work (#RequestParam can't be applied to fields).
The thing I'm thinking about is a custom message converter similar to FormHttpMessageConverter and read a custom annotation on the target object
This solution more concise but requires using RequestMappingHandlerAdapter, which Spring use when <mvc:annotation-driven /> enabled.
Hope it will help somebody.
The idea is to extend ServletRequestDataBinder like this:
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
*/
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
super(target, objectName);
this.renameMapping = renameMapping;
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
Appropriate processor:
/**
* Method processor supports {#link ParamName} parameters renaming
*
* #author jkee
*/
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public RenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty()) return Collections.emptyMap();
return renameMap;
}
}
Annotation:
/**
* Overrides parameter name
* #author jkee
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
Spring config:
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="ru.yandex.metrika.util.params.RenamingProcessor">
<constructor-arg name="annotationNotRequired" value="true"/>
</bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
And finally, usage (like Bozho solution):
public class Job {
#ParamName("job-type")
private String jobType;
#ParamName("loc")
private String location;
}
Here's what I got working:
First, a parameter resolver:
/**
* This resolver handles command objects annotated with #SupportsAnnotationParameterResolution
* that are passed as parameters to controller methods.
*
* It parses #CommandPerameter annotations on command objects to
* populate the Binder with the appropriate values (that is, the filed names
* corresponding to the GET parameters)
*
* In order to achieve this, small pieces of code are copied from spring-mvc
* classes (indicated in-place). The alternative to the copied lines would be to
* have a decorator around the Binder, but that would be more tedious, and still
* some methods would need to be copied.
*
* #author bozho
*
*/
public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor {
/**
* A map caching annotation definitions of command objects (#CommandParameter-to-fieldname mappings)
*/
private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap();
public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) {
return true;
}
return false;
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
bind(servletRequest, servletBinder);
}
#SuppressWarnings("unchecked")
public void bind(ServletRequest request, ServletRequestDataBinder binder) {
Map<String, ?> propertyValues = parsePropertyValues(request, binder);
MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
// two lines copied from ExtendedServletRequestDataBinder
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr));
binder.bind(mpvs);
}
private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) {
// similar to WebUtils.getParametersStartingWith(..) (prefixes not supported)
Map<String, Object> params = Maps.newTreeMap();
Assert.notNull(request, "Request must not be null");
Enumeration<?> paramNames = request.getParameterNames();
Map<String, String> parameterMappings = getParameterMappings(binder);
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = (String) paramNames.nextElement();
String[] values = request.getParameterValues(paramName);
String fieldName = parameterMappings.get(paramName);
// no annotation exists, use the default - the param name=field name
if (fieldName == null) {
fieldName = paramName;
}
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
params.put(fieldName, values);
} else {
params.put(fieldName, values[0]);
}
}
return params;
}
/**
* Gets a mapping between request parameter names and field names.
* If no annotation is specified, no entry is added
* #return
*/
private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) {
Class<?> targetClass = binder.getTarget().getClass();
Map<String, String> map = definitionsCache.get(targetClass);
if (map == null) {
Field[] fields = targetClass.getDeclaredFields();
map = Maps.newHashMapWithExpectedSize(fields.length);
for (Field field : fields) {
CommandParameter annotation = field.getAnnotation(CommandParameter.class);
if (annotation != null && !annotation.value().isEmpty()) {
map.put(annotation.value(), field.getName());
}
}
definitionsCache.putIfAbsent(targetClass, map);
return map;
} else {
return map;
}
}
/**
* Copied from WebDataBinder.
*
* #param multipartFiles
* #param mpvs
*/
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) {
String key = entry.getKey();
List<MultipartFile> values = entry.getValue();
if (values.size() == 1) {
MultipartFile value = values.get(0);
if (!value.isEmpty()) {
mpvs.add(key, value);
}
} else {
mpvs.add(key, values);
}
}
}
}
And then registering the parameter resolver using a post-processor. It should be registered as a <bean>:
/**
* Post-processor to be used if any modifications to the handler adapter need to be made
*
* #author bozho
*
*/
public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessAfterInitialization(Object bean, String arg1)
throws BeansException {
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String arg1)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers();
if (resolvers == null) {
resolvers = Lists.newArrayList();
}
resolvers.add(new AnnotationServletModelAttributeResolver(false));
adapter.setCustomArgumentResolvers(resolvers);
}
return bean;
}
}
In Spring 3.1, ServletRequestDataBinder provides a hook for additional bind values:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
The ExtendedServletRequestDataBinder subclass uses it to add URI template variables as binding values. You could extend it further to make it possible to add command-specific field aliases.
You can override RequestMappingHandlerAdapter.createDataBinderFactory(..) to provide a custom WebDataBinder instance. From a controller's perspective it could look like this:
#InitBinder
public void initBinder(MyWebDataBinder binder) {
binder.addFieldAlias("jobType", "jt");
// ...
}
Thanks the answer of #jkee .
Here is my solution.
First, a custom annotation:
#Inherited
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamName {
/**
* The name of the request parameter to bind to.
*/
String value();
}
A customer DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> paramMappings;
public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) {
super(target, objectName);
this.paramMappings = paramMappings;
}
#Override
protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) {
super.addBindValues(mutablePropertyValues, request);
for (Map.Entry<String, String> entry : paramMappings.entrySet()) {
String paramName = entry.getKey();
String fieldName = entry.getValue();
if (mutablePropertyValues.contains(paramName)) {
mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue());
}
}
}
}
A parameter resolver:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256);
public ParamNameProcessor() {
super(false);
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestParam.class)
&& !BeanUtils.isSimpleProperty(parameter.getParameterType())
&& Arrays.stream(parameter.getParameterType().getDeclaredFields())
.anyMatch(field -> field.getAnnotation(ParamName.class) != null);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Map<String, String> paramMappings = this.getParamMappings(target.getClass());
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
/**
* Get param mappings.
* Cache param mappings in memory.
*
* #param targetClass
* #return {#link Map<String, String>}
*/
private Map<String, String> getParamMappings(Class<?> targetClass) {
if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) {
return PARAM_MAPPINGS_CACHE.get(targetClass);
}
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> paramMappings = new HashMap<>(32);
for (Field field : fields) {
ParamName paramName = field.getAnnotation(ParamName.class);
if (paramName != null && !paramName.value().isEmpty()) {
paramMappings.put(paramName.value(), field.getName());
}
}
PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings);
return paramMappings;
}
}
Finally, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:
#Configuration
public class WebConfig {
/**
* Processor for annotation {#link ParamName}.
*
* #return ParamNameProcessor
*/
#Bean
protected ParamNameProcessor paramNameProcessor() {
return new ParamNameProcessor();
}
/**
* Custom {#link BeanPostProcessor} for adding {#link ParamNameProcessor} into the first of
* {#link RequestMappingHandlerAdapter#argumentResolvers}.
*
* #return BeanPostProcessor
*/
#Bean
public BeanPostProcessor beanPostProcessor() {
return new BeanPostProcessor() {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers());
argumentResolvers.add(0, paramNameProcessor());
adapter.setArgumentResolvers(argumentResolvers);
}
return bean;
}
};
}
}
Param pojo:
#Data
public class Foo {
private Integer id;
#ParamName("first_name")
private String firstName;
#ParamName("last_name")
private String lastName;
#ParamName("created_at")
#DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createdAt;
}
Controller method:
#GetMapping("/foos")
public ResponseEntity<List<Foo>> listFoos(#RequestParam Foo foo, #PageableDefault(sort = "id") Pageable pageable) {
List<Foo> foos = fooService.listFoos(foo, pageable);
return ResponseEntity.ok(foos);
}
That's all.
There is a simple way, you can just add one more setter method, like "setLoc,setJt".
there is no nice built in way to do it, you can only choose which workaround you apply. The difference between handling
#RequestMapping("/foo")
public String doSomethingWithJob(Job job)
and
#RequestMapping("/foo")
public String doSomethingWithJob(String stringjob)
is that job is a bean and stringjob isn't (no surprise so far). The real difference is that beans are resolved with the standard Spring bean resolver mechanism, while string params are resolved by spring MVC that knows the concept of the #RequestParam annotation. To make the long story short there is no way in the standard spring bean resolution (that is using classes like PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) to resolve "jt" to a property called "jobType" or at least I dont know about it.
The workarounds coud be as others suggested to add a custom PropertyEditor or a filter, but I think it just messes up the code. In my opinion the cleanest solution would be to declare a class like this :
public class JobParam extends Job {
public String getJt() {
return super.job;
}
public void setJt(String jt) {
super.job = jt;
}
}
then use that in your controller
#RequestMapping("/foo")
public String doSomethingWithJob(JobParam job) {
...
}
UPDATE :
A slightly simpler option is to not to extend, just add the extra getters, setters to the original class
public class Job {
private String jobType;
private String location;
public String getJt() {
return jobType;
}
public void setJt(String jt) {
jobType = jt;
}
}
You can use Jackson com.fasterxml.jackson.databind.ObjectMapper to convert any map to your DTO/POJO class with nested props. You need annotate your POJOs with #JsonUnwrapped on nested object. Like this:
public class MyRequest {
#JsonUnwrapped
private NestedObject nested;
public NestedObject getNested() {
return nested;
}
}
And than use it like this:
#RequestMapping(method = RequestMethod.GET, value = "/myMethod")
#ResponseBody
public Object myMethod(#RequestParam Map<String, Object> allRequestParams) {
MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
...
}
That's all. A little coding. Also, you can give any names to your props usign #JsonProperty.
I would like to point you to another direction. But I do not know if it works.
I would try to manipulate the binding itself.
It is done by WebDataBinder and will be invoked from HandlerMethodInvoker method Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
I have no deep look in Spring 3.1, but what I have seen, is that this part of Spring has been changed a lot. So it is may possible to exchange the WebDataBinder. In Spring 3.0 it seams not possible without overriding the HandlerMethodInvoker.
Try intercepting request using InterceptorAdaptor, and then using simple checking mechanism decide whether to foward the request to the controller handler. Also wrap HttpServletRequestWrapper around the request, to enable you override the requests getParameter().
This way you can repass the actual parameter name and its value back to the request to be seen by the controller.
Example option:
public class JobInterceptor extends HandlerInterceptorAdapter {
private static final String requestLocations[]={"rt", "jobType"};
private boolean isEmpty(String arg)
{
return (arg !=null && arg.length() > 0);
}
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//Maybe something like this
if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1]))
{
final String value =
!isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request
.getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null;
HttpServletRequest wrapper = new HttpServletRequestWrapper(request)
{
public String getParameter(String name)
{
super.getParameterMap().put("JobType", value);
return super.getParameter(name);
}
};
//Accepted request - Handler should carry on.
return super.preHandle(request, response, handler);
}
//Ignore request if above condition was false
return false;
}
}
Finally wrap the HandlerInterceptorAdaptor around your controller handler as shown below. The SelectedAnnotationHandlerMapping allows you to specify which handler will be interecepted.
<bean id="jobInterceptor" class="mypackage.JobInterceptor"/>
<bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping">
<property name="urls">
<list>
<value>/foo</value>
</list>
</property>
<property name="interceptors">
<list>
<ref bean="jobInterceptor"/>
</list>
</property>
</bean>
EDITED.
There's a little improvement to jkee's answer.
In order to support inheritance you should also analyze parent classes.
/**
* ServletRequestDataBinder which supports fields renaming using {#link ParamName}
*
* #author jkee
* #author Yauhen Parmon
*/
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
//Rename cache
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();
public ParamRenamingProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = Objects.requireNonNull(target).getClass();
if (!replaceMap.containsKey(targetClass)) {
replaceMap.put(targetClass, analyzeClass(targetClass));
}
Map<String, String> mapping = replaceMap.get(targetClass);
ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
.initBinder(paramNameDataBinder);
super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
}
private Map<String, String> analyzeClass(Class<?> targetClass) {
Map<String, String> renameMap = new HashMap<>();
for (Field field : targetClass.getDeclaredFields()) {
ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
renameMap.put(paramNameAnnotation.value(), field.getName());
}
}
if (targetClass.getSuperclass() != Object.class) {
renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
}
return renameMap;
}
}
This processor will analyze fields of superclasses annotated with #ParamName. It also doesn't use initBinder method with 2 parameters which is deprecated as of Spring 5.0. All the rest in jkee's answer is OK.

Categories