Как в spring написать валидатор использующий коды сообщений и не забыть кого-то из них

Введение в суть вопроса

Я продолжаю выкладывать на всеобщее обозрение ряд своих утилиток помогающих и упрощающих разработку. Сегодня я расскажу об инструменте, который наверняка пригодится всем тем кто пользуется spring. Предположим что вы создаете некоторый контроллер, например, такой:

<bean name="loginController" class="blz.server.controllers.LoginController">
    <property name="requireSession" value="true"/>
    <property name="supportedMethods" value="GET,POST"/>
    <property name="formView" value="loginStarted"/>
    <property name="commandClass" value="blz.model.dto.LoginDTO"/>
    <property name="commandName" value="login"/>
    <property name="validator">
        <bean class="blz.model.dto.validators.ValidateLoginDTO"/>
    </property>
</bean>

Как видите здесь я задекларировал, что входным параметром к контроллеру будет класс "blz.model.dto.LoginDTO". Для проверки того поля в форме были указаны верно, без ошибок, в spring используется validator, на примере это, blz.model.dto.validators.ValidateLoginDTO.

Сам код класса валидатора может выглядеть примерно так:

public class ValidateLoginDTO implements Validator {

   
public boolean supports(Class clazz) {
       
return clazz.isAssignableFrom(LoginDTO.class);
   
}

   
public void validate(Object target, Errors errors) {
       
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, "validation.failed.noemail.for.login");
        ValidationUtils.rejectIfEmptyOrWhitespace
(errors, LoginDTO.F_USERPASSWORD, "validation.failed.nopass.for.login");
   
}
}

Основное внимание на вызов метода "ValidationUtils.rejectIfEmptyOrWhitespace". Здесь я проверяю внутри входного объекта-комманды условие, такое чтобы значение поля email и пароль были заполненными. В случае если это не так, то в объект Errors помещается код ошибки "validation.failed.noemail.for.login" и "validation.failed.nopass.for.login".

Эти коды не берутся из ниоткуда: я должен создать файл properties, например, "/WEB-INF/localization/register_and_login.properties" и поместить в него следующий текст:

validation.failed.noemail.for.login=Ошибка, вы не указали ваш email. Вход в систему не возможен.
validation.failed.nopass.for.login=Ошибка, вы не указали ваш пароль. Вход в систему не возможен.

Это еще не все, теперь последний шаг - регистрация ресурсов внутри контекста spring:

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <value>/WEB-INF/localization/register_and_login</value>
        </list>
    </property>
    <property name="defaultEncoding" value="windows-1251"/>
    <property name="fallbackToSystemLocale" value="true"/>
</bean>

На этом все.

Надо сказать что подобный многошаговый процесс меня сильно раздражает т.к. способствует появлению ошибок в основном из-за неаккуратности или спешки (ну-ну, разве может быть иначе?). Несмотря на это изменить шаги, убрать, поменять местами нельзя, т.к. ничего лучшего не придумывается, а если даже и придумается то врядли этот самокат будет плавно входить в общую систему spring. Однако это не значит, что нельзя упростить для себя любимого процесс проверки наличия в файла свойств нужных для работы валидаторов кодов сообщений. Для этого я решил добавить поддержку специальной аннотации. Вот ее код:

/**
* Аннотация для маркировки тех классов, которые хотят использовать услуги
* проверки их на наличие корректных кодов сообщений
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ValidationBundle {
  
/**
    * Базовое имя ResourceBundle
    *
    *
@return
   
*/
  
String value();
  
  
/**
    * Список локалей, для которых нужно выполнить проверку
    *
    *
@return
   
*/
  
String[] locales () default {};
  
  
  
/**
    * Кодировка файла с ресурсами, ну так получилось, что делать ...
    *
    *
@return
   
*/
  
String encoding () default "ISO8859-1";
  
  
  
  
@Retention(RetentionPolicy.RUNTIME)
  
@Target({ElementType.FIELD} )
  

  
/**
    * Еще один маркер, но уже не для класса, а для конкретных его полей
    */
  
public static @interface Marker {}
}

Теперь я вернусь к файлу валидатора и немного его перепишу:

@SuppressWarnings ({"inchecked", "unsafe"})
@ValidationBundle(value="/WEB-INF/localization/register_and_login", locales = {"RU_ru"}, encoding="windows-1251")
public class ValidateLoginDTO implements Validator {

   
@ValidationBundle.Marker
   
private static final String VALIDATION_FAILED_NOEMAIL_FOR_LOGIN = "validation.failed.noemail.for.login";
   
@ValidationBundle.Marker
   
private static final String VALIDATION_FAILED_NOPASS_FOR_LOGIN = "validation.failed.nopass.for.login";

   
public boolean supports(Class clazz) {
       
return clazz.isAssignableFrom(LoginDTO.class);
   
}

   
public void validate(Object target, Errors errors) {
       
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, VALIDATION_FAILED_NOEMAIL_FOR_LOGIN);
        ValidationUtils.rejectIfEmptyOrWhitespace
(errors, LoginDTO.F_USERPASSWORD, VALIDATION_FAILED_NOPASS_FOR_LOGIN);
   
}
}

Как видите изменений немного: в самом начале файла я выполнил маркировку класса валидатора как использующего коды сообщений расположенные внутри ResourceBundle с базовым именем "/WEB-INF/localization/register_and_login". Указал для каких локалей необходимо выполнить сканирование, и указал кодировку файла с ресурсами (дело в том, что родные для java ResourceBundle загружают данные из файлов в кодировке ISO8859-1, а для spring можно явно указать кодировку, например, windows-1251 и это гораздо удобнее чем мучаться с утилитой native2ascii.exe).

Все поля хранящие в себе значение кода сообщения я промаркировал аннотацией "@ValidationBundle.Marker".

Что дальше?


Теперь есть два варианта как запустить проверку spring-контекста и ваших классов на корректность. Во-первых, и это наиболее удобный для меня способ это создать ant-скрипт, частью которого является создание отчета об найденных ошибках. Для этого я создал ant-скрипт.

<?xml version="1.0"?>
<project default="makeanotest">
 
 
    <target name="makeanotest">
 
        <path id="library.spring.classpath">
            <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/dist">
                <include name="**/*.jar"/>
            </fileset>
        </path>
 
        <path id="library.velocity.classpath">
            <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/velocity">
                <include name="**/*.jar"/>
            </fileset>
        </path>
 
        <path id="library.spring-all-libs.classpath">
            <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/">
                <include name="**/*.jar"/>
            </fileset>
        </path>
 
 
        <path id="library.ant.classpath">
            <fileset dir="E:/Program_Files_2/Libraries/ant/lib/">
                <include name="**/*.jar"/>
            </fileset>
        </path>
 
 
        <path id="library.commons.classpath">
            <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/jakarta-commons/">
                <include name="**/*.jar"/>
            </fileset>
        </path>
 
        <dirname property="base" file="${ant.file}"/>
 
        <path id="project.sourcepath">
            <dirset dir="${base}/src">
                <include name="**/*.java"/>
                <exclude name="com/**"/>
                <exclude name="org/**"/>
                <exclude name="javax/**"/>
            </dirset>
        </path>
 
 
        <path id="project.classpath" location="${base}/out/production/sitegraph/"/>
 
        <path id="all.classpath">
            <path refid="library.spring.classpath"/>
            <path refid="library.spring-all-libs.classpath"/>
            <path refid="library.ant.classpath"/>
            <path refid="project.classpath"/>
        </path>
 
        <!--
        Создаю новую цель
        -->
        <taskdef name="scananno"
                 classname="blz.server.utils.anno.AntTaskAnnoScan"
                 classpathref="all.classpath"
                />
 
        <property name="browser" location=
                "C:/Program Files/Internet Explorer/iexplore.exe"/>
 
        <property name="file-1" location="${base}/out/report-anno-1.html"/>
        <property name="file-2" location="${base}/out/report-anno-2.html"/>
 
        <!--
        Вот пример использования новой задачи.
        В качестве параметров я указываю будет ли завершаться скрипт ant аварийно, если
        все нужные ресурсы не были найдены
        далее я указываю путь к файлу, куда должен быть помещен результат в виде html-отчета
        затем я указываю путь к исходному файлу spring
        и наконец признак того как будут запрашиваться нужные ресурсы
        в случае если признак useContext равен true то ресурсы будут загружаться из самого spring
        -->
        <scananno
                failOnErrors="false"
                targetFileName="${file-1}"
                pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml"
                useContext="false"
                pathToBaseResourcesDir="${base}/web/"
                />
        <!--
        Теперь, после создания отчета, нужно его показать в браузере
        -->
        <exec executable="${browser}" spawn="true">
            <arg value="${file-1}"/>
        </exec>
 
        <!--
        В этом случае к параметрам показанным в прошлом примере добавляются еще два:
        признак того что загрузка должна вестись не из контекста а напрямую с помощью ResourceBundle
        а также путь к корню каталога относительно которого будут искаться файлы properties
        -->
        <scananno
                failOnErrors="true"
                targetFileName="${file-2}"
                pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml"
                useContext="false"
                pathToBaseResourcesDir="${base}/web/"
                />
 
    </target>
</project>

Как видите, я создал новую пользовательскую задачу - scananno - передав ей в качестве параметров путь к файлу, куда должен быть помещен результат в виде html-отчета, затем путь к исходному файлу spring и, наконец, признак того как будут запрашиваться нужные ресурсы (подробнее об этих режимах рассказано в комментариях).

После запуска сценария я получу такой html-отчет:

Изображение:anno_1_1.png

Надо сказать, что внешний вид отчета можно гибко настраивать: в файлах с исходниками проекта вы найдете velocity-шаблон для выходного отчета (если вы не знаете что такое velocity, то скоро я опубликую серию статей посвященную ему).

Теперь про второй вариант использования. В этом случае в контекст spring добавляется новый bean, который зарегистрирован в случае получения события "Контекст обновлен" выполнить его сканирование и сформировать отчет.

<bean id="annoBean" class="blz.server.utils.anno.AnnoBean">
    <constructor-arg index="0" value="report-anno.html" />
</bean>

При запуске контекста, автоматически будет вызван этот bean, который и сформирует html-файл с отчетом.

В качестве единственного параметра к конструктору нужно передать путь к файлу, куда будет помещен сформированный html-отчет об найденных ошибках. В случае если путь не абсолютный, а относительный (не начинается с file:), то нужно каким-то образом сообщить об "базе" файла. Класс бина (AnnoBean) реализует интерфейс ServletContextAware. А значит что если все пошло хорошо то ему сообщат о местоположении корня веб-приложения и относительный путь будет отсчитываться от него.

Источник — http://black-zorro.com/mediawiki/Как_в_spring_написать_валидатор_использующий_коды_сообщений_и_не_забыть_кого-то_из_них