1.1.14.4.2.5. fejezet, Hibernate annotáció és validáció

Annotáció alapú ellenőrzéshez a több nyelvű hibaüzeneteket az alábbi parancsal készíthetjük el:

 native2ascii -encoding UTF-8 ValidationMessages_hu_HU-utf8.properties ValidationMessages_hu_HU.properties

Ennek a körülményességnek az oka, hogy egyenlőre kizárólag ISO8859-1 karakterkészletű properties fájlokat kezel a Hibernate Validator. A native2ascii paranccsal ASCII formába konvertálhatók az amúgy UTF-8 kódolású üzenetek, és Unicode vezérlőkarakterekkel válnak a beállított nyelvnek megfelelően ékezetes karakterekké. Ez a kis program az aktuális JDK bin könyvtárában található. A böngészőben beállított nyelven jelennek meg a hibaüzenetek, amik interpolációval készülnek. Ehhez egy interpolátor osztályt kell létrehoznunk.

package com.integrity.i18n;
 
import java.util.Locale;
 
import javax.validation.MessageInterpolator;
 
public class ClientLocaleMessageInterpolator implements MessageInterpolator {
 
    private final MessageInterpolator delegate;
    private Locale locale = null;
 
    public ClientLocaleMessageInterpolator(MessageInterpolator delegate) {
      this.delegate = delegate;
    }
 
    @Override
    public String interpolate(String message, Context context) {
      return this.interpolate(message, context, locale);
    }
 
    @Override
    public String interpolate(String message, Context context, Locale locale)
    {
      return delegate.interpolate(message, context, locale);
    }
 
    public void setLocale(Locale locale){
        this.locale = locale;
    }
}

A kiválasztott nyelvet egy session bean tárolja.

package com.integrity.i18n;
 
import java.io.Serializable;
import java.util.Locale;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
 
@ManagedBean
@SessionScoped
public class LocaleBean implements Serializable {
 
    private static final long serialVersionUID = 5503640713110816880L;
 
    private Locale locale;
 
    public static final String DEFAULT_LOCALE="hu";
 
    public LocaleBean() {
        FacesContext context = FacesContext.getCurrentInstance();
        UIViewRoot root = context.getViewRoot();
        if (root != null) {
            locale = root.getLocale();
        } else {
            locale = new Locale(DEFAULT_LOCALE);
        }
    }
 
    public Locale getLocale() {
        return locale;
    }
 
    public String getLanguage() {
        return locale.getLanguage();
    }
 
    public void setLanguage(String language) {
        locale = new Locale(language);
        FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);
    }
 
}

A standard.xhtml-ben (alapértelmezett sablon) a

<f:view contentType="text/html" locale="#{localeBean.locale}">

érvényesíti a beállítást.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:p="http://primefaces.org/ui">
<f:view contentType="text/html" locale="#{localeBean.locale}">
<h:head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<title>JSF 2, Spring Web Flow, and PrimeFaces Showcase</title>
	<link rel="stylesheet" href="${request.contextPath}/app/resources/styles/blueprint/screen.css" type="text/css" media="screen, projection" />
	<link rel="stylesheet" href="${request.contextPath}/app/resources/styles/blueprint/print.css" type="text/css" media="print" />
	<ui:insert name="headIncludes"/>
</h:head>
<h:body>
<div class="container">
<h:outputLink value="${request.contextPath}/app/service-request" >
  <h:outputText value="Service request home" />
</h:outputLink><br/>
<h:outputLink value="${request.contextPath}/app/home" >
  <h:outputText value="Home" />
</h:outputLink><br/>
  <div>
    <h1>JSF 2, PrimeFaces, and Spring Web Flow</h1>
    <h3 class="alt">
      <ui:insert name="title"/>
    </h3>
    <hr/>
  </div>
  <div>	
    <ui:insert name="notes"/>
  </div>
  <div>
    <ui:insert name="content"/>
  </div>
</div>
</h:body>
</f:view>
</html>

A programban a validáció és a hibajelzés az alábbiak szerint működik

public class RequestDAOImpl implements RequestDAO {
  private LocaleBean localeBean;
 
  public void setMyLocale(LocaleBean localeBean){
        this.localeBean = localeBean;
  }
 
  public boolean validate(Request request) throws ConstraintViolationException {
        Configuration config = Validation.byDefaultProvider().configure();
        ClientLocaleMessageInterpolator interpolator = new ClientLocaleMessageInterpolator(config.getDefaultMessageInterpolator());
        interpolator.setLocale(localeBean.getLocale());
        config = config.messageInterpolator(interpolator);
        Validator localValidator = config.buildValidatorFactory().getValidator();
 
        Set constraintViolations = localValidator.validate(request);
 
        for(ConstraintViolation<Request> constraint : (Set<ConstraintViolation<Request>>)constraintViolations) {
            log.error(constraint.getMessage());
            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR,constraint.getMessage(), ""));  
        }
        return constraintViolations.isEmpty();
    }

Itt a Request egy saját osztály, tehát nem a HttpRequest-ből örökített. Saját annotációi közül az összetett osztály szintű annotációk szúrhatnak szemet első látásra (FieldMatch)

package com.integrity.domain;
 
import java.io.Serializable;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
 
/*
import org.hibernate.validator.Email;
import org.hibernate.validator.Length;
import org.hibernate.validator.NotEmpty;
import org.hibernate.validator.NotNull;
*/
 
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.FieldMatch;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
/* packages for validateEdit method
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.validation.ValidationContext;
//*/
 
@Entity
@Table(name="request")
@XmlRootElement
//*
@FieldMatch.List({
    @FieldMatch(first = "discount", second = "discountReason", message = "{com.integrity.validation.discountReason.isEmpty.message}"),
    @FieldMatch(first = "operationType", second = "serviceId", message = "{com.integrity.validation.serviceid.isEmpty.message}")
})
//*/
public class Request implements Serializable {
    private static final long serialVersionUID = -3828086455549895099L;
 
    private Long id;
    private String name;
    private String phone;
    private String email;
    private String organization;
    private String subject;
    private String operationType;
    @Transient
    private String operatoinTypeLabel;
    private String serviceId;
    private String homepage;
    private String applicantType;
    @Transient
    private String applicantTypeLabel;
    private String startTime;
    @Transient
    private String startTimeLabel;
    private Boolean discount;
    private String discountReason;
    private String comment;
 
    public static String CONTRACTION_YES = "I";
    public static String CONTRACTION_NO = "N";
    public static String CONTRACTION_TRUE = "true";
    public static String CONTRACTION_FALSE = "false";
    public static String UNIFIED_YES_HU = "Igen";
    public static String UNIFIED_NO_HU = "Nem";
    public static String SERVICE_GROW = "B";
    public static String SERVICE_SWITCH = "L";
    public static String SERVICE_NEW = "U";
 
    /**
     * @return the id
     */
    @Id
    @GeneratedValue
    @Column(name="ID")
    @XmlAttribute
    public Long getId() {
        return id;
    }
    /**
     * @param id the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }
    /**
     * @return the nev
     */
    @Column(name="name")
    @Length(max = 64)
    @XmlElement
    @NotNull
    @NotEmpty
    public String getName() {
        return name;
    }
    /**
     * @param name the nev to set
     */
    public void setName(String name) {
        this.name = name;
    }
    /**
     * @return the telefonszam
     */
    @Column(name="phone")
    @XmlElement
    @NotNull
    @NotEmpty
    public String getPhone() {
        return phone;
    }
    /**
     * @param phone the telefonszam to set
     */
    public void setPhone(String phone) {
        this.phone = phone;
    }
    /**
     * @return the email
     */
    @Email
    @Column(name="email")
    @XmlElement
    @NotNull
    @NotEmpty
    public String getEmail() {
        return email;
    }
    /**
     * @param email the email to set
     */
    public void setEmail(String email) {
        this.email = email;
    }
    /**
     * @return the szervezetnev
     */
    @Column(name="organization")
    @XmlElement
    @NotNull
    @NotEmpty
    public String getOrganization() {
        return organization;
    }
    /**
     * @param szervezetnev the szervezetnev to set
     */
    public void setOrganization(String organization) {
        this.organization = organization;
    }
    /**
     * @return the targy
     */
    @Column(name="subject")
    @XmlElement
    public String getSubject() {
        return subject;
    }
    /**
     * @param subject the targy to set
     */
    public void setSubject(String subject) {
        this.subject = subject;
    }
    /**
     * @return the muveletjelleg
     */
    @Column(name="operation_type")
    @XmlElement
    @NotNull
    @NotEmpty
    public String getOperationType() {
        return operationType;
    }
    /**
     * @param operationType the muveletjelleg to set
     */
    public void setOperationType(String operationType) {
        this.operationType = operationType;
    }
    /**
     * @return the serviceID
     */
    @Column(name="service_id")
    @XmlElement
    public String getServiceId() {
        return serviceId;
    }
    /**
     * @param serviceId the serviceId to set
     */
    public void setServiceId(String service_id) {
        this.serviceId = service_id;
    }
    /**
     * @return the honlap
     */
    @Column(name="homepage")
    @XmlElement
    public String getHomepage() {
        return homepage;
    }
    /**
     * @param homepage the honlap to set
     */
    public void setHomepage(String homepage) {
        this.homepage = homepage;
    }
    /**
     * @return the kerelmezojelleg
     */
    @Column(name="applicant_type")
    @XmlElement
    @NotNull
    @NotEmpty
    public String getApplicantType() {
        return applicantType;
    }
    /**
     * @param applicantType the kerelmezojelleg to set
     */
    public void setApplicantType(String applicantType) {
        this.applicantType = applicantType;
    }
    /**
     * @return the szolgaltatasido
     */
    @Column(name="start_time")
    @XmlElement
    @NotEmpty
    @NotNull
    public String getStartTime() {
        return startTime;
    }
    /**
     * @param startTime the szolgaltatasido to set
     */
    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }
    /**
     * @return the kedvezmeny
     */
    @Column(name="discount")
    @XmlElement
    public Boolean getDiscount() {
        return discount;
    }
 
    public void setDiscount(Boolean discount) {
        this.discount = discount;
    }
 
    /**
     * @return the kedvezmenyoka
     */
    @Column(name="discount_reason")
    @XmlElement
    public String getDiscountReason() {
        return discountReason;
    }
    /**
     * @param discountReason the kedvezmenyoka to set
     */
    public void setDiscountReason(String discountReason) {
        this.discountReason = discountReason;
    }
    /**
     * @return the megjegyzes
     */
    @Column(name="comment")
    @XmlElement
    public String getComment() {
        return comment;
    }
    /**
     * @param comment the megjegyzes to set
     */
    public void setComment(String comment) {
        this.comment = comment;
    }
 
    @XmlElement
    @Transient
    public String getOperationTypeLabel() {
        return operatoinTypeLabel;
    }
 
    public void setOperationTypeLabel(String muveletjellegLabel) {
        this.operatoinTypeLabel = muveletjellegLabel;
    }
 
    @XmlElement
    @Transient
    public String getApplicantTypeLabel() {
        return applicantTypeLabel;
    }
 
    public void setApplicantTypeLabel(String applicantTypeLabel) {
        this.applicantTypeLabel = applicantTypeLabel;
    }
 
    @XmlElement
    @Transient
    public String getStartTimeLabel() {
        return startTimeLabel;
    }
 
    public void setStartTimeLabel(String startTimeLabel) {
        this.startTimeLabel = startTimeLabel;
    }
 
    @XmlElement
    @Transient
    public String getDiscountLabel() {
        return ((discount!=null) && discount ? UNIFIED_YES_HU : UNIFIED_NO_HU);
    }
/*  NotACleanCode + uses Messages.properties  
    public void validateEdit(ValidationContext validationContext) {
        if ((discount!=null) && discount && discountReason.isEmpty()) {
            MessageContext messageContext = validationContext.getMessageContext(); 
            messageContext.addMessage(new MessageBuilder().error().code("discountReason.isEmpty").build());
        }
 
        if ((operationType.equals(SERVICE_GROW) || operationType.equals(SERVICE_SWITCH)) && serviceId.isEmpty()) {
            MessageContext messageContext = validationContext.getMessageContext(); 
            messageContext.addMessage(new MessageBuilder().error().code("serviceid.isEmpty").build());
        }
 
    }
//*/  
}

A validateEdit magával hozta a Messages_xx_XX.properties kezelésének lehetőségét, ami UTF-8 karakterkészlet használatát is eredményezte. A Spring servlet-context.xml-ben konfiguráltam is, de ez igen távol áll az igazi annotációs érték ellenőrzéstől.

<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
  <property name="basename" value="classpath:Messages"/>
  <property name="defaultEncoding" value="UTF-8"/>
</bean>

A validateEdit funkció már közelített a megoldáshoz, de még mindig külön fájlból emelte be a hibaüzenetet, és a tiszta kód szabályai szerint a validáció nem implementálható közvetlenül a domain-be.

A FieldMatch annotáció osztálya pedig az alábbi (interface majd az implementáció)

package org.hibernate.validator.constraints;
 
 
import javax.validation.Constraint;
import javax.validation.Payload;
 
import org.hibernate.validator.constraints.FieldMatchValidator;
 
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
 
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch {
 
    String message() default "{constraints.fieldmatch}";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    String first();
 
    String second();
 
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
 
}
package org.hibernate.validator.constraints;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
import org.apache.commons.beanutils.BeanUtils;
 
import com.integrity.domain.Request;
 
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
 
    private String firstFieldName;
    private String secondFieldName;
 
    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }
 
    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
            if (firstObj == null || secondObj == null){
                return false;
            }
            if (firstFieldName.equalsIgnoreCase("operationType")){
              return firstObj.equals(Request.SERVICE_NEW) || (!secondObj.toString().isEmpty() && (firstObj.equals(Request.SERVICE_SWITCH) || firstObj.equals(Request.SERVICE_GROW)));  
            } else if (firstFieldName.equalsIgnoreCase("discount")) {
              return firstObj.toString().equalsIgnoreCase("false") || (!secondObj.toString().isEmpty() && firstObj.toString().equalsIgnoreCase("true"));
            }
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
 
}

Nem túl szerencsés, hogy két feltételt is a mezőnevek alapján vizsgál, de kísérleti megoldásról van szó, ezért talán elnézhető a lazaság.