Tuesday, November 10, 2015

PrimeFaces'da İstemci Tarafta Doğrulama (Client-Side Validation)

Web uygulamaları, çok katmanlı mimari olarak tasarlanırlar. Her bir katmanın bir sorumluluğu vardır. Çok katmanlı yapıdaki her bir katman, kendinden önceki katmana hizmet verirken, kendinden sonraki katmandan hizmet alır. Şekil-1'de bu katmanlı yapıda yer alan 5 katman tanıtılmıştır:
  • İ: İstemci katmanı. Çoğu zaman bir web tarayıcısıdır.
  • S: Sunum katmanı. Uygulamanın kullanıcıya dönük yüzünü oluşturmaktan sorumludur. Sunum mantığını içerir.
  • İM: İş Mantığı katmanı. İşin asıl yapıldığı yerdir.
  • T: Tümleştirme katmanı. Kurumsal uygulamalar geliştirilirken çoğu zaman kurum içindeki ya da dışındaki diğer yazılımlarla etkileşim kurması istenir. Tümleştirme katmanı bu iletişimin sorunsuz gerçekleşmesinden sorumludur.
  • K: Kaynak katmanı. Verilerin kalıcı olarak saklandığı katmandır. Veritabanı (Oracle, MySQL, DB2 gibi), Dizin sunucusu (Active Directory, OpenLDAP), CRM (Oracle Siebel, Microsoft Dynamics), ERP uygulaması gibi çok farklı türde olabilir.
Şekil-1 Web uygulamalarında katmanlar
İstemci ile sunucu arasındaki etkileşim istek-cevap örüntüsündedir. Uygulamanın arayüzünün değişmesi için mutlaka sunucuya bir isteğin gitmesi gerekir.
Klasik Web uygulamalarında, tüm sorumluluk sunucu taraftadır. Her türlü işlem sunucu tarafta gerçekleştirilir. İstemcinin (Web tarayıcısının) herhangi bir sorumluluğu bulunmaz. Bu nedenle Web tarayıcısına herhangi bir eklenti kurulmasına da gerek bulunmaz. Bu nedenle web tarayıcısı, süper ince istemci olarak isimlendirilir:
Şekil-2 Klasik Web mimarisi
Bu mimaride, istemcideki arayüzde gerçekleşen her değişilik için bir isteğin (HTTP İsteği) sunucuya gitmesi, bu isteğin işlenmesi ve tarayıcıda gözükecek arayüzün üretilmesi ve cevabın (HTTP Cevabı) istemciye gönderilmesi gerekir. HTTP cevabı içinde temel olarak HTML ve CSS yer alır. Bu çalışma şekli, sunucunun üzerinde çok fazla yük yaratır ve sunucunun ölçeklenebilirliği üzerindeki en önemli kısıtı oluşturur. Burada Ajax teknolojisi sayesinde sunucu üzerindeki yük biraz azalmıştır. Ajax kullanıldığında, arayüzdeki değişiklik için yine bir istek gidecektir. Ancak istek asenkron olarak gönderilirken, cevap gelinceye kadar geçen sürede kullanıcı uygulama ile etkileşmeye devam edecektir. Ayrıca, sunucu arayüzün tamamını değil, değişikliği oluşturmak için vakit harcayacaktır:
Şekil-3 Ajax teknolojisinin kullanıldığı durum
Sunum katmanında uygulama geliştirirken, Model-View-Controller (MVC) mimari kalıbından yararlanıyoruz. MVC uzun süredir bildiğimiz mimari bir çözüm. Modeli, alan verisini saklayan bir Java sınıfı olarak kodluyoruz. Bu modelin arayüzde nasıl görüntüleneceğini View kontrol ediyor. Kullanıcı ise Controller ile etkileşim kuruyor. Controllermodeli oluşturmak ya da güncellemekdoğrulama yapmak, işi asıl yapacak olan İş Mantığı katmanına ihale etmek ve sunumu yapmak üzere View bileşenine aktarmaktan sorumludur. Sunucu tarafta MVC yapmakla ilgili Java platformunda çok sayıda çözüm bulunur: Java Server Pages (JSP)+ Context and Dependency Injection (CDI), Java Server Faces (JSF), MVC 1.0 (Java EE 8), Spring MVCApache Wicket, Apache TapestryApache Struts
Java Server Faces bu MVC tabanlı çatılar arasında Java EE 5'den itibaren standart bir şartname olması özelliği ile öne çıkmaktadır. JSF, masaüstü uygulama geliştirirken edindiğimiz deneyimleri web uygulaması geliştirirken de kullanmamıza olanak sağlar. Böylelikle hızlı web uygulaması geliştirebiliriz. Ancak sadece JSF kullanarak uygulama geliştirmek mümkün değildir. JSF'deki görsel bileşenler hem sayıca hem de yetenekleri açısından kısıtlıdır. Mutlaka JSF bileşen çözümlerinden birini kullanmak gerekir. Bu noktada PrimeFaces, RichFaces, IceFaces bileşen kütüphaneleri öne çıkmaktadır. Bunlar arasında en yetenekli bileşenleri PrimeFaces sunmaktadır.

Sunucu Tarafta Doğrulama

Katmanlı mimaride doğrulamayı hangi katmanda yapmak gerekir? Sunum?, İş Mantığı?, Tümleştirme? Bunun cevabını Java EE 6 ile gelen Bean Validation (JSR-303) şartnamesi veriyor: Tüm katmanlarda geçerleme yapmak gerekir. Doğrulama aslında bir İlgi (=Aspect)'dir. Her katmanda aynı doğrulama kuralını tekrar tekrar tanımlamak yerine bir kez tanımlayıp, damgaları (=Annotation) kullanarak Alan/View Model/Entity sınıflarındaki özniteliklerle ilişkilendiriyoruz. Standard damgaların sayısı fazla değil: @Min, @Max@DecimalMin@DecimalMax@Digits@NotNull, @Null, @Past, @Pattern, @Size@AssertTrue@AssertFalse. Ancak yeni kısıtlar tanımlayarak genişletilebilinir. Yeni kısıtlar oluşturmanın iki yolu bulunuyor. Bunlardan ilkinde aslında yeni bir kısıt tanımlamış olmuyoruz. Sadece standart ile gelen kısıtları birlikte kullanıyoruz ve bu birlikteliğe bir isim veriyoruz. Şimdi bunu bir kaç örnekle inceleyelim:
  • Email
package com.example.web.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Pattern;

/**
 *
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
@Target(value = ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Pattern(regexp="^\\+?[a-z0-9](([-+.]|[_]+)?[a-z0-9]+)*@([a-z0-9]+(\\.|\\-))+[a-z]{2,6}$", message="{validation.email}") @Constraint(validatedBy={}) public @interface Email { String message() default "{validation.email}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Email kısıtı aslında yeni bir kısıt değil. Sadece @Pattern kısıtının kullanımına bir kısa yol oluşturduk. Bu sayede e-posta geçerlilik sınaması için düzenli ifadeyi hatırlamamız ya da kopyala-yapıştır yapmamız gerekmeyecek. Kısıtı tekrar kullanılabilir hale getirdik. E-posta düzenli ifadesinin bakımını yapmak da kolay olacak.
  • StrongPassword
package com.example.web.validation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

/**
 *
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
@Size(min=6,message="{validation.strongPassword1}") @Pattern.List({ @Pattern(regexp="^.*\\d+.*$",message="{validation.strongPassword2}"), @Pattern(regexp="^.*[_-]+.*$",message="{validation.strongPassword3}") }) @Constraint(validatedBy = {}) @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface StrongPassword { String message() default "{validation.strongPassword1}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@StrongPassword kısıtı @Pattern ve @Size kısıtlarını birlikte kullanıyor. Güçlü parola için parolanın en az altı karakter olmasını ve bu karakterlerin en az bir rakam ve {_-} kümesinden de bir karakter içermesini istiyoruz.
  • Username
package com.example.web.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Pattern;

/**
 *
 * @author Binnur Kurt (binnur.kurt@gmail.com)
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Pattern.List({
    @Pattern(regexp = "^[a-zA-Z].*$", message = "{validation.username1}"),
    @Pattern(regexp = "^[a-zA-z0-9]{5,}$", message = "{validation.username2}")
})
@Constraint(validatedBy = {})
public @interface Username {

    String message() default "{validation.username3}";

    Class<? extends Payload>[] payload() default {};

    Class<?>[] groups() default {};

}
Kendi damgalarımızı ve bu damgaların iliştirildiği özniteliği doğrulayacak kodu JSR-303'de yazabiliriz. Bunu iki örnekle inceleyelim: Iban Numarası ve TC Kimlik Numarası. Çalışmamıza Iban Numarası ve TC Kimlik Numarası için birer damga tasarlayarak başlayacağız:
Iban.java:
package com.example.web.validation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IbanValidator.class)
public @interface Iban {
 String message() default "{validation.iban}";

 Class<?>[] groups() default {};

 Class<? extends Payload>[] payload() default {};
}
TcKimlikNo.java:
package com.example.web.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target(value={ElementType.FIELD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=TCKimlikNoValidator.class)
public @interface TcKimlikNo {
 String message() default "{validation.identityNo}";

 Class<?>[] groups() default {};

 Class<? extends Payload>[] payload() default {};
}
Ancak damgalar birer meta datadır. Derlendiklerinde çalıştırılabilir koda dönüşmezler. Eğer @Retention(RetentionPolicy.RUNTIME) damgası ile tanımlanmışlarsa .class dosyasına yazılır ve sınıf yükleyicisi ile de PermGen/Meta-space alanına yerleştirilirler. Böylelikle de Reflection API aracılığı ile bu damgalara uygulama tarafından erişilebilinir. Şimdi bu damgalar (@Iban ve @TcKimlikNo) ile tanımlanmış bir özniteliğin içeriğini doğrulayacak bir kod yazmamız gerekiyor:
IbanValidator.java:
package com.example.web.validation;

import java.util.Collections;
import java.util.Map;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;


public class IbanValidator implements ConstraintValidator<Iban, String> {

   private static final long MAX = 999999999;
   private static final long MODULUS = 97;

   @Override
   public void initialize(Iban iban) {
   }

   @Override
   public boolean isValid(String value, ConstraintValidatorContext ctx) {
      if (value == null || value.length() < 5) {
          return false;
      }
      try {
         int modulusResult = calculateModulus(value);
         return (modulusResult == 1);
      } catch (Exception ex) {
         return false;
      }
   }

   private int calculateModulus(String code) throws Exception {
      String reformattedCode = code.substring(4) + code.substring(0, 4);
      long total = 0;
      for (int i = 0; i < reformattedCode.length(); i++) {
          int charValue = Character.getNumericValue(reformattedCode.charAt(i));
          if (charValue < 0 || charValue > 35) {
              throw new Exception("Invalid Character[" + i + "] = '"
                                  + charValue + "'");
          }
          total = (charValue > 9 ? total * 100 : total * 10) + charValue;
          if (total > MAX) {
             total = (total % MODULUS);
      }
    } 
    return (int) (total % MODULUS);
   }

}
TcKimlikNoValidator.java:
package com.example.web.validation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class TCKimlikNoValidator implements ConstraintValidator<TcKimlikNo, String> {

   @Override
   public void initialize(TcKimlikNo tcKimlikNo) {
   }

   @Override
   public boolean isValid(String value, ConstraintValidatorContext ctx) {
      if (!value.matches("^\\d{11}$")) {
         return false;
      }
      int[] digits = new int[11];
      for (int i = 0; i < digits.length; ++i) {
      digits[i] = value.charAt(i) - '0';
      }
      int x = digits[0];
      int y = digits[1];
      for (int i = 1; i < 5; i++) {
        x += digits[2 * i];
      }
      for (int i = 2; i <= 4; i++) {
         y += digits[2 * i - 1];
      }
      int c1 = 7 * x - y;
      if (c1 % 10 != digits[9]) {
         return false;
      }
      int c2 = 0;
      for (int i = 0; i < 10; ++i) {
         c2 += digits[i];
      }
      if (c2 % 10 != digits[10]) {
         return false;
      }
      return true;
   } 

}
Damga ile doğrulamayı yapacak kod arasındaki ilişki damganın validatedBy isimli özniteliğinin değeri olarak doğrulama yapacak sınıfın adı verilerek sağlanır: @Constraint(validatedBy={IbanValidator.class}) ve @Constraint(validatedBy={TcKimlikNoValidator.class})Şimdi artık @TcKimlikNo ve @Iban kısıtlarını herhangi bir bileşende kullanabiliriz. Bunun için bir CDI bileşenini kullanalım:
package com.example.imdb.web.model;

import java.io.Serializable;

import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import javax.inject.Named;

import com.example.web.validation.Email;
import com.example.web.validation.Iban;
import com.example.web.validation.StrongPassword;
import com.example.web.validation.Username;
import com.example.web.validation.TcKimlikNo;

@Named("UserVM")
@SessionScoped
public class UserViewModel implements Serializable{

   @Email(message="You must enter a valid e-mail")
   private String email;

   @Iban(message="You must enter a valid iban!")
   private String iban;

   @TcKimlikNo(message="You must enter a valid identity no!")
   private String identityNo;

   @Username
   private String username;

   @StrongPassword
   private String password;

   .
   .
   .     

}
Önyüzü ise JSF'nin XML tabanlı View Description Language (VDL)'ini kullanarak oluşturuyoruz (logon.xhtml):
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!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:p="http://primefaces.org/ui"
   xmlns:f="http://java.sun.com/jsf/core"
   xmlns:h="http://java.sun.com/jsf/html">
<h:head>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
   <title>Logon Page</title>
</h:head>
<h:body>
   <f:view>
      <h:form>
         <h:panelGrid columns="3">
            <p:outputLabel for="email" value="E-mail:" />
            <p:inputText id="email" 
               required="true"
               requiredMessage="You must enter an e-mail!" 
               value="#{UserVM.email}">
            </p:inputText>
            <p:message id="emailError" for="email" />

            <p:outputLabel for="iban" value="Iban:" />
            <p:inputText id="iban" 
               required="true"
               requiredMessage="You must enter an iban!" 
               value="#{UserVM.iban}" />
            <p:message id="ibanError" for="iban" />

            <p:outputLabel for="identityNo" value="Identity no:" />
            <p:inputText id="identityNo" 
               required="true"
               requiredMessage="You must enter an identity No!"
               value="#{UserVM.identityNo}" />
            <p:message id="identityNoError" for="identityNo" />

            <p:outputLabel for="username" value="Username:" />
            <p:inputText id="username" 
               required="true"
               requiredMessage="You must enter a username!"
               value="#{UserVM.username}">
            </p:inputText>
            <p:message id="usernameError" for="username" />

            <p:outputLabel for="password" value="Password:" />
            <h:panelGroup>
               <p:password id="password" 
                   required="true"
                   requiredMessage="You must enter a valid year!"
                   value="#{UserVM.password}">
               </p:password>
               <p:message id="passwordError" for="password" />
            </h:panelGroup>
            <p:commandButton action="#{UserVM.doLogon}" 
                   value="Logon"
                   update="emailError usernameError passwordError ibanError identityNoError"/>
         </h:panelGrid>
      </h:form>
   </f:view>
</h:body>
</html>
Yukarıdaki sayfayı istediğimizde ekran görüntüsü aşağıdaki gibi olacaktır:
Doğrulama yapılacak ekran görüntüsü
Geçersiz verilerle doldurduğumuz sayfada Logon butonuna basılınca sunucuya bir istek gidecektir:
Hatalı verilerle dolu ekran görüntüsü
Bu isteğe karşı olarak JSF yaşam döngüsünde Restore View, Apply Request Values, Process Validations ve Render Response evreleri çalışır:
JSF Yaşam Döngüsü
Render Response evresinde hata mesajları eklenen HTML cevabı oluşturulur ve cevap tarayıcıya gönderilir:  
Hata iletilerinin gösterildiği ekran sunucu tarafta oluşturuldu
Web tarayıcısında, Logon butonuna basıldığında sunucuya giden isteği ve cevap süresini gösteren geliştirici araçları ekranı 
Bu çözümde doğrulama sunucu tarafta gerçekleşmektedir. Ancak özellikle iki nedenden dolayı istemci tarafta da doğrulama yapmak gerekir:
  1. Uygulamanın başarımı artacaktır. Doğrulama istemci tarafta da gerçekleştirildiği için başarısız girişlerde istemci sunucu arasında zaman kaybettirecek ve sunucunun yükünü arttıracak gereksiz istekler oluşmayacaktır.
  2. Daha yüksek bir kullanıcıyı deneyimi yaşanacaktır.
İstemci Tarafta Doğrulama

İstemci tarafta doğrulama yapalım ya da yapmayalım sunucu tarafta mutlaka doğrulama yapmamız gerekir. JSF, istemci tarafta doğrulama yapmak için hazır bir çözüm sunmaz. Ancak PrimeFaces, JSR-303'de sunucu tarafta yapılan doğrulamaya benzer bir çözümü istemci tarafta gerçeklememizi sağlayan bir mekanizma sunuyor. Şimdi bu mekanizmayı inceleyeceğiz.
PrimeFaces'da istemci tarafta doğrulama yapmak için öncelikle, web.xml'de bu özelliği açmamız gerekir:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="http://xmlns.jcp.org/xml/ns/javaee"
 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
 id="WebApp_ID" version="3.1">
   <display-name>imdb-jsf</display-name>
   <welcome-file-list>
       <welcome-file>faces/logon.xhtml</welcome-file>
   </welcome-file-list>
   <servlet>
       <servlet-name>Faces Servlet</servlet-name>
       <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
       <load-on-startup>1</load-on-startup>
   </servlet>
   <servlet-mapping>
       <servlet-name>Faces Servlet</servlet-name>
       <url-pattern>/faces/*</url-pattern>
   </servlet-mapping>
   <context-param>
       <param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
       <param-value>true</param-value>
   </context-param>
</web-app>
Burada istemci tarafta doğrulamayı açmak için primefaces.CLIENT_SIDE_VALIDATION parametresinin değerini true yapmamız gerekir. İstemci tarafta doğrulamayı Javascript (JS) kullanarak gerçekleştireceğiz. Ama öncelikle daha önce yukarıda Bean Validation için kısıt tasarlarken yaptığımıza benzer bir kodlama yapacağız. Iban ve TcKimlikNo damgalarında daha önce sunucu tarafta hangi sınıfın doğrulama yapacağını @Constraint(validatedByIbanValidator.class) ve @Constraint(validatedBy=TCKimlikNoValidator.class) damgaları ile tanımlamıştık. Şimdi ise aynı damgalarda istemci tarafta doğrulama ile tanımlama yapacağımız sınıfları tanıtacağız:
Iban.java:
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IbanValidator.class)
@ClientConstraint(resolvedBy = IbanClientValidator.class)
public @interface Iban {
   String message() default "{validation.iban}";
   
   Class<?>[] groups() default {};
   
   Class<? extends Payload>[] payload() default {};
}
TcKimlikNo.java:
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=TCKimlikNoValidator.class)
@ClientConstraint(resolvedBy = TcKimlikNoClientValidator.class)
public @interface TcKimlikNo {
   String message() default "{validation.identityNo}";
   
   Class<?>[] groups() default {};
   
   Class<? extends Payload>[] payload() default {};
}
@ClientConstraint(resolvedBy = IbanClientValidator.class) ve @ClientConstraint(resolvedBy = TcKimlikNoClientValidator.class) damgalarında adı geçen sınıflara bir bakalım:
IbanClientValidator.java:
package com.example.web.validation;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import javax.validation.metadata.ConstraintDescriptor;
import org.primefaces.validate.bean.ClientValidationConstraint;

public class IbanClientValidator implements ClientValidationConstraint {

   @Override
   public Map<String, Object> getMetadata(ConstraintDescriptor constraintDescriptor) {
      Map<String,Object> metadata = new HashMap<String, Object>();
      for (Object cd : constraintDescriptor.getComposingConstraints()) {
          if (cd instanceof ConstraintDescriptor) {
              Annotation annotation = ((ConstraintDescriptor) cd).getAnnotation();
              if (annotation instanceof Iban){
                 metadata.put("data-message", ((Iban) annotation).message());
              }
           }
      }
      return metadata;
   }

   @Override
   public String getValidatorId() {
      return Iban.class.getSimpleName();
   }

}
TcKimlikNoClientValidator.java:
package com.example.web.validation;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

import javax.validation.metadata.ConstraintDescriptor;

import org.primefaces.validate.bean.ClientValidationConstraint;

public class TcKimlikNoClientValidator implements ClientValidationConstraint {

   @Override
   public Map<String, Object> getMetadata(ConstraintDescriptor constraintDescriptor) {
     Map<String, Object> metadata = new HashMap<String, Object>();
     for (Object cd : constraintDescriptor.getComposingConstraints()) {
        if (cd instanceof ConstraintDescriptor) {
           Annotation annotation = ((ConstraintDescriptor) cd).getAnnotation();
           if (annotation instanceof TcKimlikNo) {
              metadata.put("data-message", ((TcKimlikNo) annotation).message());
           }
        }
     }
     return metadata;
   }

   @Override
   public String getValidatorId() {
        return TcKimlikNo.class.getSimpleName();
   }

}
Gördüğünüz gibi sunucu taraftaki kodda doğrulama ile ilgili herhangi bir kod yok. Buradaki en önemli metod, istemci tarafta doğrulama yapacak JS kodu tanımlayan bir etiket döndüren getValidatorId()İstemci tarafta bu etiketlere karşı düşen JS kodu yazıyoruz:
iban-validator.js:
PrimeFaces.validator['Iban'] = {
   MAX : 999999999,
   MODULUS : 97,
   throwError : function(detail) {
        throw {
          summary : 'Validation Error',
          detail : detail
        }
   },
   calculateModulus : function(code) {
     var reformattedCode = code.substring(4) + code.substring(0, 4);
     reformattedCode = reformattedCode.replace(/[A-Z]/g, function(match) {
         return match.charCodeAt(0) - 55;
     });
     var total = 0;
     for (i = 0; i < reformattedCode.length; i++) {
        charValue = reformattedCode.charCodeAt(i) - 48;
        if (charValue < 0 || charValue > 35) {
          return 0;
        }
        total = (Number(charValue) > 9 ? total * 100 : total * 10) + charValue;
        if (total < this.MAX) {
          total = (total % this.MODULUS);
        }
     }
     return total % this.MODULUS;
   },
   validate : function(element, value) {
      if (value == undefined || value.length < 5) {
         this.throwError("This is not a valid IBAN!")
      }
      modulusResult = this.calculateModulus(value);
      if (modulusResult != 1) {
         messageId = element.data('message');
         message = PrimeFaces.util.ValidationContext.getMessage(messageId).detail;
         this.throwError(message);
      }
   }
};
tckimlik-validator.js:
PrimeFaces.validator['TcKimlikNo'] = {
   throwError : function(detail) {
      throw {
        summary : 'Validation Error',
        detail : detail
      }
   },
   isValid : function(value) {
     if (value.match("^\\d{11}$")==null) {
       return false;
     }
     digits = new Array(11);
     for (i=0;i<digits.length;++i) {
         digits[i] = value.charCodeAt(i) - 48;
         if (digits[i] < 0 || digits[i] > 9) {
            return false;
         }
     }
     x = digits[0];
     y = digits[1];
     for (i = 1; i < 5; i++) {
         x += Number(digits[2 * i]);
     }
     for (i = 2; i <= 4; i++) {
         y += Number(digits[2 * i - 1]);
     }
     c1 = 7 * x - y;
     if (c1 % 10 != digits[9]) {
        return false;
     }
     c2 = 0;
     for (i = 0; i < 10; ++i) {
         c2 += digits[i];
     }
     if (c2 % 10 != digits[10]) {
        return false;
     }
     return true;
   },
   validate : function(element, value) {
     if (value == undefined || value.length != 11) {
         this.throwError("This is not a valid identity no!")
     }
     if (!this.isValid(value)) {
         messageId = element.data('message');
         message = PrimeFaces.util.ValidationContext.getMessage(messageId).detail;
         this.throwError(message);
     }
}
};
PrimeFaces.validator['Iban'] ve PrimeFaces.validator['TcKimlikNo'] aracılığı ile IbanClientValidator ve TcKimlikClientNoValidator sınıfları arasında bağı kurmuş oluyoruz. Şimdi, istemci tarafta doğrulamayı açtığımız çözümün VDL'ine (logon-csv.xhtml) bir bakalım:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!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:p="http://primefaces.org/ui"
 xmlns:f="http://java.sun.com/jsf/core"
 xmlns:h="http://java.sun.com/jsf/html">
<h:head>
 <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
 <title>Logon Page</title>
<script type="text/javascript" src="js/lang_en.js"></script>
<script type="text/javascript" src="js/iban-validator.js"></script> <script type="text/javascript" src="js/tckimlik-validator.js"></script>
<script type="text/javascript" src="js/username-validator.js"></script>
 <script type="text/javascript" src="js/strongpassword-validator.js"></script>
</h:head> <h:body> <f:view> <h:form> <h:panelGrid columns="3"> <p:outputLabel for="email" value="E-mail:" /> <p:inputText id="email" 
                 required="true"        
                 requiredMessage="You must enter an e-mail!" 
                 value="#{UserVM.email}">
         <p:clientValidator  event="blur" />                                           
    </p:inputText>
    <p:message id="emailError" for="email" />

    <p:outputLabel for="iban" value="Iban:" />
    <p:inputText id="iban" 
                 required="true"
                 requiredMessage="You must enter an iban!" 
                 value="#{UserVM.iban}">
        <p:clientValidator event="blur" />                        
    </p:inputText> 
    <p:message id="ibanError" for="iban" />

    <p:outputLabel for="identityNo" value="Identity no:" />
    <p:inputText id="identityNo" 
                 required="true"
                 requiredMessage="You must enter an identity No!"
                 value="#{UserVM.identityNo}" >
        <p:clientValidator event="blur"  />     
    </p:inputText>
    <p:message id="identityNoError" for="identityNo" />

    <p:outputLabel for="username" value="Username:" />
    <p:inputText id="username" 
                 required="true"
                 requiredMessage="You must enter a username!"
                 value="#{UserVM.username}">
          <p:clientValidator event="blur"  />                      
    </p:inputText>
    <p:message id="usernameError" for="username" />

    <p:outputLabel for="password" value="Password:" />
    <h:panelGroup>
     <p:password id="password" 
                 required="true"
                 requiredMessage="You must enter a valid year!"
                 value="#{UserVM.password}">
           <p:clientValidator event="blur"  />                       
     </p:password>
     <p:commandButton action="#{UserVM.doLogon}" 
         value="Logon" 
         validateClient="true"
         update="emailError usernameError passwordError ibanError identityNoError"/>
    </h:panelGroup>
    <p:message id="passwordError" for="password" />
   </h:panelGrid>
  </h:form>
 </f:view>
</h:body>
</html>
İstemci tarafta doğrulama özelliğini çalıştırmak için <p:commandButton> takısında validateClient özniteliğinin değerini true olarak veriyoruz. Giriş elemanında bir değer verip, elemanı terk ettiğimizde, o eleman için JS dosya içinde verdiğimiz doğrulama çalıştırılır: <p:clientValidator event="blur" />. Tarayıcıdaki doğrulama yapılmış ekran görüntüsüne bakalım:
Doğrulamanın istemci tarafta gerçekleştiği ekran görüntüsü
Ağ kaydına baktığımızda, doğrulama için sunucuya gitmediğini, doğrulamanın istemci tarafta gerçekleştiğini görüyoruz. Son olarak @Username ve @StrongPassword Bean Validation damgaları için istemci tarafta doğrulama yapmak üzere tasarladığımız çözümlere bakalım:
EmailClientValidator.java:
package com.example.web.validation;

import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.Pattern;
import javax.validation.metadata.ConstraintDescriptor;

import org.primefaces.util.HTML;
import org.primefaces.validate.bean.ClientValidationConstraint;

public class EmailClientValidator implements ClientValidationConstraint {

   private static final String MESSAGE_METADATA = "data-p-pattern-msg";
   private static final String MESSAGE_ID = "{javax.validation.constraints.Pattern.message}";
    
   @Override
   public Map<String, Object> getMetadata(ConstraintDescriptor constraintDescriptor) {
       for (Object o : constraintDescriptor.getComposingConstraints()) {
          if (o instanceof ConstraintDescriptor) {
             ConstraintDescriptor cd = (ConstraintDescriptor) o;
             Map<String,Object> metadata = new HashMap<String, Object>();
             Map attrs = cd.getAttributes();
             Object message = attrs.get("message");
        
             metadata.put(HTML.VALIDATION_METADATA.PATTERN, attrs.get("regexp"));
        
            if(!message.equals(MESSAGE_ID)) {
               metadata.put(MESSAGE_METADATA, "You must enter a valid e-mail!");
            }
        
            return metadata;
         }
      }
      return null;
   }

   @Override
   public String getValidatorId() {
      return Pattern.class.getSimpleName();
   }
 
}
StrongPasswordClientValidator.java:
package com.example.web.validation;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import javax.validation.metadata.ConstraintDescriptor;

import org.primefaces.validate.bean.ClientValidationConstraint;

public class StrongPasswordClientValidator implements ClientValidationConstraint {

   @Override
   public Map<String, Object> getMetadata(ConstraintDescriptor constraintDescriptor) {
      int patternNo = 1, numberOfPatterns = 0;
      Map<String, Object> metadata = new HashMap<String, Object>();
      for (Object o : constraintDescriptor.getComposingConstraints()) {
          if (o instanceof ConstraintDescriptor) {
             ConstraintDescriptor cd = (ConstraintDescriptor) o;
             Annotation annotation = cd.getAnnotation();
             if (annotation instanceof Pattern) {
                Pattern pattern = (Pattern) annotation;
                String patternNoAsString = Integer.toString(patternNo);
                metadata.put("data-pattern".concat(patternNoAsString), pattern.regexp());
                metadata.put("data-message".concat(patternNoAsString), pattern.message());
                patternNo++;
                numberOfPatterns++;
             } else if (annotation instanceof Size) {
                Size size = (Size) annotation;
                metadata.put("data-size-min", size.min());
                metadata.put("data-size-max", size.max());
                metadata.put("data-size-message", size.message());
             }
          }
      }
      metadata.put("data-no-of-patterns", numberOfPatterns);
      return metadata;
   }

   @Override
   public String getValidatorId() {
      return StrongPassword.class.getSimpleName();
   }

}
UsernameClientValidator.java:
package com.example.web.validation;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import javax.validation.constraints.Pattern;
import javax.validation.metadata.ConstraintDescriptor;
import org.primefaces.validate.bean.ClientValidationConstraint;

public class UsernameClientValidator implements ClientValidationConstraint {
 
   @Override
   public Map<String, Object> getMetadata(ConstraintDescriptor constraintDescriptor) {
       int patternNo = 1, numberOfPatterns = 0;
       Map<String, Object> metadata = new HashMap<String, Object>();
       for (Object o : constraintDescriptor.getComposingConstraints()) {
           if (o instanceof ConstraintDescriptor) {
              ConstraintDescriptor cd = (ConstraintDescriptor) o;
              Annotation annotation = cd.getAnnotation();
              if (!(annotation instanceof Pattern)) continue;
              Pattern pattern = (Pattern) annotation;
              String patternNoAsString = Integer.toString(patternNo);
              metadata.put("data-pattern".concat(patternNoAsString), pattern.regexp());
              metadata.put("data-message".concat(patternNoAsString), pattern.message());
              patternNo++;
              numberOfPatterns++;
           }
       }
       metadata.put("data-no-of-patterns", numberOfPatterns);
       return metadata;
   }

   @Override
   public String getValidatorId() {
 return Username.class.getSimpleName();
   }

}
PrimeFaces, Bean Validation API'si ile gelen standard damgalar (@Min, @Max, @DecimalMin, @DecimalMax, @Digits, @NotNull, @Null, @Past, @Pattern, @Size, @AssertTrue, @AssertFalse) için istemci tarafta doğrulama yapacak JS kodunu bize sağlıyor. Bunlar için JS kodu yazmamıza gerek yok. Ancak kendi yazdığımız Bean Validation kısıtları için ise istemci tarafta asıl doğrulamayı yapacak kodu yazmamız gerekir. @Username ve @StrongPassword için bu JS kodları aşağıda verilmiştir:
username-validation.js:
PrimeFaces.validator['Username'] = {
   throwError : function(detail) {
      throw {
         summary : 'Validation Error',
         detail : detail
      }
   },
   validate : function(element, value) {
      if (value == undefined) {
         this.throwError("This is not a valid username!")
      }
      var noOfPatterns = Number(element.data('no-of-patterns'));
      patterns = [];
      messages = [];
      for (i = 1; i <= noOfPatterns; ++i) {
          patterns.push(element.data('pattern' + i));
          messageId = element.data('message' + i);
          message = PrimeFaces.util.ValidationContext.getMessage(messageId);
          messages.push(message.detail);
      }
      for (i in patterns) {
          pattern = patterns[i];
          var regex = new RegExp(pattern);
          if (!regex.test(value)) {
             this.throwError(messages[i]);
          }
      }
   }
};
strongpassword-validation.js:
PrimeFaces.validator['StrongPassword'] = {
   throwError : function(detail) {
        throw {
          summary : 'Validation Error',
          detail : detail
        } 
   },
   validate : function(element, value) {
      if (value == undefined) {
         this.throwError("This is not a valid username!")
      }
      var noOfPatterns = Number(element.data('no-of-patterns'));
      patterns = [];
      messages = [];
      for (i = 1; i <= noOfPatterns; ++i) {
         patterns.push(element.data('pattern' + i));
         messageId = element.data('message' + i);
         message = PrimeFaces.util.ValidationContext.getMessage(messageId);
         messages.push(message.detail);
      }
      for (i in patterns) {
          pattern = patterns[i];
          var regex = new RegExp(pattern);
          if (!regex.test(value)) {
             this.throwError(messages[i]);
          }
      }
      messageId = element.data('size-message');
      sizeMessage = PrimeFaces.util.ValidationContext.getMessage(messageId).detail;
      sizeMin=  element.data('size-min');
      sizeMax=  element.data('size-max');
      valueLength= value.length;
      if (valueLength<sizeMin || valueLength>sizeMax)
         this.throwError(sizeMessage);
   }
};
@StrongPassword ve @Username damgalarında kullanılan Bean Validation damgalarının parametrelerini (örneğin; @Pattern için regexp parametresi, @Size için min parametresi gibi) istemci tarafta çalışacak JS koduna aktarmak için UsernameClientValidator ve StrongPasswordClientValidator sınıflarındaki getMetadata() metodundan yararlanıyoruz. Bu metodun döndürdüğü Map<String,Object> içine String tipinden etiketlerle bu damga parametrelerini atıyoruz. Etiketlere isim verirken "data-"  ile başlamasına dikkat etmelisiniz. Benzer şekilde, JS kodunda bu değere erişirken element.data() ile erişildiğine dikkat etmişsinizdir. Ancak JS içinden damga parametresine erişirken "data-" önekini kullanmıyoruz.
Kodun tamamına Eclipse projesi olarak bu adresten ulaşabilirsiniz. JSF 2 ve PrimeFaces ile ilgili daha fazla detay öğrenmek isterseniz "Core JSF 2 and PrimeFaces 5" eğitimini incelemenizi öneririm.

No comments:

Post a Comment