I have created a custom string converter for spring webflow that trims spaces for every String property
public class StringTrimmerConverter implements Converter {
public Object convertSourceToTargetClass(final Object object, final Class clazz) throws Exception {
if ((object != null) && (object.getClass() == getSourceClass()) && (clazz == getTargetClass())) {
return ((String) object).trim();
}
return object;
}
public Class<String> getSourceClass() { return String.class; }
public Class<String> getTargetClass() { return String.class; }
}
It is added in conversion services
public class FlowConversationService extends DefaultConversionService {
protected void addDefaultConverters() {
super.addDefaultConverters();
this.addConverter(new StringTrimmerConverter());
}
}
Is there a way to disable this converter for password fields only?
I'm using spring webflow 2.3.2.RELEASE and spring 3.2.2.RELEASE.
You can force a specific converter with <binding> configuration.
Alternatively you can store password field in char[]. I had discussions with people which prefer this over String as char array will not get interned.
Related
I have a Spring Boot Controller -
#RestController
public class UserController {
#PostMapping
#ResponseStatus(CREATED)
public UserResponse register( #Valid #RequestBody UserRequest userRequest) {
//return ....
}
}
Below is UserRequest.java
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class UserRequest {
private String email;
//other property
}
I am sending below json in request body -
{
"email" : "TEST#Example.com",
//some other fields.
}
Sometime client send email in uppercase or in camel case so in userRquest I want to change value of email field to lowercase like test#example.com while de serializing to UserRequest Object.
Is there any easy way to do this. Can I introduce my own annotation like #ToLowerCase how I can create my own annotation and use that at field level in UserRequest.
There is no easy way just by introducing a new annotation #ToLowerCase,
because then you would also need to implement some annotation processor
for doing the real conversion work.
But you can achieve your goal in a slightly different way.
In your UserRequest class annotate the email property
with #JsonDeserialize and specify a converter there.
#JsonDeserialize(converter = ToLowerCaseConverter.class)
private String email;
You need to implement the converter class by yourself,
but it is easy by extending it from StdConverter.
public class ToLowerCaseConverter extends StdConverter<String, String> {
#Override
public String convert(String value) {
return value.toLowerCase();
}
}
Jackson will use the setter methods in your class.
Perform the conversion to lower case in the setter.
For example
public void setEmail(String newValue)
{
email = StringUtils.lowerCase(newValue);
}
StringUtils is an apache commons class.
You can make a general StringDeserializer and register it in ObjectMapper as shown below:-
StringDeserializer class
public final class StringDeserializer extends StdDeserializer<String> {
public StringDeserializer() {
super((Class<String>) null);
}
#Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonToken token = parser.getCurrentToken();
if (token == JsonToken.VALUE_STRING) {
String text = parser.getText();
return text == null ? null : text.toLowerCase().trim();
}
return null;
}
}
JacksonConfiguration class
#Configuration
public class JacksonConfiguration {
#Autowired
void mapper(ObjectMapper mapper) {
mapper.registerModule(initModule());
}
private Module initModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(String.class, new StringDeserializer());
return module;
}
}
The above code makes jackson deserialize all strings to lowercase and trimmed.
I'm stuck in an apparently simple problem: I want to perform some custom validation based on the object id in a PUT request.
#RequestMapping(value="/{id}", method=RequestMethod.PUT)
public ResponseEntity<Void> update(#Valid #RequestBody ClientDTO objDto, #PathVariable Integer id) {
Client obj = service.fromDTO(objDto);
service.update(obj);
return ResponseEntity.noContent().build();
}
I'd like to create a custom validator to output a custom message in case I update some field that can't be the same of another object in my database. Something like this:
public class ClientUpdateValidator implements ConstraintValidator<ClientUpdate, ClientDTO> {
#Autowired
private ClientRepository repo;
#Override
public void initialize(ClientInsert ann) {
}
#Override
public boolean isValid(ClientDTO objDto, ConstraintValidatorContext context) {
Client aux = repo.findByName(objDto.getName());
if (aux != null && !aux.getId().equals(objDto.getId())) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Already exists")
.addPropertyNode("name").addConstraintViolation();
return false;
}
return true;
}
}
However, the object id comes from #PathVariable, not from #RequestBody. I can't call "objDto.getId()" like I did above.
On the other hand, it doesn't make much sense to obligate to fill up the object id in the request body, because this way the path variable would become meaninless.
How can I solve this problem? Is there a way to inject the id from PathVariable into RequestBody object before bean validation is executed? If not, what would be a viable solution? Thanks.
Try to inject httpServletRequest into the custom validator
public class ClientUpdateValidator implements ConstraintValidator<ClientUpdate, ClientDTO> {
#Autowired
private HttpServletRequest request;
#Autowired
private ClientRepository repo;
#Override
public void initialize(ClientInsert ann) {
}
#Override
public boolean isValid(ClientDTO objDto, ConstraintValidatorContext context) {
// for example your path to put endpoint is /client/{id}
Map map = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String id = map.get("id");
Client aux = repo.findByName(objDto.getName());
if (aux != null && !aux.getId().equals(id)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Already exists")
.addPropertyNode("name").addConstraintViolation();
return false;
}
return true;
}
}
I'm trying to use something similar to org.springframework.cache.annotation.Cacheable :
Custom annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
}
Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
}
Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(key = "#id")
public Entity getEntity(Long id) {
return new Entity(id);
}
}
My IDE (IntelliJ) doesn't see anything special with the key = "#id" usage in contrast to similar usages for Cacheable where it's shown with different color than plain text. I'm mentioning the IDE part just as a hint in case it helps, it looks like the IDE is aware in advance about these annotations or it just realizes some connection which doesn't exist in my example.
The value in the checkEntity.key is '#id' instead of an expected number.
I tried using ExpressionParser but possibly not in the right way.
The only way to get parameter value inside the checkEntity annotation is by accessing the arguments array which is not what I want because this annotation could be used also in methods with more than one argument.
Any idea?
Adding another simpler way of doing it using Spring Expression. Refer below:
Your Annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String keyPath() default "";
}
Your Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(keyPath = "[0]")
public Entity getEntity(Long id) {
return new Entity(id);
}
#CheckEntity(keyPath = "[1].otherId")
public Entity methodWithMoreThanOneArguments(String message, CustomClassForExample object) {
return new Entity(object.otherId);
}
}
class CustomClassForExample {
Long otherId;
}
Your Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
Object[] args = joinPoint.getArgs();
ExpressionParser elParser = new SpelExpressionParser();
Expression expression = elParser.parseExpression(checkEntity.keyPath());
Long id = (Long) expression.getValue(args);
// Do whatever you want to do with this id
// This works for both the service methods provided above and can be re-used for any number of similar methods
}
}
PS: I am adding this solution because I feel this is a simpler/clearner approach as compared to other answers and this might be helpful for someone.
Thanks to #StéphaneNicoll I managed to create a first version of a working solution:
The Aspect
#Component
#Aspect
public class CheckEntityAspect {
protected final Log logger = LogFactory.getLog(getClass());
private ExpressionEvaluator<Long> evaluator = new ExpressionEvaluator<>();
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntity checkEntity) {
Long result = getValue(joinPoint, checkEntity.key());
logger.info("result: " + result);
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
private Long getValue(JoinPoint joinPoint, String condition) {
return getValue(joinPoint.getTarget(), joinPoint.getArgs(),
joinPoint.getTarget().getClass(),
((MethodSignature) joinPoint.getSignature()).getMethod(), condition);
}
private Long getValue(Object object, Object[] args, Class clazz, Method method, String condition) {
if (args == null) {
return null;
}
EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
return evaluator.condition(condition, methodKey, evaluationContext, Long.class);
}
}
The Expression Evaluator
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
/**
* Create the suitable {#link EvaluationContext} for the specified event handling
* on the specified method.
*/
public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
Method targetMethod = getTargetMethod(targetClass, method);
ExpressionRootObject root = new ExpressionRootObject(object, args);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
/**
* Specify if the condition defined by the specified expression matches.
*/
public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod == null) {
targetMethod = method;
}
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
}
The Root Object
public class ExpressionRootObject {
private final Object object;
private final Object[] args;
public ExpressionRootObject(Object object, Object[] args) {
this.object = object;
this.args = args;
}
public Object getObject() {
return object;
}
public Object[] getArgs() {
return args;
}
}
I think you probably misunderstand what the framework is supposed to do for you vs. what you have to do.
SpEL support has no way to be triggered automagically so that you can access the actual (resolved) value instead of the expression itself. Why? Because there is a context and as a developer you have to provide this context.
The support in Intellij is the same thing. Currently Jetbrains devs track the places where SpEL is used and mark them for SpEL support. We don't have any way to conduct the fact that the value is an actual SpEL expression (this is a raw java.lang.String on the annotation type after all).
As of 4.2, we have extracted some of the utilities that the cache abstraction uses internally. You may want to benefit from that stuff (typically CachedExpressionEvaluator and MethodBasedEvaluationContext).
The new #EventListener is using that stuff so you have more code you can look at as examples for the thing you're trying to do: EventExpressionEvaluator.
In summary, your custom interceptor needs to do something based on the #id value. This code snippet is an example of such processing and it does not depend on the cache abstraction at all.
Spring uses internally an ExpressionEvaluator to evaluate the Spring Expression Language in the key parameter (see CacheAspectSupport)
If you want to emulate the same behaviour, have a look at how CacheAspectSupport is doing it. Here is an snippet of the code:
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
/**
* Compute the key for the given caching operation.
* #return the generated key, or {#code null} if none can be generated
*/
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext(
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result);
}
I don't know which IDE you are using, but it must deal with the #Cacheable annotation in a different way than with the others in order to highlight the params.
Your annotation can be used with methods with more than 1 parameter, but that doesn't mean you can't use the arguments array. Here's a sollution:
First we have to find the index of the "id" parameter. This you can do like so:
private Integer getParameterIdx(ProceedingJoinPoint joinPoint, String paramName) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
String parameterName = parameterNames[i];
if (paramName.equals(parameterName)) {
return i;
}
}
return -1;
}
where "paramName" = your "id" param
Next you can get the actual id value from the arguments like so:
Integer parameterIdx = getParameterIdx(joinPoint, "id");
Long id = joinPoint.getArgs()[parameterIdx];
Of course this assumes that you always name that parameter "id". One fix there could be to allow to specify the parameter name on the annotation, something like
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
String paramName() default "id";
}
I'm trying to call Spring's ControllerLinkBuilder.methodOn() with a non-String type, which always fails. And I don't know which kind of Converter to use and where to register it.
Here's my Controller:
#RestController
#RequestMapping("/companies")
class CompanyController {
#RequestMapping(value="/{c}", method=RequestMethod.GET)
void getIt(#PathVariable Company c) {
System.out.println(c);
Link link = linkTo(methodOn(getClass()).getIt(c));
}
}
The System.out.println(c) works well. My Company Domain object get's fetched from DB. (I'm using DomainClassConverter)
But the other way doesn't work: ConverterNotFoundException: No converter found capable of converting from type #PathVariable Company to type String
Do I just need a Converter<Company, String>? And where should I register it? I tried something within the addFormatters(FormatterRegistry registry) method of WebMvcConfigurationSupport, but it did just display the same error. But after all I'm not sure what exactly I tried...
I had the same issue, it is a bug. If you don't want to do copy & paste on every controller you can try something like this in your WebMvcConfigurationSupport. It works for me.
#Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
try {
Class<?> clazz = Class.forName("org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor$BoundMethodParameter");
Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
field.setAccessible(true);
DefaultFormattingConversionService service = (DefaultFormattingConversionService) field.get(null);
for (Converter<?, ?> converter : beanFactory.getBeansOfType(Converter.class).values()) {
service.addConverter(converter);
}
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
Found a "solution". It requires a lot copy & paste from Spring's classes, but at least it works!
Basically I had to copy org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor and change two lines:
class AnnotatedParametersParameterAccessor {
...
static class BoundMethodParameter {
// OLD: (with this one you can't call addConverter())
// private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
// NEW:
private static final FormattingConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
...
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
...
// ADD:
CONVERSION_SERVICE.addConverter(new MyNewConverter());
}
...
}
This class get's used by ControllerLinkBuilderFactory. So I had to copy & paste that, too.
And this one get's used by ControllerLinkBuilder. Also copy & paste.
My Converter just does myDomainObject.getId().toString():
public class MyNewConverter implements Converter<Company, String> {
#Override
public String convert(Company source) {
return source.getId().toString();
}
}
Now you can use the copy&pasted ControllerLinkBuilder inside the controller and it works as expected!
I developed a framework to render links in spring hateoas and it supports annotated parameters (#PathVariable and #RequestParam) and arbitrary parameters types.
In order to render these arbitrary types you have to create a spring bean that implements com.github.osvaldopina.linkbuilder.argumentresolver.ArgumentResolver interface.
The interface has 3 methods:
public boolean resolveFor(MethodParameter methodParameter)
Is used to determine if the ArgumentResolver can be used to deal with the methodParameter. For example:
public boolean resolveFor(MethodParameter methodParameter) {
return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}
Defines that this ArgumentResover will be used for UserDefinedType.
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter)
Is used to include in the uriTemplate associated with the method the proper template parts. For example:
#Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
uriTemplateAugmenter.addToQuery("value1");
uriTemplateAugmenter.addToQuery("value2");
}
adds 2 query parameters (value1 and value2) to the uri template.
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames)
Sets in the template the values for the template variables. For example:
#Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
template.set("value1", ((UserDefinedType) parameter).getValue1());
}
else {
template.set("value1", "null-value");
}
if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
template.set("value2", ((UserDefinedType) parameter).getValue2());
}
else {
template.set("value2", "null-value");
}
}
gets the UserDefinedType instance and use it to sets the templates variables value1 and value2 defined in augmentTemplate method.
A ArgumentResolver complete example would be:
#Component
public class UserDefinedTypeArgumentResolver implements ArgumentResolver {
#Override
public boolean resolveFor(MethodParameter methodParameter) {
return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}
#Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
uriTemplateAugmenter.addToQuery("value1");
uriTemplateAugmenter.addToQuery("value2");
}
#Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
template.set("value1", ((UserDefinedType) parameter).getValue1());
}
else {
template.set("value1", "null-value");
}
if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
template.set("value2", ((UserDefinedType) parameter).getValue2());
}
else {
template.set("value2", "null-value");
}
}
}
and for the following link builder:
linksBuilder.link()
.withRel("user-type")
.fromControllerCall(RootRestController.class)
.queryParameterForUserDefinedType(new UserDefinedType("v1", "v2"));
to the following method:
#RequestMapping("/user-defined-type")
#EnableSelfFromCurrentCall
public void queryParameterForUserDefinedType(UserDefinedType userDefinedType) {
}
would generate the following link:
{
...
"_links": {
"user-type": {
"href": "http://localhost:8080/user-defined-type?value1=v1&value2=v2"
}
...
}
}
full config in spring boot. same as Franco Gotusso's answer just provide more detail.
```
/**
* This configuration file is to fix bug of Spring Hateoas.
* please check https://github.com/spring-projects/spring-hateoas/issues/118.
*/
#Component
public class MvcConfig extends WebMvcConfigurerAdapter {
#Autowired
private ApplicationContext applicationContext;
#Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
try {
Class<?> clazz = Class.forName("org.springframework.hateoas.mvc."
+ "AnnotatedParametersParameterAccessor$BoundMethodParameter");
Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
field.setAccessible(true);
DefaultFormattingConversionService service =
(DefaultFormattingConversionService) field.get(null);
for (Formatter<?> formatter : applicationContext
.getBeansOfType(Formatter.class).values()) {
service.addFormatter(formatter);
}
for (Converter<?, ?> converter : applicationContext
.getBeansOfType(Converter.class).values()) {
service.addConverter(converter);
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
```
For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?
For example:
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") String customerNumber) {
...
}
Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?
Thanks.
Spring does not support #javax.validation.Valid on #PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.
Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.
You can use like this:
use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.
*
* Variant of JSR-303's {#link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
step.1 init ValidationConfig
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
return processor;
}
}
step.2 Add #Validated to your controller handler class, Like:
#RequestMapping(value = "poo/foo")
#Validated
public class FooController {
...
}
step.3 Add validators to your handler method:
#RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public ResponseEntity<Foo> delete(
#PathVariable("id") #Size(min = 1) #CustomerValidator int id) throws RestException {
// do something
return new ResponseEntity(HttpStatus.OK);
}
final step. Add exception resolver to your context:
#Component
public class BindExceptionResolver implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex.getClass().equals(BindException.class)) {
BindException exception = (BindException) ex;
List<FieldError> fieldErrors = exception.getFieldErrors();
return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
}
}
}
The solution is simple:
#GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(#PathVariable(value = "hash", required = false) String historyHash)
{
// Accepted requests: either "/" or "/{40 character long hash}"
}
And yes, PathVariables are ment to be validated, like any user input.
Instead of using #PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:
#RestController
#RequestMapping("/user")
public class UserController {
#GetMapping("/{id}")
public void get(#Valid GetDto dto) {
// dto.getId() is the path variable
}
}
And the bean contains the actual validation rules:
#Data
public class GetDto {
#Min(1) #Max(99)
private long id;
}
Make sure that your path variables ({id}) correspond to the bean fields (id);
#PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:
a bug generated a bad url (an href in jsp for example). No #Valid is
needed and no message is needed, just fix the code;
"the user" is manipulating the url.
Again, no #Valid is needed, no meaningful message to the user should
be given.
In both cases just leave an exception bubble up until it is catched by
the usual Spring ExceptionHandlers in order to generate a nice
error page or a meaningful json response indicating the error. In
order to get this result you can do some validation using custom editors.
Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)
public class CustomerNumber implements CharSequence {
private String customerNumber;
public CustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
#Override
public String toString() {
return customerNumber == null ? null : customerNumber.toString();
}
#Override
public int length() {
return customerNumber.length();
}
#Override
public char charAt(int index) {
return customerNumber.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return customerNumber.subSequence(start, end);
}
#Override
public boolean equals(Object obj) {
return customerNumber.equals(obj);
}
#Override
public int hashCode() {
return customerNumber.hashCode();
}
}
Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)
public class CustomerNumberEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
setValue(new CustomerNumber(text));
} else {
throw new IllegalArgumentException();
// you could also subclass and throw IllegalArgumentException
// in order to manage a more detailed error message
}
}
#Override
public String getAsText() {
return ((CustomerNumber) this.getValue()).toString();
}
}
Register the editor in the Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
// ... other editors
}
Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") CustomerNumber customerNumber) {
...
}
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations?
To validate path variable you should use this approach Problem validating #PathVariable url on spring 3 mvc
Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below
#RequestMapping(value = "/number")
#ResponseBody
public ResponseObject searchByNumber() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
}