sschmeck
sschmeck

Reputation: 7685

Validate URL path for several controller methods

Given multiple REST resources to gather order information.

Within the a Srping Boot 2 application it's implemented across multiple controllers, e.g. OrderController, InvoiceController, etc.

Currently every controller uses the OrderRepository to ensure the the order with the given id exists. Otherwise it throws an exception. It's always the same replicated code.

@RestController
public class OrderController {
    // ...
    @GetMapping("order/{id}")
    public Order getCustomer(@PathVariable final Integer id) {
        return this.orderRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("order not found"));
    }
}

Does the framework provide a callback to write the order id check just once?


I found the AntPathMatcher but it seems not the right way, since it provides just an boolean interface.

Upvotes: 1

Views: 210

Answers (1)

Dimitri Mestdagh
Dimitri Mestdagh

Reputation: 44685

This is usually a good case for bean validation. While there is already builtin support for many cases of validation (@Size, @NotNull, ...), you can also write your own custom constraints.

First of all, you need to define an annotation to validate your ID, for example:

@Documented
@Constraint(validatedBy = OrderIdValidator.class)
@Target({PARAMETER})
@Retention(RUNTIME)
public @interface ValidOrderId {
    String message() default "Invalid order ID";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Since your order ID is always a parameter for your controller mappings, you could use ElementType.PARAMETER to only allow the annotation for method parameters.

The next step is to add the @Constraint annotation to it and point to a custom validator class (eg. OrderIdValidator):

@Component
public class OrderIdValidator implements ConstraintValidator<ValidOrderId, Integer> {
    private OrderRepository repository;

    public OrderIdValidator(OrderRepository repository) {
        this.repository = repository;
    }

    @Override
    public boolean isValid(Integer id, ConstraintValidatorContext constraintValidatorContext) {
        return repository.existsById(id);
    }
}

By implementing the isValid method, you can check whether or not the order exists. If it doesn't exist, an exception will be thrown and the message() property of the @ValidOrderId annotation will be used as a message.

The last step is to add the @Validated annotation to all of your controllers, and to add the @ValidOrderId annotation to all order ID parameters, for example:

@Validated // Add this
@RestController
public class OrderController {
    @GetMapping("order/{id}")
    public Order getCustomer(@PathVariable @ValidOrderId final Integer id) { // Add @ValidOrderId
        // Do stuff
    }
}

If you prefer to use a different response status for your validations, you could always add a class annotated with the @ControllerAdvice annotation and use the following method:

@ExceptionHandler(ConstraintViolationException.class)
public void handleConstraints(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value());
}

Upvotes: 3

Related Questions