Xorty
Xorty

Reputation: 18861

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

Use Case:

So far:

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

Desired output:

Any ideas how do I achieve the desired output here?

Upvotes: 6

Views: 1134

Answers (2)

Leffchik
Leffchik

Reputation: 2030

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.

Upvotes: 2

ESala
ESala

Reputation: 7058

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

}

Upvotes: 1

Related Questions