I'm using HibernateValidator 4.3.1. Validations are performed as intended throughout the entire application.
I have registered some custom editors to perform validation globally such as for ensuring numeric values (double, int etc) in a text-field, for ensuring valid dates regarding the Joda-Time API etc.
In this type of validation, I'm allowing null/empty values by setting the allowEmpty parameter to false as usual to validate it separately especially for displaying separate user friendly error messages when such fields are left blank.
Therefore, in addition to validating with HibernateValidator and custom editors, I'm trying to use the following validation strategy. Again, this kind of validation is only for those fields which are registered for custom editors are when left blank.
The following is the class that implements the org.springframework.validation.Validator interface.
package test;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import validatorbeans.TempBean;
#Component
public final class TempValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
System.out.println("supports() invoked.");
return TempBean.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
TempBean tempBean = (TempBean) target;
System.out.println("startDate = " + tempBean.getStartDate() + " validate() invoked.");
System.out.println("doubleValue = " + tempBean.getDoubleValue() + " validate() invoked.");
System.out.println("stringValue = " + tempBean.getStringValue() + " validate() invoked.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "startDate", "java.util.date.nullOrEmpty.error");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "doubleValue", "java.lang.double.nullOrEmpty.error");
}
}
The class is designated with the #Component annotation so that it can be auto-wired to a specific Spring controller class. The debugging statements display exactly based on the input provided by a user.
The following is the controller class.
package controller;
import customizeValidation.CustomizeValidation;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import javax.validation.groups.Default;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import test.TempValidator;
import validatorbeans.TempBean;
#Controller
public final class TempController {
#Autowired
private TempService tempService;
private TempValidator tempValidator;
public TempValidator getTempValidator() {
return tempValidator;
}
#Autowired
public void setTempValidator(TempValidator tempValidator) {
this.tempValidator = tempValidator;
}
#RequestMapping(method = {RequestMethod.GET}, value = {"admin_side/Temp"})
public String showForm(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult error, Map model, HttpServletRequest request, HttpServletResponse response) {
return "admin_side/Temp";
}
#RequestMapping(method = {RequestMethod.POST}, value = {"admin_side/Temp"})
public String onSubmit(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult errors, Map model, HttpServletRequest request, HttpServletResponse response) {
//tempValidator.supports(TempBean.class);
//tempValidator.validate(tempBean, errors);
DataBinder dataBinder = new DataBinder(tempBean);
dataBinder.setValidator(tempValidator);
dataBinder.validate();
//errors=dataBinder.getBindingResult();
if (CustomizeValidation.isValid(errors, tempBean, TempBean.ValidationGroup.class, Default.class) && !errors.hasErrors()) {
System.out.println("Validated");
}
return "admin_side/Temp";
}
}
I'm invoking the validator from the Spring controller class itself (which I indeed want) by
DataBinder dataBinder = new DataBinder(tempBean);
dataBinder.setValidator(tempValidator);
dataBinder.validate();
The validator is called but the validation which is expected is not performed.
If only I invoke the validator manually using the following statement (which is commented out above),
tempValidator.validate(tempBean, errors);
then validation is performed. So I don't believe my validator is correctly working. Why does it fail to work with DataBinder?
In my application-context.xml file, this bean is simply configured as follows.
<bean id="tempValidator" class="test.TempValidator"/>
This many packages as below including the test package which the TempValidator class is enclosed within are auto-detected.
<context:component-scan base-package="controller spring.databinder validatorbeans validatorcommands test" use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
<context:include-filter expression="org.springframework.web.bind.annotation.ControllerAdvice" type="annotation"/>
</context:component-scan>
I have even tried to put
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
In my dispatcher-servlet.xml file.
What am I overlooking here?
If I understand well what you try to achieve - distinguish between blank fields and incorrect values entered - you can use MUCH MORE SIMPLER approach:
public class MyBean {
#NotNull
#DateTimeFormat(pattern="dd.MM.yyyy HH:mm")
private DateTime date;
#NotNull
#Max(value=5)
private Integer max;
#NotNull
#Size(max=20)
private String name;
// getters, setters ...
}
Controller mapping:
public void submitForm(#ModelAttribute #Valid MyBean myBean, BindingResult result) {
if (result.hasErrors){
// do something}
else{
// do something else
}
}
Validation messages:
NotNull=Required field.
NotNull.date=Date is required field.
NotNull.max=Max is required field.
Size=Must be between {2} and {1} letters.
Max=Must be lower than {1}.
typeMismatch.java.lang.Integer=Must be number.
typeMismatch.org.joda.time.DateTime=Required format dd.mm.yyyy HH:mm
Spring configuration:
#Configuration
public class BaseValidatorConfig {
#Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean lvfb = new LocalValidatorFactoryBean();
lvfb.setValidationMessageSource(getValidationMessageSource());
return lvfb;
}
protected MessageSource getValidationMessageSource() {// return you validation messages ...}
}
I can provide more details and explanation, if needed.
I don't know why the approach as mentioned in the question didn't work. I didn't make it work but walking through this document, I found another approach that worked for me as per my requirements.
I set the validator inside a method which was designated by the #InitBinder annotation.
From docs
The Validator instance invoked when a #Valid method argument is
encountered may be configured in two ways. First, you may call
binder.setValidator(Validator) within a #Controller's #InitBinder
callback. This allows you to configure a Validator instance per
#Controller class:
Specifically, in my requirements, the validation should only be performed while updating or inserting data into the database i.e when an associated submit button for those operations is pressed (there is a common button for both of these tasks (insert and update) in my application whose name is btnSubmit).
The validation should be muted in any other case (for example, when the delete button is pressed). To meet this requirement, I have registered the validator as follows.
#InitBinder
protected void initBinder(WebDataBinder binder, WebRequest webRequest) {
if (webRequest.getParameter("btnSubmit") != null) {
binder.setValidator(new TempValidator());
} else {
binder.setValidator(null);
}
}
In this situation, the validator - TempValidator would only be set when the submit button whose name attribute is btnSubmit is clicked by the client.
There is no need for xml configuration anywhere as well as auto-wiring.
The exemplary controller class now looks like the following.
#Controller
public final class TempController {
#Autowired
private TempService tempService;
#InitBinder
protected void initBinder(WebDataBinder binder, WebRequest webRequest) {
if (webRequest.getParameter("btnSubmit") != null) {
binder.setValidator(new TempValidator());
} else {
binder.setValidator(null);
}
}
//Removed the #Valid annotation before TempBean, since validation is unnecessary on page load.
#RequestMapping(method = {RequestMethod.GET}, value = {"admin_side/Temp"})
public String showForm(#ModelAttribute("tempBean") TempBean tempBean, BindingResult error, Map model, HttpServletRequest request, HttpServletResponse response) {
return "admin_side/Temp";
}
#RequestMapping(method = {RequestMethod.POST}, value = {"admin_side/Temp"})
public String onSubmit(#ModelAttribute("tempBean") #Valid TempBean tempBean, BindingResult errors, Map model, HttpServletRequest request, HttpServletResponse response) {
if (CustomizeValidation.isValid(errors, tempBean, TempBean.ValidationGroup.class, Default.class) && !errors.hasErrors()) {
System.out.println("Validated");
}
return "admin_side/Temp";
}
}
The WebRequest paramenter in the initBinder() method is not meant for handling the entire Http request as obvious. It's just for using general purpose request metadata.
Javadocs about WebRequest.
Generic interface for a web request. Mainly intended for generic web
request interceptors, giving them access to general request metadata,
not for actual handling of the request.
If there is something wrong that I might be following, then kindly clarify it or add another answer.
Related
I have a Spring project with spring-data-rest as a dependency. I have quite a number of repositories in my project, which spring-data-rest automatically created REST API endpoints for. This suited my needs pretty well until now. Now I have a requirement to change the default functionality of one endpoint for all my repositories, specifically, /BASE_PATH/REPOSITORY. This path responds with a paged list of all records of my db.
Now I want to reimplement this endpoint for all my repositories. This is where I am hitting a roadblock. I tried
#RestController
public class MyTableResource {
private MyTableService myTableService;
#Autowired
public MyTableResource(MyTableService myTableService) {
this.myTableService = myTableService;
}
#GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
public ResponseEntity getMyTables(#QuerydslPredicate(root = MyTable.class) Predicate predicate) throws NoSuchMethodException {
// My custom implementation
}
}
Now this somewhat works but the problem is I need to write almost the same code for all my repositories. I tried #GetMapping(value = "/api/v1/{repository}", produces = MediaTypes.HAL_JSON_VALUE) but this is also matching /api/v1/notarepository which I have implemented separately.
Also, even if I do #GetMapping(value = "/api/v1/{repository}", produces = MediaTypes.HAL_JSON_VALUE) I would like to get a handle to a repository object (MyTable) using {repository} path variable, which would be myTables in this case.
In short, I want to write a single custom controller for all my repositories, since the logic would be the same for each of them, while making sure the correct repository is called based on the path called also making sure that any path variables I introduce does not hide other controller classes I have written.
More things I have tried
I was attempting to get paged HATEOAS resource objects automatically from my list of entities. For this I found that I can use PagedResourceAssembler
#RestController
public class MyTableResource {
private MyTableService myTableService;
#Autowired
public MyTableResource(MyTableService myTableService) {
this.myTableService = myTableService;
}
#GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
public ResponseEntity getMyTables(#QuerydslPredicate(root = MyTable.class) Predicate predicate, PagedResourcesAssembler<Object> pagedResourcesAssembler) throws NoSuchMethodException {
// My custom implementation
return ResponseEntity.ok(pagedResourcesAssembler.toResource(myTableList);
}
}
This gives me a good response with the required links for the page but does not give links per entity. Then I found I can hook up PersistentEntityResourceAssembler and pass it to toResource above so I did
#RestController
public class MyTableResource {
private MyTableService myTableService;
#Autowired
public MyTableResource(MyTableService myTableService) {
this.myTableService = myTableService;
}
#GetMapping(value = "/api/v1/myTables", produces = MediaTypes.HAL_JSON_VALUE)
public ResponseEntity getMyTables(#QuerydslPredicate(root = MyTable.class) Predicate predicate, PagedResourcesAssembler<Object> pagedResourcesAssembler, PersistentEntityResourceAssembler assembler) throws NoSuchMethodException {
// My custom implementation
return ResponseEntity.ok(pagedResourcesAssembler.toResource(myTableList, assembler);
}
}
This does not work as reported in How to have PersistentEntityResourceAssembler injected into request methods of custom #RepositoryRestController in a #WebMvcTest unit test .
It kind of works if I replace #RestController with RepositoryRestController but then Predicate stops working as mentioned in https://jira.spring.io/browse/DATAREST-838 .
So, I tried using #QuerydslPredicate RootResourceInformation resourceInformation instead of #QuerydslPredicate(root = MyTable.class) Predicate predicate. This also did not work as my controller endpoint does not have /{repository} in it.
Then I tried setting #GetMapping(value = "/{repository}" produces = MediaTypes.HAL_JSON_VALUE). This threw a mapping conflict error.
So I am completely stuck as to what to do next.
You can extend the default behavior provided by Spring Data Rest by extending RepositoryRestMvcConfiguration.
RepositoryRestMvcConfiguration has a DelegatingHandlerMapping bean which holds a list of HandlerMapping. Spring iterates over this list and tries to find a handler for the request. The order of this list is important. The first one gets picked up first for the execution. So if we add a new handler in front of the ones we already have then our HandlerMapping will be called.
You can use whatever logic you want to find the handler for the request. In your case, this would be if the path variable is a repository name.
The following code adds a new handler:
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
#Configuration
public class CustomRestMvcConfiguration extends RepositoryRestMvcConfiguration {
public CustomRestMvcConfiguration(ApplicationContext context,
ObjectFactory<ConversionService> conversionService) {
super(context, conversionService);
}
#Override public DelegatingHandlerMapping restHandlerMapping() {
DelegatingHandlerMapping delegatingHandlerMapping = super.restHandlerMapping();
List<HandlerMapping> delegates = delegatingHandlerMapping.getDelegates();
delegates.add(0, new HandlerMapping() {
#Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//Your custom logic to decide if you should handle the request
//If you don't want to handle the request return null
return null;
}
});
return new DelegatingHandlerMapping(delegates);
}
}
Hope this helps!
Note: RepositoryRestHandlerMapping is the default one you can check it while writing your logic. It might be helpful.
I am developing a web application in Java using spring.
This application includes Ajax calls in javascript which requests html code that is then inserted into the html document.
In order to process a thymeleaf template into a String i'm using TemplateEngine process(..) method.
I encountered an error when the thymeleaf template contains a form.
My sample code:
form.html:
<form th:object="${customer}" xmlns:th="http://www.w3.org/1999/xhtml">
<label>Name</label>
<input type="text" th:field="*{name}" />
</form>
AjaxController.java:
package project;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
#Controller
public class AjaxController {
#Autowired
private TemplateEngine templateEngine;
private ObjectMapper objectMapper = new ObjectMapper();
#ResponseBody
#GetMapping(value="/form1")
public String form1() throws JsonProcessingException {
Customer customer = new Customer("Burger King");
Context templateContext = new Context();
templateContext.setVariable("customer", customer);
AjaxResponse response = new AjaxResponse();
response.html = templateEngine.process("form", templateContext);
response.additionalData = "ab123";
return objectMapper.writeValueAsString(response);
}
#GetMapping(value="/form2")
public String form2(Model model) throws JsonProcessingException {
Customer customer = new Customer("Burger King");
model.addAttribute("customer", customer);
return "form";
}
class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class AjaxResponse {
public String html;
public String additionalData;
}
}
form1 is the one crashing, I'm trying to return the html code parsed by the thymeleaf template and also include additional data in this json response.
It crashes on the line templateEngine.process("form", templateContext);
form1 works when replacing form.html with:
Customer name is: [[${customer.name}]]
Which leads me to conclude that it is the form tag and th:object which causes this to crash.
form2 works just as expected, but without any way to manipulate the thymeleaf return value. It proves that the thymeleaf template itself is valid.
The whole error output is a bit too massive to paste in here but:
org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/form.html]")
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Cannot process attribute '{th:field,data-th-field}': no associated BindStatus could be found for the intended form binding operations. This can be due to the lack of a proper management of the Spring RequestContext, which is usually done through the ThymeleafView or ThymeleafReactiveView (template: "form" - line 3, col 21)
My question is: Is this a bug in the spring framework? or if not then what am i doing wrong?
Update 1:
Replacing th:field with th:value makes it work, seems that th:field inside a form when using TemplateEngine .process is what produces the error.
Update 2:
Okay so after a lot of detective work i've figured out a sort of hack to make this work temporarily. The problem is that thymeleaf requires IThymeleafRequestContext to process a template with a form, When TemplateEngine .process runs then this will not be created. It is possible to inject this into your model like following:
#Autowired
ServletContext servletContext;
private String renderToString(HttpServletRequest request, HttpServletResponse response, String viewName, Map<String, Object> parameters) {
Context templateContext = new Context();
templateContext.setVariables(parameters);
RequestContext requestContext = new RequestContext(request, response, servletContext, parameters);
SpringWebMvcThymeleafRequestContext thymeleafRequestContext = new SpringWebMvcThymeleafRequestContext(requestContext, request);
templateContext.setVariable("thymeleafRequestContext", thymeleafRequestContext);
return templateEngine.process(viewName, templateContext);
}
and now you use this method like this:
#ResponseBody
#GetMapping(value="/form1")
public String form1(HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException {
Customer customer = new Customer("Burger King");
BindingAwareModelMap bindingMap = new BindingAwareModelMap();
bindingMap.addAttribute("customer", customer);
String html = renderToString(request, response, "form", bindingMap);
AjaxResponse resp = new AjaxResponse();
resp.html = html;
resp.additionalData = "ab123";
String json = objectMapper.writeValueAsString(resp);
return json;
}
I will not put down this as an answer as i don't see any reason of this being intended to be used this way. I'm in communication with the spring people to get a real fix for this.
Welcome to SO.
Remove xmlns:th="http://www.w3.org/1999/xhtml" from the form tag. This is not proper syntax. This would belong in the html tag.
You can find plenty of clear examples in the docs.
It seems you're trying to manually render an HTML template, outside of the web request context, return it serialized as an AJAX response - but still expect form binding to work. This is the key problem here.
Using th:field in a template means that you're expecting form binding from the HTTP request. In your code snippet, you're providing an empty, non-web context and still expect form binding to happen.
Since Thymeleaf can be used in various contexts (like rendering an email template before sending a newsletter, rendering a document in a batch application), we can't enforce a web context in all cases.
When rendering views the way Spring Framework expects things (by returning the view name as the return value of the controller handler), Spring will use and configure Thymeleaf accordingly.
Your answer is technically valid because it solves your problem, but it comes from the convoluted constraint of rendering a template and wrap that into a json String, and still expect HTTP binding.
I want to create Spring aspect which would set method parameter, annotated by custom annotation, to an instance of a particular class identified by an id from URI template. Path variable name is parameter of the annotation. Very similar to what Spring #PathVariable does.
So that controller method would look like:
#RestController
#RequestMapping("/testController")
public class TestController {
#RequestMapping(value = "/order/{orderId}/delete", method = RequestMethod.GET)
public ResponseEntity<?> doSomething(
#GetOrder("orderId") Order order) {
// do something with order
}
}
Instead of classic:
#RestController
#RequestMapping("/testController")
public class TestController {
#RequestMapping(value = "/order/{orderId}/delete", method = RequestMethod.GET)
public ResponseEntity<?> doSomething(
#PathVariable("orderId") Long orderId) {
Order order = orderRepository.findById(orderId);
// do something with order
}
}
Annotation source:
// Annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface GetOrder{
String value() default "";
}
Aspect source:
// Aspect controlled by the annotation
#Aspect
#Component
public class GetOrderAspect {
#Around( // Assume the setOrder method is called around controller method )
public Object setOrder(ProceedingJoinPoint jp) throws Throwable{
MethodSignature signature = (MethodSignature) jp.getSignature();
#SuppressWarnings("rawtypes")
Class[] types = signature.getParameterTypes();
Method method = signature.getMethod();
Annotation[][] annotations = method.getParameterAnnotations();
Object[] values = jp.getArgs();
for (int parameter = 0; parameter < types.length; parameter++) {
Annotation[] parameterAnnotations = annotations[parameter];
if (parameterAnnotations == null) continue;
for (Annotation annotation: parameterAnnotations) {
// Annotation is instance of #GetOrder
if (annotation instanceof GetOrder) {
String pathVariable = (GetOrder)annotation.value();
// How to read actual path variable value from URI template?
// In this example case {orderId} from /testController/order/{orderId}/delete
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder
.currentRequestAttributes()).getRequest();
????? // Now what?
}
} // for each annotation
} // for each parameter
return jp.proceed();
}
}
UPDATE 04/Apr/2017:
Answer given by Mike Wojtyna answers the question -> thus it is accepted.
Answer given by OrangeDog solves the problem form different perspective with existing Spring tools without risking implementation issue with new aspect. If I knew it before, this question would not be asked.
Thank you!
If you already have access to HttpServletRequest, you can use HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE spring template to select map of all attributes in the request. You can use it like that:
request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)
The result is a Map instance (unfortunately you need to cast to it), so you can iterate over it and get all the parameters you need.
The easiest way to do this sort of thing is with #ModelAttribute, which can go in a #ControllerAdvice to be shared between multiple controllers.
#ModelAttribute("order")
public Order getOrder(#PathVariable("orderId") String orderId) {
return orderRepository.findById(orderId);
}
#DeleteMapping("/order/{orderId}")
public ResponseEntity<?> doSomething(#ModelAttribute("order") Order order) {
// do something with order
}
Another way is to implement your own PathVariableMethodArgumentResolver that supports Order, or register a Converter<String, Order>, that the existing #PathVariable system can use.
Assuming that it is always the first parameter bearing the annotation, maybe you want to do it like this:
package de.scrum_master.aspect;
import java.lang.annotation.Annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import de.scrum_master.app.GetOrder;
#Aspect
#Component
public class GetOrderAspect {
#Around("execution(* *(#de.scrum_master.app.GetOrder (*), ..))")
public Object setOrder(ProceedingJoinPoint thisJoinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
for (Annotation[] annotations : annotationMatrix) {
for (Annotation annotation : annotations) {
if (annotation instanceof GetOrder) {
System.out.println(thisJoinPoint);
System.out.println(" annotation = " + annotation);
System.out.println(" annotation value = " + ((GetOrder) annotation).value());
}
}
}
return thisJoinPoint.proceed();
}
}
The console log would look like this:
execution(ResponseEntity de.scrum_master.app.TestController.doSomething(Order))
annotation = #de.scrum_master.app.GetOrder(value=orderId)
annotation value = orderId
If parameter annotations can appear in arbitrary positions you can also use the pointcut execution(* *(..)) but this would not be very efficient because it would capture all method executions for each component in your application. So you should at least limit it to REST controlers and/or methods with request mappings like this:
#Around("execution(#org.springframework.web.bind.annotation.RequestMapping * (#org.springframework.web.bind.annotation.RestController *).*(..))")
A variant of this would be
#Around(
"execution(* (#org.springframework.web.bind.annotation.RestController *).*(..)) &&" +
"#annotation(org.springframework.web.bind.annotation.RequestMapping)"
)
I have some fields in a model that I only want to be returned when the logged in user has the role ROLE_ADMIN. I can use #JsonIgnore but that hides it for everyone. How can I make it hide dynamically?
You should use Jackson Json Views technology to acheive it - it allows to choose a different set of fields to be serialized programatically. It is also supported by Spring
Consider you have a class Model with two properties: commonField which should be available for everyone and secretField which should be available only for certain users. You should create an hierarchy of views (any classes would work) and specify which field is available in which view using #JsonView annotation
package com.stackoverflow.jsonview;
import com.fasterxml.jackson.annotation.JsonView;
public class Model {
public static class Public {}
public static class Secret extends Public {}
#JsonView(Public.class)
private String commonField;
#JsonView(Secret.class)
private String secretField;
public Model() {
}
public Model(String commonField, String secretField) {
this.commonField = commonField;
this.secretField = secretField;
}
public String getCommonField() {
return commonField;
}
public void setCommonField(String commonField) {
this.commonField = commonField;
}
public String getSecretField() {
return secretField;
}
public void setSecretField(String secretField) {
this.secretField = secretField;
}
}
Now you can specify the view you want to use in concrete ObjectMapper
package com.stackoverflow.jsonview;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import static org.junit.Assert.*;
/**
*/
public class ModelTest {
#Test
public void testSecretField() throws JsonProcessingException {
Model model = new Model("commonField","secretField");
assertEquals("{\"commonField\":\"commonField\",\"secretField\":\"secretField\"}", new ObjectMapper().writerWithView(Model.Secret.class).writeValueAsString(model));
assertEquals("{\"commonField\":\"commonField\"}", new ObjectMapper().writerWithView(Model.Public.class).writeValueAsString(model));
}
}
I am not sure if you can use declaratie approach to make spring choose the right view based on user role out of the box, so probably you will have to write some code like this:
#RequestMapping("/data")
public String getData(HttpServletRequest request) {
Model model = service.getModel();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper = request.isUserInRole("ROLE_ADMIN") ? objectMapper.writerWithView(Model.Secret.class) : objectMapper.writerWithView(Model.Public.class);
return objectMapper.writeValueAsString(model);
}
I solved this after literally a full month of trying various things. I'm working with Spring 4.3.1 and boot, with data being returned in Hal using a pagedrepository.
extend RepositoryRestMvcConfiguration as MyRepositoryRestMvcConfiguration and add #Configuration to the class, make sure your starter class has #EnableWebMvc
add this to MyRepositoryRestMvcConfiguration- extend TypeConstrainedMappingJackson2HttpMessageConverter as MyResourceSupportHttpMessageConverter
add this to MyRepositoryRestMvcConfiguration
#Override
#Bean
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() {
ArrayList<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaTypes.HAL_JSON);
if (config().useHalAsDefaultJsonMediaType()) {
mediaTypes.add(MediaType.APPLICATION_JSON);
}
int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 10
: Ordered.LOWEST_PRECEDENCE - 1;
TypeConstrainedMappingJackson2HttpMessageConverter converter = new MyResourceSupportHttpMessageConverter(
order);
converter.setObjectMapper(halObjectMapper());
converter.setSupportedMediaTypes(mediaTypes);
converter.getObjectMapper().addMixIn(Object.class, MyFilteringMixin.class);
final FilterProvider myRestrictionFilterProvider = new SimpleFilterProvider()
.addFilter("MyFilteringMixin", new MyPropertyFilter()).setFailOnUnknownId(false);
converter.getObjectMapper().setFilterProvider(myRestrictionFilterProvider);
return converter;
}
Create an empty Mixin
package filters;
import com.fasterxml.jackson.annotation.JsonFilter;
#JsonFilter("MyFilteringMixin")
public class MyFilteringMixin {}
Create an empty Mixin
create class MyPropertyFilter extending SimpleBeanPropertyFilter and override adapt this method
serializeAsField(Object, JsonGenerator, SerializerProvider, PropertyWriter)you need to call either super.serializeAsField(pPojo, pJgen, pProvider, pWriter) or pWriter.serializeAsOmittedField(pPojo, pJgen, pProvider) depending on whether you wish to include or discard this particular field.
I added an annotation to the particular fields I wanted to alter and interrogated that annotation when deciding which of these two to call. I injected the security role and stored permitted roles in the annotation.
This alters what Hal shares out to the caller, not what Hal is holding in its repository. Thus you can morph it depending on who the caller is.
My controller. Note the custom #Exists annotation:
#RestController
public class ClientApiController {
#RequestMapping(path = "/{client}/someaction", method = RequestMethod.GET)
String handleRequest(#Exists Client client) {
// ...
}
}
The Exists annotation:
/**
* Indicates that a controller request mapping method parametet should not be
* null. This is meant to be used on model types to indicate a required entity.
*/
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Exists {}
The converter which converts the String from the path variable into a Client instance:
#Component
public class StringToClient implements Converter<String, Client> {
#Autowired
private ClientDAO clientDAO;
#Override
public Client convert(String source) {
return clientDAO.getClientById(source);
}
}
The ResourceNotFoundException exception used to trigger a 404
#ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
My controller method receives the converted Client as desired. If the client id used in the URL matches a client, everything works fine. If the id doesn't match, the client parameter is null empty (uses default constructor) in the handle() controller method.
What I can't get to work now is declarative checking that the Client is not null (i.e. that the id refers to an existing client). If it's null, a ResourceNotFoundException should be thrown. Checking whether the argument is null in the method body and throwing my custom ResourceNotFoundException is easy to do, but repetitive (like this one does). Also, this declarative approach should work for all model classes implementing the interface ModelWithId so it can be used for multiple model types.
I've searched the Spring documentation and I haven't found how to achieve this. I need to insert some processing somewhere after type conversion and before the controller's handleRequest method.
I'm using Spring Boot 1.3.3
After type conversion and before the controller's method there is a validation. You can implement custom validator and raise exception in it. Add new validator to DataBinder, and mark method's parameter as #Validated:
#RestController
public class ClientApiController {
#InitBinder
public void initBinder(DataBinder binder){
binder.addValidators(new Validator() {
#Override
public boolean supports(Class<?> aClass) {
return aClass==Client.class;
}
#Override
public void validate(Object o, Errors errors) {
Client client = (Client)o;
if(client.getId()==null) throw new ResourceNotFoundException();
}
});
}
#RequestMapping(path = "/{client}/someaction", method = RequestMethod.GET)
String handleRequest(#Validated #Exists Client client) {
// ...
}
#RequestMapping(path = "/{client}/anotheraction", method = RequestMethod.GET)
String handleAnotherRequest(#Validated #Exists Client client) {
// ...
}
}
Of course, you can declare validator as separate class, and use it repeatedly in other controllers. Actually, you can raise exception right in your converter, but there is possibility, that you'll need the conversion without exception in other places of your application.