Tuesday, April 2, 2013

Bean Validation (JSR-303) API

Java EE 6 standardının yayınlanalı 3 yılı aşkın bir süre oldu. Bu uzunca bir süre. Hatta yakında yeni sürümünü bekliyoruz. Ancak buna rağmen henüz daha yeni yeni kullanılmaya başlandığını görüyoruz. Java EE 6 kurumsal Java uygulaması geliştirmeyi kolaylaştıran epey bir yenilik içeriyor. Bunlardan biri alan nesnelerini doğrulamakta kullanılan "Bean Validation" Uygulama Programlama Arayüzüdür (UPA). 
Java EE platformu çok kullanıcılı, ölçeklenebilir ve her zaman erişilebilinir yazılım mimarisi üzerinde dağıtılmış, bileşen tabanlı ve Java SE 5 ile gelen damgalar (=annotations) aracılığı ile betimsel yazılım geliştirme modeli sunuyor. Bu yaklaşım kurumsal uygulama geliştirmeyi oldukça kolaylaştırıyor. Java EE standardı makinamıza indirip kurabileceğimiz bir yazılım değildir. Bir dizi arayüz şartnamesini tek bir şemsiye altında toplayan bir ana çatıdır. Toplam 33 adet bu tür şartnameden oluşmaktadır. Bean Validation UPA'sı da bunlardan sadece biridir ve platforma bu sürüm ile yeni eklenmiştir. Java EE uygulamaları çalışabilmeleri için bir uygulama sunucusuna ihtiyaç duyarlar. Uygulama sunucuları işte bu 33 standardı gerçekleyen yazılımlardır ve makinamıza bunlardan birini kuruyor olmamız gerekir. Uygulamamızı uygulama sunucusuna yayınlarız ve uygulama sunucusu bileşenlerden oluşan uygulamamızı çalıştırır. Bileşenler de tıpkı sınıflar gibi tanımlanır. Arada ufak bir fark bulunmaktadır. Nesneye dayalı programlamada modelleme için kullandığımızdan sınıflardan problemi çözebilmek için nesneler yaratmamız gerekir. Yarattığımız bu nesneler Java'da bellekte Heap adını verdiğimiz bölgeye yerleşir ve biz ona herhangi bir istekte bulunmadığımız sürece hiç bir iş yapmaz. Bu anlamda nesnelerin pasif olduklarını söyleyebiliriz. Bileşenler ise yine sınıf olarak tasarlanmakta ve tanımlanmaktadırlar. Ancak biz hiçbir zaman bu bileşenlerden nesne yaratmayız ya da metodlarını çağırmayız. Bileşenler var olmak ve yaşayabilmek için bir kap teknolojisine ihtiyaç duyarlar. Kap gerektiğinde bileşen yaratmaya karar verir, gerektiğinde metodlarından birini çağırır ve bazen de bileşeni yok eder. Yani bileşenlerin yaşam döngüsü kap tarafından yönetilir. Java EE'de bu bileşenler ölçeklenebilirlik ve her zaman erişilebilirlik için dağıtık bir yapıda yer alabilirler. Java EE'de iki tane kap bulunmaktadır: Web Kabı ve EJB Kabı. Uygulama sunucusunun bu iki kaptan oluştuğunu söyleyebiliriz. 
Uygulama içinde farklı türde bileşenler bulunur. Sunum katmanında Servlet/JSP iş mantığı katmanında ise "Session Bean" ve "Message Driven Bean" olarak adlandırılan bileşen modelleri vardır. Her bir tür bileşenin bir sorumluluğu ve uzmanlığı vardır. Örneğin JSP uygulamanın kullanıcıya dönük yüzünü oluşturmaktan sorumludur. İş mantığını ise basit karmaşıklıkta olanlarını Session Bean ve uzun soluklu ya da yığın işlemleri Message Driven Bean bileşenlerine yaptırıyoruz.
Geliştirici olarak doğrulama sorumluluğunu her katmanda yerine getirmeliyiz. Kullancıdan bilgileri alırken doğruluğunu sınamalıyız. İş katmanında iş mantığını çalıştırmadan önce ve tümleştirme katmanında alan nesnelerini veritabanında kalıcı hale getirmeden önce geçerlilik sınaması yapmamız gerekir. Özet olarak geçerlilik sınaması tek bir katmanın bir sorumluluğu değildir. Sınamayı yapabilmek için bir dizi kurala ihtiyacımız olacaktır. Bu kuralları tanımlamak için en uygun yer neresi olabilir? İşte bu soruya JSR-303 alan nesneleri olarak cevap veriyor. Her standardın, standarda dönüşmeden önce geçmesi gereken belirli prosedürler bulunmaktadır. Java'nın gelişimi Java Community Process tarafından yönetilmektedir. Yenilik veya değişimleri nedenleri ile açıklayan bir doküman JCP'ye sunulur. Kabul edilen her başvuru için bir JSR açılır, şartname lideri atanır. Lider uzmanlardan oluşan teknik bir grup oluşturur. Bu grup şartname dokümanı yazmaktan ve bu şartname ile uyumlu bir referans uygulama geliştirmekten sorumludur. Çeşitli aşamalardan geçen şartname en sonunda JCP tarafından oylanır ve kabul edilirse Java şartnamesine dönüşür. Bu sürecin de bir JSR şartnamesi mevcuttur: JSR-355. 
Kuralları damgalar aracılığı ile alan nesnelerinde tanımlıyoruz. Standard damgalar fazla değil: @Min, @Max, @NotNull, @Null, @Past, @Pattern, @Size. Ancak genişletilebilinir. Biz de kendi damgalarımızı ve bu damganın iliştirildiği özniteliği doğrulayacak kodu yazabiliriz. Bunu basit bir örnekle göstermek istiyorum. Örneğimize kredi kartı için bir damga tasarlayarak başlayacağız: 
1:  package com.example.validation;  
2:    
3:  import java.lang.annotation.ElementType;  
4:  import java.lang.annotation.Retention;  
5:  import java.lang.annotation.RetentionPolicy;  
6:  import java.lang.annotation.Target;  
7:  import javax.validation.Constraint;  
8:  import javax.validation.Payload;  
9:    
10:  /**  
11:   *  
12:   * @author Binnur Kurt  
13:   */  
14:  @Constraint(validatedBy={CreditCardValidator.class})  
15:  @Retention(RetentionPolicy.RUNTIME)  
16:  @Target(ElementType.FIELD)  
17:  public @interface CreditCard {  
18:      String delimeter() default "-";  
19:      public String message() default "This is not a valid credit card!";  
20:      public Class<?>[] groups() default {};  
21:      public Class<? extends Payload>[] payload() default {};  
22:  }  
Ancak damgalar birer meta-datadır. Derlendiklerinde bytecode'a 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'e yerleştirilirler. Böylelikle de Reflection API aracılığı ile uygulama tarafından işlenebilirler. Şimdi bu damgalar ile tanımlanmış bir özniteliğin içeriğini doğrulayacak bir kod yazmamız gerekiyor:
com.example.validation.CreditCardValidator.java:
1:  package com.example.validation;  
2:    
3:  import com.example.domain.CreditCardBean;  
4:  import javax.validation.ConstraintValidator;  
5:  import javax.validation.ConstraintValidatorContext;  
6:    
7:  /**  
8:   *  
9:   * @author Binnur Kurt  
10:   */  
11:  public class CreditCardValidator implements ConstraintValidator<CreditCard, CreditCardBean> {  
12:    
13:    private String delimeter;  
14:    
15:    @Override  
16:    public void initialize(CreditCard constraintAnnotation) {  
17:      delimeter = constraintAnnotation.delimeter();  
18:    }  
19:    
20:    @Override  
21:    public boolean isValid(CreditCardBean value, ConstraintValidatorContext context) {  
22:      int sum = 0;  
23:      for (int i = 0; i < 4; i++) {  
24:        String num = value.getCardNumbers(i);  
25:        sum += (num.charAt(1)-'0') + (num.charAt(3)-'0');  
26:      }  
27:      for (int i = 0; i < 4; i++) {  
28:        String num = value.getCardNumbers(i);  
29:        sum += 2 * ((num.charAt(0)-'0') + (num.charAt(2)-'0'));  
30:      }  
31:    
32:      if (sum % 10 == 0) {  
33:        return true;  
34:      }  
35:      return false;  
36:    }  
37:  }  
com.example.domain.CreditCardBean.java:
1:  package com.example.domain;  
2:    
3:  import java.io.Serializable;  
4:    
5:  /**  
6:   *  
7:   * @author Binnur Kurt  
8:   */  
9:  public class CreditCardBean implements Serializable {  
10:    
11:    private String[] cardNumbers;  
12:    
13:    public CreditCardBean() {  
14:      cardNumbers= new String[4];  
15:    }  
16:    
17:    public String[] getCardNumbers() {  
18:      return cardNumbers;  
19:    }  
20:      
21:    public String getCardNumbers(int index) {  
22:      return cardNumbers[index];  
23:    }  
24:    
25:    public void setCardNumbers(String[] cardNumbers) {  
26:      this.cardNumbers = cardNumbers;  
27:    }  
28:  }  
Damga ile doğrulamayı yapacak kod arasındaki ilişki damganın validatedBy isimli özniteliğini n değerine sınıfın adı verilerek sağlanır: 
@Constraint(validatedBy={CreditCardValidator.class})
Şimdi artık @CreditCard damgasını herhangi bir alan sınıfında kullanabiliriz. Bunun için JSF 2'deki bir CDI Bean'i seçtim:
com.example.beans.UserBean.java:
1:  package com.example.beans;  
2:  import com.example.domain.CreditCardBean;  
3:  import com.example.validation.CreditCard;  
4:  import java.io.Serializable;  
5:  import javax.enterprise.context.SessionScoped;  
6:  import javax.inject.Named;  
7:  import javax.validation.constraints.Pattern;  
8:  import javax.validation.constraints.Size;  
9:  /**  
10:   *  
11:   * @author Binnur Kurt  
12:   */  
13:  @Named("user")  
14:  @SessionScoped  
15:  public class UserBean implements Serializable{  
16:    @Size(min=5)  
17:    private String username;  
18:    @Pattern(regexp="^\\+?[a-z0-9](([-+.]|[_]+)?[a-z0-9]+)*@([a-z0-9]+(\\.|\\-))+[a-z]{2,6}$")  
19:    private String email;  
20:    @CreditCard  
21:    private CreditCardBean creditCard;  
22:    public UserBean() {  
23:    }  
24:    public CreditCardBean getCreditCard() {  
25:      return creditCard;  
26:    }  
27:    public void setCreditCard(CreditCardBean creditCard) {  
28:      this.creditCard = creditCard;  
29:    }  
30:    public String getEmail() {  
31:      return email;  
32:    }  
33:    public void setEmail(String email) {  
34:      this.email = email;  
35:    }  
36:    public String getUsername() {  
37:      return username;  
38:    }  
39:    public void setUsername(String username) {  
40:      this.username = username;  
41:    }  
42:  }  
JSR-303'de 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ıtın kullanımına yeni bir isim veriyoruz:
Email.java:
package com.example.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;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp="^\\+?[a-z0-9](([-+.]|[_]+)?[a-z0-9]+)*@([a-z0-9]+(\\.|\\-))+[a-z]{2,6}$",message="You must enter a valid e-mail!")
@Constraint(validatedBy={})
public @interface Email {
    String message() default "You must enter a valid e-mail!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
StrongPassword.java:

package com.example.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;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Pattern.List({
  @Pattern(regexp="^.*\\d+.*$",message="Your password must contain a digit!"),
  @Pattern(regexp="^.*[-_]+.*$",message="Your password must contain \"-\" or \"_\"")
})
@Constraint(validatedBy = {})
public @interface StrongPassword {
    String message() default "{validation.strongPassword1}";
     Class<?>[] groups() default {};
     Class<? extends Payload>[] payload() default {};
}

İkinci yöntemde ise doğrulamasını da bizim yaptığımız yepyeni bir kısıt tanımlıyoruz:
Iban.java:

package com.example.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({ 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 {};

}
IbanValidator.java:

package com.example.validation;

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);
 }

}
TcKimlikNo.java:

package com.example.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({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=TcKimlikNoValidator.class)
public @interface TcKimlikNo {
 String message() default "{validation.identityNo}";

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

 Class<? extends Payload>[] payload() default {};
}
TcKimlikNoValidator.java:

package com.example.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 context) {
  if (value==null) return false;
  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;
 }

}
Projenin tamamını NetBeans projesi olarak bu adresten indirebilirsiniz. Projede RichFaces 4 kullanıldığı için bağımlı olduğu jar dosyalara ihtiyacınız olacaktır. İlgili dosyaları bu adresten indirebilirsiniz.

1 comment: