Walidator NIP
Opisując tworzenie nowego walidatora Hibernate wspomniałem o parametrach adnotacji. Postanowiłem utworzyć przykład takiej walidacji dla Numerów Identyfikacji Podatkowej.
NIP osoby fizycznej ma format: XXX-XXX-XX-XX, gdzie X jest dowolną cyfrą. Jednak dla osoby prawnej, jest to format: XXX-XX-XX-XXX. Dlatego w adnotacji @NIP uwzględniłem typ osoby (Person.UNSPECIFIED – dowolny podmiot, Person.NATURAL – osoba fizyczna, Person.LEGAL – osoba prawna):
package pl.info.czerwinski;
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 org.hibernate.validator.ValidatorClass;
@ValidatorClass(NIPValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NIP {
String message() default "Nieprawidłowy NIP";
public enum Person {
UNSPECIFIED,
NATURAL,
LEGAL
}
Person person() default Person.UNSPECIFIED;
}
Domyślnie NIP będzie dotyczyć dowolnego podmiotu.
Klasa walidatora dla tej adnotacji będzie sprawdzała format numeru, zależnie od wartości parametru person:
package pl.info.czerwinski;
import java.util.regex.Pattern;
import org.hibernate.validator.Validator;
public class NIPValidator implements Validator<NIP> {
private NIP.Person person;
public void initialize(NIP annotation) {
person = annotation.person();
}
public boolean isValid(Object value) {
if (value instanceof String) {
if (person == NIP.Person.NATURAL) {
return Pattern.matches(
"^[0-9]{3}-[0-9]{3}-[0-9]{2}-[0-9]{2}$",
value.toString());
} else if (person == NIP.Person.LEGAL) {
return Pattern.matches(
"^[0-9]{3}-[0-9]{2}-[0-9]{2}-[0-9]{3}$",
value.toString());
} else {
return Pattern.matches(
"^[0-9]{3}-[0-9]{3}-[0-9]{2}-[0-9]{2}$",
value.toString()) ||
Pattern.matches(
"^[0-9]{3}-[0-9]{2}-[0-9]{2}-[0-9]{3}$",
value.toString());
}
}
return false;
}
}
Jak widać, pominąłem obliczanie cyfry kontrolnej – dokładny algorytm opisuje Wikipedia.
Testy jednostkowe dla tego walidatora będą wyglądały następująco:
package pl.info.czerwinski;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class NIPValidatorTest {
private NIPValidator validator;
@NIP
public String nip;
@NIP(person=NIP.Person.NATURAL)
public String naturalPersonNIP;
@NIP(person=NIP.Person.LEGAL)
public String legalPersonNIP;
@BeforeClass
public void setUp() {
validator = new NIPValidator();
}
public NIP getAnnotation(String fieldName) {
try {
return getClass().getField(
fieldName).getAnnotation(NIP.class);
} catch (NoSuchFieldException e) {
e.printStackTrace();
return null;
}
}
@DataProvider(name="nipValidator.isValid")
public Object[][] getIsValidData() {
return new Object[][] {
{getAnnotation("nip"), "123-456-78-90", true},
{getAnnotation("nip"), "123-45-67-890", true},
{getAnnotation("naturalPersonNIP"), "123-456-78-90", true},
{getAnnotation("naturalPersonNIP"), "123-45-67-890", false},
{getAnnotation("legalPersonNIP"), "123-456-78-90", false},
{getAnnotation("legalPersonNIP"), "123-45-67-890", true},
{getAnnotation("naturalPersonNIP"), "1234567890", false},
{getAnnotation("legalPersonNIP"), "1234567890", false},
{getAnnotation("nip"), "1234567890", false},
{getAnnotation("naturalPersonNIP"), "123-456-78-9a", false},
{getAnnotation("legalPersonNIP"), "123-45-67-89a", false},
{getAnnotation("nip"), "123-456-78-9a", false},
{getAnnotation("nip"), "123-45-67-89a", false},
};
}
@Test(dataProvider="nipValidator.isValid")
public void testIsValid(
NIP annotation, Object value, boolean valid) {
validator.initialize(annotation);
assert validator.isValid(value) == valid :
"Validation result is wrong.";
}
}
Działanie przypadków testowych jest analogiczne do walidatora nazwy użytkownika, więc pozostawię ten przykład bez dodatkowych komentarzy.




Testowanie walidatorów nie jest zbyt skomplikowane, na wstęp bardzo dobre i nakreśla problem. Czekam jednak na testy akcji, które uważam tak naprawdę za warte uwagi i stosowania (walidatora napiszemy i raczej nie będziemy go tak często zmieniać jak niektóre akcje).
Myślę że warto dodać do Twojego validatora weryfikację cyfry kontrolnej, która jest zarówno w NIP, PESEL jak i REGON. Algorytmy są proste a ich opis jest dostępny na polskiej Wikipedii.
Pozdrawiam,
Łukasz
Do repozytorium
http://maven2.czerwinski.info.pl/dodałem nowy projekt:<groupId>pl.info.czerwinski</groupId>
<artifactId>additional-hibernate-validators</artifactId>
<version>1.0-SNAPSHOT</version>
Na razie uzupełniłem walidator NIP, ale planuję dodać też odpowiednie klasy dla PESEL i REGON.
Forma zapisu z myślnikami nie ma znaczenia; obecnie to zaszłość i w walidatorach numeru NIP nie powinno się jej brać pod uwagę; zresztą to samo podpowiada zdrowy rozsądek. Powinno się za to brać pod uwagę liczbę kontrolną – Twój walidator jej nie analizuje, myślę, że warto byłoby to doimplementować. Poza tym dopuszczalny jest format z prefiksem PL jako oznaczenie kraju. Czyli PLxxxxxxxxxx, gdzie x to liczby całkowite z zakresu {0-9} może być poprawnym numerem NIP, pod warunkiem, że zostanie spełniony warunek o sumie kontrolnej.
We wpisie Dodatkowe walidatory Hibernate opisuję projekt, w którym uwzględniłem formę bez myślników i sprawdzam cyfrę kontrolną. Jeszcze kilka lat temu księgowi zwracali szczególną uwagę na położenie myślników, gdyż zdaniem niektórych mogły wystąpić dwa identyczne NIP – jeden dla osoby fizycznej, drugi dla osoby prawnej.
Jeżeli chodzi o prefiks „PL”, to nie spotkałem się z nim do tej pory; nie ma też o nim mowy w Wikipedii. Postaram się go uwzględnić w bibliotece jak najszybciej.
Pozdrawiam,
Sławek