Spring - disable bind exceptions (for a particular property) - java

In a web application I'm working on using Spring 2.5.6.SEC01, I essentially have an Integer field that takes a number to determine which page to scroll to. The requirements changed, and we no longer want to display an error message, but simply ignore the user's input if they enter an invalid number, say "adfadf".
I was reading that you can do that via:
TypeMismatch.property=Some New Error Message
However, after having tried that, we are still getting the original error message:
java.lang.Integer.TypeMismatch=...
I only want to disable this message for that given property. How can I do that? I still want binding to occur automatically, I just don't want to hear about it now.
Walter

According to DefaultMessageCodesResolver
In case of code "typeMismatch", object name "user", field "age"
typeMismatch.user.age
typeMismatch.age
typeMismatch.int
typeMismatch
So you should get (I suppose your commandName is called command and your property is age) Adapt according to your code
typeMismatch.command.age
typeMismatch.age
typeMismatch.java.lang.Integer
typeMismatch
Notice The third code
typeMismatch.java.lang.Integer
It will solve what you want
UPDATE
I have created a Person command class
public class Person implements Serializable {
private Integer age;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
And a person controller
public class PersonController extends SimpleFormController {
public PersonController() {
setCommandClass(Person.class);
setValidator(new Validator() {
public boolean supports(Class clazz) {
return clazz.isAssignableFrom(Person.class);
}
public void validate(Object command, Errors errors) {
rejectIfEmpty(errors, "age", "Age is required");
}
});
}
#Override
protected ModelAndView onSubmit(Object command) throws Exception {
return new ModelAndView();
}
}
Here goes my myMessages.properties (root of the classpath)
typeMismatch.command.age=typeMismatch.command.age
typeMismatch.age=typeMismatch.age
typeMismatch.java.lang.Integer=typeMismatch.java.lang.Integer
typeMismatch=typeMismatch
So, i have done the following test
public class PersonControllerTest {
private PersonController personController;
private MockHttpServletRequest request;
private MessageSource messageSource;
#Before
public void setUp() {
request = new MockHttpServletRequest();
request.setMethod("POST");
personController = new PersonController();
messageSource = new ResourceBundleMessageSource();
((ResourceBundleMessageSource) messageSource).setBasename("myMessages");
}
#Test
public void failureSubmission() throws Exception {
/**
* Ops... a bindException
*
* Age can not be a plain String, It must be a plain Integer
*/
request.addParameter("age", "not a meaningful age");
ModelAndView mav = personController.handleRequest(request, new MockHttpServletResponse());
BindingResult bindException = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + "command");
for (Object object : bindException.getAllErrors()) {
if(object instanceof FieldError) {
FieldError fieldError = (FieldError) object;
assertEquals(fieldError.getField(), "age");
/**
* outputs typeMismatch.command.age
*/
System.out.println(messageSource.getMessage((FieldError) object, null));
}
}
}
}
If you want the second one, you must get rid of typeMismatch.command.age key resource bundle
typeMismatch.age=typeMismatch.age
typeMismatch.java.lang.Integer=typeMismatch.java.lang.Integer
typeMismatch=typeMismatch
Or write your own implementation of MessageCodesResolver
public class MyCustomMessageCodesResolver implements MessageCodesResolver {
private DefaultMessageCodesResolver defaultMessageCodesResolver = new DefaultMessageCodesResolver();
public String [] resolveMessageCodes(String errorCode, String objectName) {
if(errorCode.equals("age"))
/**
* Set up your custom message right here
*/
return new String[] {"typeMismatch.age"};
return defaultMessageCodesResolver.resolveMessageCodes(String errorCode, String objectName);
}
public void String[] resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType) {
if(errorCode.equals("age"))
/**
* Set up your custom message right here
*/
return new String[] {"typeMismatch.age"};
return defaultMessageCodesResolver.resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType);
}
}
And set up your PersonController
public class PersonController extends SimpleFormController {
public PersonController() {
setMessageCodesResolver(new MyCustomMessageCodesResolver());
setCommandClass(Person.class);
setValidator(new Validator() {
public boolean supports(Class clazz) {
return clazz.isAssignableFrom(Person.class);
}
public void validate(Object command, Errors errors) {
rejectIfEmpty(errors, "age", "Age is required");
}
});
}

You can register a custom PropertyEditor for that field, which wouldn't fail on type mismatch.

Since this is a Spring MVC application and assuming that it is a simple form, you can set this up in many ways. Can you specify your controller settings? For post request, you can record a suppressed field before the validator is called (assuming you have specified one) or after the validator is called. If you want to do it before validation, you can call [this][2]. After validation, you can call [this][3]
[2]: http://static.springsource.org/spring/docs/2.0.x/api/org/springframework/web/servlet/mvc/BaseCommandController.html#onBind(javax.servlet.http.HttpServletRequest, java.lang.Object, org.springframework.validation.BindException)
[3]: http://static.springsource.org/spring/docs/2.0.x/api/org/springframework/web/servlet/mvc/BaseCommandController.html#onBindAndValidate(javax.servlet.http.HttpServletRequest, java.lang.Object, org.springframework.validation.BindException)

Related

Spring invalid target for Validator

I am trying to validate the string email to check if it already appears within my MYSQL database, when I execute with an email thats already used I get the following error:
java.lang.IllegalStateException: Invalid target for Validator [co2103.hw2.controller.TestResultsValidator#62b41c6]: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'testResults' on field 'email': rejected value [abc#le.ac.uk]; codes [email.testResults.email,email.email,email.java.lang.String,email]; arguments []; default message [is already provided by a different user! Please user another one!]
Here is the validator code
public class TestResultsValidator implements Validator{
private TestResultsRepository TrRepo;
private HomeTestRepository HTRepo;
public TestResultsValidator (TestResultsRepository TrRepo, HomeTestRepository HTRepo) {
this.TrRepo = TrRepo;
this.HTRepo = HTRepo;
}
#Override
public boolean supports(Class<?> clazz) {
return TestResults.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
TestResults tr = (TestResults) target;
for(TestResults t : TrRepo.findAll()) {
//SAME EMAIL
if (tr.getEmail().equals(t.getEmail())) {
errors.rejectValue("email", "email", "is already provided by a different user! Please user another one!");
System.out.println("Email is already taken by a different user, please try another username");
break;
}
The controller code
//Add new results
#RequestMapping(value = "/addResults",method = {RequestMethod.POST , RequestMethod.GET})
public String newHotel(#Valid #ModelAttribute TestResults results, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("errors", result);
return "start";
}
else {
trRepo.save(results);
return "Submitted";
}}
You need to register you validator to spring.
First ad Component Annotation to your validtor.
#Component
public class TestResultsValidator implements Validator{
.....
}
Register it in the controller.
#Controller
class TestResultController {
#Autowired
TestResultsValidator testResultsValidator ;
#InitBinder("testResultsValidator")
protected void initMessageBinder(WebDataBinder binder) {
binder.addValidators(testResultsValidator );
}
}

Implementatio of mutliple ConstraintValidator and their priority (enable/disable by requests endpoint)

Lets say I have an Object with two fields which should be validated:
public class AnyRQ {
#MerchantAccountValidation
#JsonProperty(value = "merchant-account")
private MerchantAccount merchantAccount;
#RequestIdValidation
#JsonProperty(value = "request-id")
private String requestId;
}
Both of the Annotations #MerchantAccountValidation and #RequestIdValidation implements a ConstraintValidator and including the rules to be valid or not. (Just show one class)
public class RequestIdValidator
implements ConstraintValidator<RequestIdValidation, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.length() > 10;
}
}
Now I have a Controller with two endpoints. Endpoint 1 should validate both Fields but Request 2 should just validate requestId.
#RestController
#RequestMapping("/validate")
public class ValidController {
#PostMapping("/endpoint1")
public ResponseEntity<?> register(#Valid #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
#PostMapping("/endpoint2")
public ResponseEntity<?> authorization(#Valid #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
}
Is there any way to achive a kind of priority or inheritance to get this working? I was thinking about to have the Validation Annotation on the method level of the endpoints. But unfortunately this is not working.
Patrick!
To achieve the desired outcome you can use #GroupSequence. It mostly meant for ordering validations (no need to check that entity exists in database, if id is null f.e.), but would work for your task.
Let's say you have 2 validation groups (better names are required :) ):
public interface InitialValidation {
}
#GroupSequence(InitialValidation.class)
public interface InitialValidationGroup {
}
public interface FullValidation {
}
#GroupSequence(FullValidation.class)
public interface FullValidationGroup {
}
Specify them in the DTO:
public class AnyRQ {
#MerchantAccountValidation(groups = FullValidation.class)
#JsonProperty(value = "merchant-account")
private MerchantAccount merchantAccount;
#RequestIdValidation(groups = {InitialValidation.class, FullValidation.class})
#JsonProperty(value = "request-id")
private String requestId;
}
And in the controller use #Validated instead of #Valid to provide corresponding group:
#RestController
#RequestMapping("/validate")
public class ValidController {
#PostMapping("/endpoint1")
public ResponseEntity<?> register(#Validated(FullValidationGroup.class) #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
#PostMapping("/endpoint2")
public ResponseEntity<?> authorization(#Validated(InitialValidationGroup.class) #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
}
The other option is to keep one group in DTO, but specify two groups in controller for #Validated.

How to write custom validation in rest api?

In Spring boot.
I want to do field validation and return an error if the input does not exist in the database.
I am trying to write the custom annotation for multiple input fields.
The controller is as below
#RestController
#Api(description = "The Mailer controller which provides send email functionality")
#Validated
public class SendMailController {
#Autowired
public SendMailService sendemailService;
org.slf4j.Logger logger = LoggerFactory.getLogger(SendMailService.class);
#RequestMapping(method = RequestMethod.POST, value = "/sendMail", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}, produces = {"text/xml", "application/json"})
#ResponseBody
#Async(value = "threadPoolTaskExecutor")
#ApiOperation("The main service operation which sends one mail to one or may recipient as per the configurations in the request body")
public Future<SendMailResult> sendMail(#ApiParam("Contains the mail content and configurations to be used for sending mail") #Valid #RequestBody MailMessage message) throws InterruptedException {
SendMailResult results = new SendMailResult();
try {
sendemailService.sendMessages(message);
long txnid = sendemailService.createAudit (message);
results.setTxnid (txnid);
results.setStatus("SUCCESS");
} catch(MessagingException | EmailServiceException e) {
logger.error("Exception while processing sendMail " + e);
results.setStatus("FAILED");
// TODO Handle error create results
e.printStackTrace();
} catch(Exception e) {
logger.error("Something went wrong " + e);
results.setStatus("FAILED");
// TODO Handle error create results
e.printStackTrace();
}
return new AsyncResult<SendMailResult>(results);
}
}
one DTO that is mapped with request
public class MailContext {
#NotNull
private String clientId;
#NotNull
private String consumer;
public int getClientId() {
return Integer.parseInt(clientId);
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String toJson() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String writeValueAsString = mapper.writeValueAsString(this);
return writeValueAsString;
}
}
Request xml
<mailMessage>
<mailContext>
<clientId>10018</clientId>
<consumer>1</consumer>
</mailContext>
</mailMessage>
I want to write a custom annotation to validate client which exists in the database (table client_tbl) if provided in the request.
consumer: is present in database table cunsumer_tbl
if these not present in database send error message else call service method.
Please suggest how to write such custom annotation with the error.
I know another way to validate this.
Inside your controller, you can register a validator.
#InitBinder
public void setup(WebDataBinder webDataBinder) {
webDataBinder.addValidators(dtoValidator);
}
Where dtoValidator is an instance of Spring Bean, for example, which must implements org.springframework.validation.Validator.
So, you just have to implement two methods: supports() and validate(Object target, Errors errors);
Inside supports() method you can do whatever you want to decide whether the object should be validated by this validator or not. (for example, you can create an interface WithClientIdDto and if the tested object isAssignableFrom() this interface you can do this validation. Or you can check your custom annotation is presented on any field using reflection)
For example: (AuthDtoValidator.class)
#Override
public boolean supports(Class<?> clazz) {
return AuthDto.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
final AuthDto dto = (AuthDto) target;
final String phone = dto.getPhone();
if (StringUtils.isEmpty(phone) && StringUtils.isEmpty(dto.getEmail())) {
errors.rejectValue("email", "", "The phone or the email should be defined!");
errors.rejectValue("phone", "", "The phone or the email should be defined!");
}
if (!StringUtils.isEmpty(phone)) {
validatePhone(errors, phone);
}
}
UPDATE:
You can do that.
Create an annotation
for example:
#Target({ FIELD })
#Retention(RUNTIME)
#Constraint(validatedBy = ClientIdValidator.class)
#Documented
public #interface ClientId {
String message() default "{some msg}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
and implement this validator:
class ClientIdValidator implements ConstraintValidator<ClientId, Long> {
#Override
public boolean isValid(Long value, ConstraintValidatorContext context) {
//validation logc
}
}
More details you can find here: https://reflectoring.io/bean-validation-with-spring-boot/

How to programmatically replace Spring's NumberFormatException with a user-friendly text?

I am working on a Spring web app and i have an entity that has an Integer property which the user can fill in when creating a new entity using a JSP form. The controller method called by this form is below :
#RequestMapping(value = {"/newNursingUnit"}, method = RequestMethod.POST)
public String saveNursingUnit(#Valid NursingUnit nursingUnit, BindingResult result, ModelMap model)
{
boolean hasCustomErrors = validate(result, nursingUnit);
if ((hasCustomErrors) || (result.hasErrors()))
{
List<Facility> facilities = facilityService.findAll();
model.addAttribute("facilities", facilities);
setPermissions(model);
return "nursingUnitDataAccess";
}
nursingUnitService.save(nursingUnit);
session.setAttribute("successMessage", "Successfully added nursing unit \"" + nursingUnit.getName() + "\"!");
return "redirect:/nursingUnits/list";
}
The validate method simply checks if the name already exists in the DB so I did not include it. My issue is that, when I purposely enter text in the field, I would like to have a nice message such as "The auto-discharge time must be a number!". Instead, Spring returns this absolutely horrible error :
Failed to convert property value of type [java.lang.String] to required type [java.lang.Integer] for property autoDCTime; nested exception is java.lang.NumberFormatException: For input string: "sdf"
I fully understand why this is happening but i cannot for the life of me figure out how to, programmatically, replace Spring's default number format exception error message with my own. I am aware of message sources which can be used for this type of thing but I really want to achieve this directly in the code.
EDIT
As suggested, i built this method in my controller but i'm still getting Spring's "failed to convert property value..." message :
#ExceptionHandler({NumberFormatException.class})
private String numberError()
{
return "The auto-discharge time must be a number!";
}
OTHER EDIT
Here is the code for my entity class :
#Entity
#Table(name="tblNursingUnit")
public class NursingUnit implements Serializable
{
private Integer id;
private String name;
private Integer autoDCTime;
private Facility facility;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
#Size(min = 1, max = 15, message = "Name must be between 1 and 15 characters long")
#Column(nullable = false, unique = true, length = 15)
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
#NotNull(message = "The auto-discharge time is required!")
#Column(nullable = false)
public Integer getAutoDCTime()
{
return autoDCTime;
}
public void setAutoDCTime(Integer autoDCTime)
{
this.autoDCTime = autoDCTime;
}
#ManyToOne (fetch=FetchType.EAGER)
#NotNull(message = "The facility is required")
#JoinColumn(name = "id_facility", nullable = false)
public Facility getFacility()
{
return facility;
}
public void setFacility(Facility facility)
{
this.facility = facility;
}
#Override
public boolean equals(Object obj)
{
if (obj instanceof NursingUnit)
{
NursingUnit nursingUnit = (NursingUnit)obj;
if (Objects.equals(id, nursingUnit.getId()))
{
return true;
}
}
return false;
}
#Override
public int hashCode()
{
int hash = 3;
hash = 29 * hash + Objects.hashCode(this.id);
hash = 29 * hash + Objects.hashCode(this.name);
hash = 29 * hash + Objects.hashCode(this.autoDCTime);
hash = 29 * hash + Objects.hashCode(this.facility);
return hash;
}
#Override
public String toString()
{
return name + " (" + facility.getCode() + ")";
}
}
YET ANOTHER EDIT
I am able to make this work using a message.properties file on the classpath containing this :
typeMismatch.java.lang.Integer={0} must be a number!
And the following bean declaration in a config file :
#Bean
public ResourceBundleMessageSource messageSource()
{
ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
resource.setBasename("message");
return resource;
}
This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.
Thank you for your help!
You may be able to override that messaging by providing an implementation of the Spring DefaultBindingErrorProcessor similar to what is done here:
Custom Binding Error Message with Collections of Beans in Spring MVC
You can annotate a method with:
#ExceptionHandler({NumberFormatException.class})
public String handleError(){
//example
return "Uncorrectly formatted number!";
}
and implement whatever you want to do in case the exception of that type is thrown. The given code will handle exceptions happened in the current controller.
For further reference consult this link.
To make global error handling you can use #ControllerAdvice in the following way:
#ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler({NumberFormatException.class})
public String handleError(){
//example
return "Uncorrectly formatted number!";
}
}
#Martin, I asked you about the version because #ControllerAdvice is available starting with version 3.2.
I would recommend you to use #ControllerAdvice, which is an annotation that allows you to write code that is sharable between controllers(annotated with #Controller and #RestController), but it can also be applied only to controllers in specific packages or concrete classes.
ControllerAdvice is intended to be used with #ExceptionHandler, #InitBinder, or #ModelAttribute.
You set the target classes like this #ControllerAdvice(assignableTypes = {YourController.class, ...}).
#ControllerAdvice(assignableTypes = {YourController.class, YourOtherController.class})
public class YourExceptionHandler{
//Example with default message
#ExceptionHandler({NumberFormatException.class})
private String numberError(){
return "The auto-discharge time must be a number!";
}
//Example with exception handling
#ExceptionHandler({WhateverException.class})
private String whateverError(WhateverException exception){
//do stuff with the exception
return "Whatever exception message!";
}
#ExceptionHandler({ OtherException.class })
protected String otherException(RuntimeException e, WebRequest request) {
//do stuff with the exception and the webRequest
return "Other exception message!";
}
}
What you need to keep in mind is that if you do not set the target and you define multiple exception handlers for the same exceptions in different #ControllerAdvice classes, Spring will apply the first handler that it finds. If multiple exception handlers are present in the same #ControllerAdvice class, an error will be thrown.
Solution 1: StaticMessageSource as Spring bean
This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.
Your example uses ResourceBundleMessageSource which uses resource bundles (such as property files). If you want to use everything programmatically, then you could use a StaticMessageSource instead. Which you can then set as a Spring bean named messageSource. For example:
#Configuration
public class TestConfig {
#Bean
public MessageSource messageSource() {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage("typeMismatch.java.lang.Integer", Locale.getDefault(), "{0} must be a number!");
return messageSource;
}
}
This is the simplest solution to get a user friendly message.
(Make sure the name is messageSource.)
Solution 2: custom BindingErrorProcessor for initBinder
This solution is lower level and less easy than solution 1, but may give you more control:
public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
Throwable cause = ex.getCause();
if (cause instanceof NumberFormatException) {
String field = ex.getPropertyName();
Object rejectedValue = ex.getValue();
String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field);
boolean useMyOwnErrorMessage = true; // just so that you can easily see to default behavior one line below
String message = useMyOwnErrorMessage ? field + " must be a number!" : ex.getLocalizedMessage();
FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, codes, arguments, message);
error.wrap(ex);
bindingResult.addError(error);
} else {
super.processPropertyAccessException(ex, bindingResult);
}
}
}
#ControllerAdvice
public class MyControllerAdvice {
#InitBinder
public void initBinder(WebDataBinder binder) {
BindingErrorProcessor customBindingErrorProcessor = new CustomBindingErrorProcessor();
binder.setBindingErrorProcessor(customBindingErrorProcessor);
}
}
It basically intercepts the call to DefaultBindingErrorProcessor.processPropertyAccessException and adds a custom FieldError message when binding failed with a NumberFormatException.
Example code without Spring Web/MVC
In case you want to try it without Spring Web/MVC, but just plain Spring, then you could use this example code.
public class MyApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
Validator validator = context.getBean(LocalValidatorFactoryBean.class);
// Empty person bean to be populated
Person2 person = new Person2(null, null);
// Data to be populated
MutablePropertyValues propertyValues = new MutablePropertyValues(List.of(
new PropertyValue("name", "John"),
// Bad value
new PropertyValue("age", "anInvalidInteger")
));
DataBinder dataBinder = new DataBinder(person);
dataBinder.setValidator(validator);
dataBinder.setBindingErrorProcessor(new CustomBindingErrorProcessor());
// Bind and validate
dataBinder.bind(propertyValues);
dataBinder.validate();
// Get and print results
BindingResult bindingResult = dataBinder.getBindingResult();
bindingResult.getAllErrors().forEach(error ->
System.out.println(error.getDefaultMessage())
);
// Output:
// "age must be a number!"
}
}
#Configuration
class MyConfig {
#Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
class Person2 {
#NotEmpty
private String name;
#NotNull #Range(min = 20, max = 50)
private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Person2(String name, Integer age) {
this.name = name;
this.age = age;
}
}
Handle NumberFormatException.
try {
boolean hasCustomErrors = validate(result, nursingUnit);
}catch (NumberFormatException nEx){
// do whatever you want
// for example : throw custom Exception with the custom message.
}

Jersey filter in Dropwizard to set some global FreeMarker variables

I'm reading https://jersey.github.io/documentation/latest/filters-and-interceptors.html and http://www.dropwizard.io/1.1.4/docs/manual/core.html#jersey-filters to try and make this:
#CookieParam("User-Data") userData: String,
#HeaderParam("User-Agent") userAgent: String,
Not needed in each and every resource GET method of my web app. userData is json data from a cookie with fields like "name" and "id" and userAgent is the full User-Agent string from the header. For each view I pass in:
AppUser.getName(userData), AppUser.isMobile(userAgent)
The getName function parses the json and returns just the name field and the isMobile function returns a true boolean if the string "mobile" is found.
I use this in each view of the app in FreeMarker to display the user's name and to change some layout stuff if mobile is true.
Is there a way to make this less repetitive? I'd rather use a BeforeFilter to just set this automatically each time.
Sounds like something you can just do in a ContainerResponseFilter, which gets called after the return of the view resource/controller. Assuming you are returning a Viewable, you get the Viewable from the ContainerRequestContext#getEntity, get the model from it, and add the extra information to the model.
#Provider
#UserInModel
public class UserInModelFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext request,
ContainerResponseContext response) throws IOException {
Cookie cookie = request.getCookies().get("User-Data");
String header = request.getHeaderString("User-Agent");
String username = AppUser.getName(cookie.getValue());
boolean isMobile = AppUser.isMobile(header);
Viewable returnViewable = (Viewable) response.getEntity();
Map<String, Object> model = (Map<String, Object>) returnViewable.getModel();
model.put("username", username);
model.put("isMobile", isMobile);
}
}
The #UserInModel annotation is a custom Name Binding annotation, which is used to determine which resource classes or methods should go through this filter. Since you don't want all endpoints to go through this filter, just annotate the methods or classes you want.
#NameBinding
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.TYPE, ElementType.METHOD})
public #interface UserInModel {
}
#Path("/")
public class IndexController {
#GET
#UserInModel
#Produces(MediaType.TEXT_HTML)
public Viewable home() {
Map<String, Object> model = new HashMap<>();
return new Viewable("/index", model);
}
}
With Dropwizard, all you need to do is register the filter.
env.jersey().register(UserInModelFilter.class);
If you want to do some preprocessing of the cookie and header before the resource method is called, you can do that in a ContainerRequestFilter, which can also be name bound. And instead of recalculating the AppUser.xxx method in the response filter, you can also just set a property on the ContainerRequestContext#setProperty that you can later retrieve from the same context (getProperty) in the response filter.
UPDATE
The above answer assumes you are using Jersey's MVC support, hence the use of Viewable. If you are using Dropwizard's view support, then it's not much different. You may want to create an abstract class as a parent for all the view classes, that way you can just cast to the abstract type when retrieving the entity from the filter.
public class AbstractView extends View {
private String userName;
private boolean isMobile;
protected AbstractView(String templateName) {
super(templateName);
}
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public boolean isMobile() { return isMobile; }
public void setIsMobile(boolean mobile) { isMobile = mobile; }
}
public class PersonView extends AbstractView {
private final Person person;
public PersonView(Person person) {
super("person.ftl");
this.person = person;
}
public Person getPerson() {
return this.person;
}
}
In the filter
#Provider
#UserInModel
public class UserInModelFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext request,
ContainerResponseContext response) throws IOException {
Cookie cookie = request.getCookies().get("User-Data");
String header = request.getHeaderString("User-Agent");
String username = AppUser.getName(cookie.getValue());
boolean isMobile = AppUser.isMobile(header);
AbstractView returnViewable = (AbstractView) response.getEntity();
returnViewable.setUserName(username);
returnViewable.setIsMobile(isMobile);
}
}
Tested resource class for completeness
#Path("person")
public class PersonController {
#GET
#UserInModel
#Produces(MediaType.TEXT_HTML)
public PersonView person() {
Person person = new Person("peeskillet#fake.com");
return new PersonView(person);
}
}

Categories

Resources