„Nicht mit Erfindungen, sondern mit Verbesserungen macht man Vermögen.“
Henry Ford
Nachdem im vorherigen Beitrag zum Thema, die Prüfung der deutschen IBAN und BIC verbessert wurde, rundet dieser Beitrag die Validierung ab.
Der deutsche BIC wird gegen Einträge in der BLZ.txt
geprüft. Für andere Länder steht eine solche Datei vermutlich nicht bereit. Daher liefert die bisherige Implementierung für syntaktisch korrekte BIC den Wert true
zurück. Wie bereits erwähnt ist dies ein wenig kontraintuitiv, daher wird das Verhalten an dieser Stelle konfigurierbar gestaltet.
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface BIC { String message() default "{de.schegge.bank.validator.BIC.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; boolean lenient() default false; }
Die Validierungsannotation BIC
erhält einen optionales Attribut lenient
. Wird dieses Attribute auf true gesetzt, dann wird das bisherige Verhalten beibehalten. Standardmäßig ist jetzt aber der Wert false
gesetzt. Dies bedeutet, ohne einen BankService
für die entsprechende Sprache, ist die Validierung nicht mehr erfolgreich.
@Data private MemberDto { // ... @BIC private String notLenientBic; @BIC(lenient=true) private String lenientBic; }
Im obigen Beispiel kann die notLenientBic
nur erfolgreich validiert werden, wenn es sich um einen deutsche BIC handelt. Andere Sprachen liefern nur true
zurück, wenn jemand einen entsprechenden BankService
bereitstellt.
Die Änderungen an der Implementierung sind recht einfach, weil in der letzten Zeile, statt true
, nur der aktuelle lenient
Wert der Annotation zurückgegeben werden muss.
public class BicValidator implements ConstraintValidator<BIC, String> { private static final Pattern PATTERN = Pattern.compile("([A-Z0-9]{4})([A-Z]{2})([A-Z0-9]{2})([A-Z0-9]{3})?"); private boolean lenient; @Override public void initialize(BIC constraintAnnotation) { this.lenient = constraintAnnotation.lenient(); } @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) { return true; } Matcher matcher = PATTERN.matcher(value); if (!matcher.matches()) { return false; } Optional<BankService> bankService = BankService.byCountry(matcher.group(2)); return bankService.map(service -> !service.byBankIdentifierCode(value).isEmpty()).orElseGet(() -> lenient); } }
Ähnliches soll auch für die IBAN möglich sein. Zum einen erhält die Annotation IBAN
ein lenient
Attribut um das Validierungsverhalten zu ändern. Zum anderen kann über das Attribut type
entschieden werden, ob eine IBAN gegen alle unterstützten Länder, den Ländern im SEPA-Raum oder denen außerhalb des SEPA-Raums geprüft werden soll. Teilweise IBAN-Länder (Experimentell) wie Algerien werden momentan noch nicht unterstützt.
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface IBAN { enum IbanType { ALL, SEPA, NO_SEPA } String message() default "{de.schegge.bank.validator.IBAN.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; boolean lenient() default false; IbanType type() default IbanType.ALL; }
Im folgenden Beispiel sind drei Anwendungsfälle der Annotation dargestellt. Die ersten beiden Fälle erlauben unbekannte Ländercodes. Jemand könnte also eine IBAN für Mordor mit dem unbekannten Länderkürzel MD
erzeugen und sie würde bei korrekter Prüfsumme die Validierung bestehen. Im zweiten Fall würde die Validierung bei einem bekannten Ländercode aber nur gelingen, wenn es ein SEPA Land wie Deutschland oder Frankreich ist. Der Ländercode von Kuwait würde abgelehnt. Die beiden letzten Annotationen arbeiten ähnlich, aber sie erlauben keine Validierung bei unbekannten Ländercodes.
@Data private MemberDto { // ... @IBAN(lenient=true) private String lenientIban; @IBAN(lenient=true,type=IbanType=SEPA) private String lenientSepaIban; @IBAN(type=IbanType=SEPA) private String nonLenientIban; @IBAN private String nonLenientSepaIban; }
Die Implementierung ist wegen der Überprüfung der Ländercodes etwas aufwändiger. Der Großteil der Arbeit ist aber in die Hilfsklasse IbanLocale
ausgegliedert.
In der isValid
Methode des IbanValidators
, wird ein IbanLocale
für die aktuelle Sprache geholt und damit einige Prüfungen auf der IBAN durchgeführt.
@Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) { return true; } Matcher matcher = PATTERN.matcher(value); if (!matcher.matches()) { return false; } String countryCode = matcher.group(1); String basicBankAccountNumber = matcher.group(3); if (isValidChecksum(matcher, countryCode, basicBankAccountNumber)) { return false; } IbanLocale ibanLocale = IbanLocale.byCode(countryCode); if (ibanLocale == null) { return lenient; } if (type == IbanType.SEPA && !IbanLocale.allSepaIbanLocales().contains(ibanLocale)) { return false; } if (type == IbanType.NO_SEPA && IbanLocale.allSepaIbanLocales().contains(ibanLocale)) { return false; } if (basicBankAccountNumber.length() != ibanLocale.getLength() - 4) { return false; } Optional<BankService> bankService = BankService.byCountry(countryCode); if (bankService.isEmpty()) { return lenient; } return bankService.map(x -> x.byBasicBankAcountNumber(basicBankAccountNumber)).isPresent(); }
Für IbanType.SEPA
wird geschaut, ob der IbanLocale
in der entsprechenden Gruppe enthalten ist. Für IbanType.NO_SEPA
wird das Gegenteil geprüft. Zusätzlich wird geprüft, ob die aktuelle Länge der IBAN mit der im IbanLocale
hinterlegten nationalen Länge übereinstimmt. Bislang war es noch immer möglich, eine deutsche IBAN mit 23 Stellen erfolgreich zu validieren.
Die Klasse IbanLocale
ist der Standardklasse Locale
nachempfunden. In ihr sind einige IBAN spezifische nationale Daten hinterlegt. Insbesondere die Länge der IBAN und die Zugehörigkeit des Landes zum SEPA-Raum.
public class IbanLocale { private static final Map<String, IbanLocale> IBAN_LOCALES = new HashMap<>(); private static final Set<IbanLocale> SEPA = new HashSet<>(); public static final IbanLocale GERMANY; static { GERMANY = createConstant("DE", true, 22); try { Properties properties = new Properties(); properties.load(IbanLocale.class.getResourceAsStream("ibanLocales.properties")); properties.forEach((code, value) -> createConstant(String.valueOf(code), String.valueOf(value))); IBAN_LOCALES.values().stream().filter(IbanLocale::isSepa).forEach(SEPA::add); } catch (RuntimeException | IOException e) { throw new ExceptionInInitializerError("cannot find iban locales"); } } private static void createConstant(String code, String value) { String[] parts = value.split(","); createConstant(code, "true".equals(parts[0]), Integer.parseInt(parts[1])); } private static IbanLocale createConstant(String country, boolean sepa, int length) { return IBAN_LOCALES.computeIfAbsent(country, c -> new IbanLocale(country, sepa, length)); } public static IbanLocale byCode(String country) { return IBAN_LOCALES.get(country); } public static Set<IbanLocale> allIbanLocales() { return Set.copyOf(IBAN_LOCALES.values()); } public static Set<IbanLocale> allSepaIbanLocales() { return Set.copyOf(SEPA); } private final String country; private final boolean sepa; private final int length; public IbanLocale(String country, boolean sepa, int length) { this.country = country; this.sepa = sepa; this.length = length; } public String getCountry() { return country; } public boolean isSepa() { return sepa; } public int getLength() { return length; } @Override public String toString() { return "iban country-code=" + country + ", sepa=" + sepa + ", length=" + length; } }
Im static
Block der Klasse wird die Konstante GERMANY
mit dem IbanLocale
für Deutschland initialisiert und alle anderen IbanLocale
Instanzen aus einer Property Datei initialisiert. Mit der statischen Methode byCode
kann auf das IbanLocale
für einen hinterlegten Ländercode zugegriffen werden. Über entsprechende Getter-Methoden der Instanz kann dann auf die jeweilige nationale Konfiguration zugegriffen werden.
Interessierte Leser können die Bibliothek gerne ausprobieren. Sie ist auf Maven Central zu finden.
<dependency> <groupId>de.schegge</groupId> <artifactId>bank-account-validator</artifactId> <version>0.1.0</version> </dependency>
Am Ende noch einmal der wichtige Hinweis. Eine validierte IBAN ist keine verifizierte IBAN. Eine Verifizierung kann nur die entsprechende Bank oder ein ermächtigter Finanzdienstleister durchführen. Eine valide IBAN mit korrekter Länderkennung und Bankleitzahl kann jede Person erstellen. Dafür sind nur Kenntnisse aus den öffentlichen Quellen notwendig, die auch Grundlage für diese Beiträge waren.