piątek, 16 maja 2014

Spring MVC i komunikaty walidacji JSR-303

Człowiek potrzebował zrobić trywialną rzecz, komunikaty o błędach walidacji JSR 303 trzymać w tym samym MessageSource, co inne propertiesy aplikacji, oraz korzystać ze springowego DefaultMessageCodesResolver. A skończyło się jak zwykle na deep-debugging...

Może od razu przykład. Dla klasy:
public class Dorosly {
 @Range(min = 18, max = 100)
 private Integer wiek;
}
Definicja komunikatu może wyglądać tak:
org.hibernate.validator.constraints.Range.message = pole musi byc z przedzialu {min} - {max}

Żeby w wygenerowanym komunikacie pojawiły się wartości atrybutów pochodzące z adnotacji, trzeba skonfigurować MessageInterpolatora, co było już poruszanie nie raz i jest dość dobrze opisane tutaj.
W wersja w xml, może to wyglądać tak:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
 <property name="defaultEncoding" value="UTF-8"/>
 <property name="basenames">
  <list>
    <value>classpath:/web</value>
  </list>
 </property>
</bean>  

 
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
 <property name="validationMessageSource" ref="messageSource" />
</bean>

Na pierwszy rzut oka nie widać tutaj żadnego interpolatora, ale wystarczy zajrzeć do settera od validationMessageSource i wszystko staje się jasne. A co jeśli chciałbym teraz skorzystać z wspomnianego DefaultMessageCodesResolver? Definując sobie taki properties:
Range.java.lang.Integer = liczba całkowita z przedziału {min} - {max}
Dostajemy jakże przyjemny wyjątek:
java.lang.NumberFormatException: For input string: "min"
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 at java.lang.Integer.parseInt(Integer.java:492)
 at java.lang.Integer.parseInt(Integer.java:527)
 at java.text.MessageFormat.makeFormat(MessageFormat.java:1418)
 at java.text.MessageFormat.applyPattern(MessageFormat.java:479)
 at java.text.MessageFormat.(MessageFormat.java:381)
 at org.springframework.context.support.MessageSourceSupport.createMessageFormat(MessageSourceSupport.java:159)
 at org.springframework.context.support.ReloadableResourceBundleMessageSource$PropertiesHolder.getMessageFormat(ReloadableResourceBundleMessageSource.java:656)
 at org.springframework.context.support.ReloadableResourceBundleMessageSource.resolveCode(ReloadableResourceBundleMessageSource.java:294)
 at org.springframework.context.support.AbstractMessageSource.getMessageInternal(AbstractMessageSource.java:205)
 at org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:146)
 at org.springframework.context.support.DelegatingMessageSource.getMessage(DelegatingMessageSource.java:71)
 at org.springframework.context.support.AbstractApplicationContext.getMessage(AbstractApplicationContext.java:1238)
 at org.springframework.web.servlet.support.RequestContext.getMessage(RequestContext.java:577)
 at org.springframework.web.servlet.support.BindStatus.initErrorMessages(BindStatus.java:177)
 at org.springframework.web.servlet.support.BindStatus.getErrorMessages(BindStatus.java:273)
 at org.springframework.web.servlet.tags.form.ErrorsTag.exposeAttributes(ErrorsTag.java:174)
W czym problem? Ano w tym, że tag <form:errors/> nie korzysta wcale ze wspomnianego interpolatora i bazuje na tym, że póki nie dodamy kluczy:
  • Range.doroslyForm.wiek, 
  • Range.wiek, 
  • Range.java.lang.Integer, 
  • Range
to będzie brana domyślna wartość komunikatu, która wygenerowana jest przez validation api. Jeśli natomiast pojawi się któryś z wymienionych kluczy, korzystający z np. {min}, {max} dostajemy właśnie taki wyjątek. Na szczęście jest obejście, a nawet kilka:
  1. Bazując na tym zgłoszeniu, można zredagować propertiesa, na coś co spring będzie umiał sparsować, czyli: 
    Range.java.lang.Integer = liczba całkowita z przedziału {2} - {1}
    Kolejność argumentów odpowiada kolejności alfabetycznej nazwy atrybutów adnotacji (max = 1, min = 2).
  2. Zdefiniować komunikat bezpośrednio przy polu: 
    @Range(min = 18, max = 100, message="liczba całkowita z przedziału \\{min\\} - \\{max\\}")
    
  3. Zdefiniować osobnego propetiesa, którego klucz nie pokrywa się z tymi z resolvera:
    @Range(min = 18, max = 100, message="{liczba.calkowita.od.do}")
    

Brak komentarzy:

Publikowanie komentarza