Spring pageable cache key for each page - spring

I am trying to do something like following
#Cacheable(value = ACTIVE_DATA_CONFIGURATION_CACHE, key = "#tenant.id.concat('-').concat(#pageable.page)")
public Page<DataConfiguration> findAllByTenant(final Pageable pageable, final Tenant tenant) {
}
exception
org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'page' cannot be found on object of type 'org.springframework.data.domain.PageRequest' - maybe not public?
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:224)

As Pageable does not have a property page, if you are trying to generate a key as {tenant.id}-{pageNumber}, you can change the key value to #tenant.id.concat('-').concat(#pageable.pageNumber).

For your use case try using KeyGenerator
#Cacheable(value = "doOneThing", keyGenerator = "CustomGenerator")
Something like this:
public class CustomGenerator implements KeyGenerator {
public Object generate(Object target, Method method, Object... params) {
String code = "UNIQUE_CODE"; // implements logic from params
return code;
}
}

Related

FormUrlEncoded POST request, I need to convert snake case values into camelCase with SpringBoot and Jackson

I am integrating with a third-party's vendor API.
I have a SpringBoot and Jackson setup
They are sending me a POST request that is of type formUrlEncoded and with the params in snake_case
(over 10 params in total and no body)
e.g.
POST www.example.com?player_id=somePlayerId&product_id=someProductId&total_amount=totalAmount...
There are many out of the box helpers for JSON but I cannot find any for formUrlEncoded (I hope I am missing something obvious).
I have tried #ModelAttribute and #RequestParam but had no luck.
I am trying to avoid the #RequestParam MultiValueMap<String, String> params + custom mapper option
#RequestParam is the simplest way which allows you to define the exact name of the query parameter something like:
#PostMapping
public String foo(#RequestParam("player_id") String playerId){
}
If you want to bind all the query parameters to an object , you have to use #ModelAttribute. It is based on the DataBinder and is nothing to do with Jackson. By default it only supports binding the query parameter to an object which fields have the same name as the query parameter. So you can consider to bind the query paramater to the following object :
public class Request {
private String player_id;
private String product_id;
private Long total_amount;
}
If you really want to bind to the object that follow traditional java naming convention (i.e lower camel case) from the query parameter that has snake case values , you have to cusomtize WebDataBinder.
The idea is to override its addBindValues() and check if the query parameter name is in snake case format , convert it the lower camel case format and also add it as the bind values for the request. Something like :
public class MyServletRequestDataBinder extends ExtendedServletRequestDataBinder {
private static Converter<String, String> snakeCaseToLowerCamelConverter = CaseFormat.LOWER_UNDERSCORE
.converterTo(CaseFormat.LOWER_CAMEL);
public MyServletRequestDataBinder(Object target) {
super(target);
}
public MyServletRequestDataBinder(Object target, String objectName) {
super(target, objectName);
}
#Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames != null && paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
if(paramName.contains("_")) {
String[] values = request.getParameterValues(paramName);
if (values == null || values.length == 0) {
// Do nothing, no values found at all.
} else if (values.length > 1) {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values);
} else {
mpvs.addPropertyValue(snakeCaseToLowerCamelConverter.convert(paramName), values[0]);
}
}
}
}
}
P.S I am using Guava for helping me to convert snake case to lowerCamelCase.
But in order to use the customized WebDataBinder , you have to in turn customize WebDataBinderFactory and RequestMappingHandlerAdapter because :
customize WebDataBinderFactory in order to create the customised WebDataBinder
customize RequestMappingHandlerAdapter in order to create the WebDataBinderFactory
Something like:
public class MyServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {
public MyServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(binderMethods, initializer);
}
#Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName,
NativeWebRequest request) throws Exception {
return new MyServletRequestDataBinder(target, objectName);
}
}
and
public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
#Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
throws Exception {
return new MyServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}
}
And finally register to use the customised RequestMappingHandlerAdapter in your configuration :
#Configuration
public class Config extends DelegatingWebMvcConfiguration {
#Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
}
I don't think you are missing anything. Looking at the RequestParamMethodArgumentResolver#resolveName source I do no see a way to customize how a request parameter is matched. So it looks either you have to implement your own resolver or just annotate each parameter with #RequestParam and provide the name, e.g. #RequestParam("product_id") String productId
EDIT:
As for ModelAttribute, ModelAttributeMethodProcessor uses WebDataBinder. Again you can customize it with your custom DataBinder but I didn't found any that out of the box supports aliases as Jackson does.

How to test SpringMvc controller that returns pageable result with MockMvc?

Using spring-boot 2.2.4.
I have a SpringMvc Controller that returns pageable objects:
#RestController
#RequestMapping("/call-data")
public class CallDataController {
#GetMapping
public Page<CallDataDto> findAll(Pageable page) {
...
Trying to test it with MockMvc:
ObjectMapper mapper = new ObjectMapper();
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/call-data")).andReturn();
Page<CallDataDto> myDtos = mapper.readValue(mvcResult.getResponse().getContentAsString(), TypeUtils.pageTypeRef());
...
public class TypeUtils {
public static <T> TypeReference<RestResponsePage<T>> pageTypeRef() {
return new TypeReference<>() {
};
}
But instead of page with dto objects I get a page with LinkedHashMaps.
So how to get the page with dto objects?
Similar question: ObjectMapper using TypeReference not working when passed type in generic method
You can solve the problem by replacing the type parameter(T) with CallDataDto.
public class TypeUtils {
public static TypeReference<RestResponsePage<CallDataDto>> pageTypeRef() {
return new TypeReference<>() {
};
}
Type parameters(e.g. <T>) don't exist at runtime so you have to replace them with some concrete values so that Jackson can obtain full generics type information.

DTO has only null with GET request params, but not POST #RequestBody

I'm trying to get my query params in a DTO like in this question but my DTO has always null value.
Is there anything wrong in my code ? I made it as simple as possible.
Queries:
GET http://localhost:8080/api/test?a=azaz => null
POST http://localhost:8080/api/test with {"a":"azaz"} => "azaz"
Controller with a GET and a POST:
#RestController
#RequestMapping(path = {"/api"}, produces = APPLICATION_JSON_VALUE)
public class MyController {
// GET: dto NOT populated from query params "?a=azaz"
#RequestMapping(method = GET, path = "test")
public #ResponseBody String test(TestDto testDto){
return testDto.toString(); // null
}
// POST: dto WELL populated from body json {"a"="azaz"}
#RequestMapping(method = POST, path = "test")
public #ResponseBody String postTest(#RequestBody TestDto testDto){
return testDto.toString(); // "azaz"
}
}
DTO:
public class TestDto {
public String a;
#Override
public String toString() {
return a;
}
}
Thanks !
Full Spring boot sample to illustrate it
The problem is that you are missing setter for the field.
public void setA(String a) {
this.a = a;
}
should fix it.
I'm assuming that you have done required configuration like having Jackson mapper in the class path, consume json attribute, getter and setter in DTO classes etc.
One thing missed here is, in RequestMapping use value attribute instead of path attribute as shown below
#RequestMapping(method = POST, value= "/test", consumes="application/json")
public #ResponseBody String postTest(#RequestBody TestDto testDto){
return testDto.toString();
}
And, make sure that you set content-type="application/json" while sending the request
I think what you are trying to do is not possible. To access the query Parameter you have to use #RequestParam("a"). Then you just get the String. To get your object this way you have to pass json as Parameter. a={"a":"azaz"}
Kind regards

My spring mvc session was changed implicitly after called a service method?

I'm using spring 3.2.5 via annotations and got some issue dealing with session.
My controller class is like this:
#Controller
public class WebController {
#Autowired
private IElementService elementService;
...
//in this method I set the "elementList" in session explicitly
#RequestMapping("/elementSearch.do")
public String elementSearch(
#RequestParam("keyword") String keyword,
HttpSession session){
List<Element> elementList= elementService.searchElement(keyword);
session.setAttribute("elementList", elementList);
return "searchResult";
}
//here I got my problem
#RequestMapping(value="/anotherMethod.do", produces="text/html; charset=utf-8")
#ResponseBody
public String anotherMethod(
...
//I called my service method here like
Element e = elementService.searchElement("something").get(0);
...
}
And I have a ElementServiceImpl class like this:
#Service
public class ElementServiceImpl implements IElementService {
#Autowired
private IBaseDAO baseDao;
#Override
public List<Metadata> searchElement(String keyword) {
List<Metadata> re = baseDao.searchElement(keyword);
return re;
}
}
And I have a BaseDAOImpl class implemented IBaseDAO and annonated with #Repository:
#Repository
public class BaseDAOImpl implements IBaseDAO {
...
}
Here is the problem, when I visit ".../anotherMethod.do", which will call the anotherMethod up there, my "elementList" in session was changed!
Then I looked into the anotherMethod() and found everytime
Element e = elementService.searchElement("something").get(0);
was called, my elementList was change to the new result returned by searchElement method(which returns a List).
But I didn't set session in that method, and I'm not using #SessionAttributes, so I don't understand how could my session attribute changed after calling a service method?
This problem is torturing me right now so any advise would be a great help, thanks!
update: I tried to print all my session attributes around that method call like this:
StringBuilder ss1 = new StringBuilder("-------------current session-------------\n");
session.setAttribute("test1", "test value 1");
log.info("sessionTest - key:test1 value:" + session.getAttribute("test"));
Enumeration<String> attrs1 = session.getAttributeNames();
while(attrs1.hasMoreElements()){
String key = attrs1.nextElement();
ss1.append(key).append(":").append(session.getAttribute(key)).append("\n");
}
log.info(ss1);
But I didn't see whether the "elementList" or the test value which I added just before print. And I do can get some value by
List<Element> elementList = (List<Element>) session.getAttribute("elementList");
and the elementList I get changed after calling service method, just like I posted before. Where my elementList stored? not in the session?
My goal is to show the elementList to the users in a table, and let them pick one of them, then I get the row number of the table and take it as a index of the elemntList, so I'll know which elemnt object the user picked. Are there any better way to do this so I can get rid of that problem?
Thanks again.

Is it possible to drive the #Size "max" value from a properties file?

I'm using Spring 3.1.1.RELEASE. I have a model with the following attribute
import javax.validation.constraints.Size;
#Size(max=15)
private String name;
I validate the model in my controller my running
#RequestMapping(value = "/save", method = RequestMethod.POST)
public ModelAndView save(final Model model,
#Valid final MyForm myForm,
I would like to have the "15" value come from a properties file instead of hard-coded, but am unclear if that's possible or how its done. Any ideas?
This is not possible. The constant expression that you provide as a value for the max attribute is added at compile time. There is no way to change the value of an annotation at runtime. Setting it from a properties file you read is therefore not possible
What you can do instead is to create and register your own Validator for the your class that has that field. For example,
public class MyValidator implements Validator {
public void validate(Object target, Errors errors) {
MyObject obj = (MyObject) target;
int length = getProperties().get("max.size");
if (obj.name.length() > length) {
errors.rejectValue("name", "String length is bigger than " + length);
}
}
public boolean supports(Class<?> clazz) {
return clazz == MyOBject.class;
}
}
Take a look at Spring's validation framework.

Resources