Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MVC (RESTful API): Validating payload dependent on a path variable

Use Case:

  • let's design a RESTful create operation using POST HTTP verb - creating tickets where creator (assigner) specifies a ticket assignee
  • we're creating a new "ticket" on following location: /companyId/userId/ticket
  • we're providing ticket body containing assigneeId:

    { "assigneeId": 10 }

  • we need to validate that assigneeId belongs to company in URL - companyId path variable

So far:

@RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
public void createTicket(@Valid @RequestBody Ticket newTicket, @PathVariable Long companyId, @PathVariable Long userId) {
  ...
}
  • we can easily specify a custom Validator (TicketValidator) (even with dependencies) and validate Ticket instance
  • we can't easily pass companyId to this validator though! We need to verify that ticket.assigneeId belongs to company with companyId.

Desired output:

  • ability to access path variables in custom Validators

Any ideas how do I achieve the desired output here?

like image 929
Xorty Avatar asked Oct 28 '15 21:10

Xorty


2 Answers

If we assume that our custom validator knows desired property name, then we can do something like this:

Approach one:

1) We can move this getting path variables logic to some kind of a base validator:

public abstract class BaseValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz)
    {
        // supports logic
    }

    @Override
    public void validate(Object target, Errors errors)
    {
        // some base validation logic or empty if there isn't any
    }

    protected String getPathVariable(String name) {
        // Getting current request (Can be autowired - depends on your implementation)
        HttpServletRequest req = HttpServletRequest((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (req != null) {
            // getting variables map from current request
            Map<String, String> variables = req.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            return variables.get(name);
        }
        return null;
    }
}

2) Extend it with your TicketValidator implementation:

public class TicketValidator extends BaseValidator {

    @Override
    public void validate(Object target, Errors errors)
    {
        // Getting our companyId var
        String companyId = getPathVariable("companyId");
        ...
        // proceed with your validation logic. Note, that all path variables
        // is `String`, so you're going to have to cast them (you can do 
        // this in `BaseValidator` though, by passing `Class` to which you 
        // want to cast it as a method param). You can also get `null` from 
        // `getPathVariable` method - you might want to handle it too somehow
    }
}

Approach two:

I think it worth to mention that you can use @PreAuthorize annotation with SpEL to do this kind of validation (You can pass path variables and request body to it). You'll be getting HTTP 403 code though if validation woudnt pass, so I guess it's not exaclty what you want.

like image 59
Leffchik Avatar answered Oct 20 '22 23:10

Leffchik


You could always do this:

@Controller
public class MyController {

    @Autowired
    private TicketValidator ticketValidator;

    @RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
    public void createTicket(@RequestBody Ticket newTicket,
            @PathVariable Long companyId, @PathVariable Long userId) {

        ticketValidator.validate(newTicket, companyId, userId);
        // do whatever

    }

}

Edit in response to the comment:

It doesn't make sense to validate Ticket independently of companyId when the validity of Ticket depends on companyId.

If you cannot use the solution above, consider grouping Ticket with companyId in a DTO, and changing the mapping like this:

@Controller
public class MyController {

    @RequestMapping(value="/{userId}/ticket", method=POST)
    public void createTicket(@Valid @RequestBody TicketDTO ticketDto,
            @PathVariable Long userId) {

        // do whatever
    }

}

public class TicketDTO {

    private Ticket ticket;

    private Long companyId;

    // setters & getters

}
like image 37
ESala Avatar answered Oct 20 '22 22:10

ESala