Source code
Description: source: package: org.apache.commons.validator.routines, class: IBANValidator

    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.validator.routines;
    018
    019import java.util.ArrayList;
    020import java.util.Arrays;
    021import java.util.List;
    022import java.util.concurrent.ConcurrentHashMap;
    023import java.util.concurrent.ConcurrentMap;
    024
    025import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit;
    026
    027/**
    028 * IBAN Validator.
    029 * <p>
    030 * Checks an IBAN for:
    031 * </p>
    032 * <ul>
    033 * <li>country code prefix</li>
    034 * <li>IBAN length</li>
    035 * <li>pattern (digits and/or uppercase letters)</li>
    036 * <li>IBAN Checkdigits (using {@link IBANCheckDigit})</li>
    037 * </ul>
    038 * The class does not perform checks on the embedded BBAN (Basic Bank Account Number).
    039 * Each country has its own rules for these.
    040 * <p>
    041 * The validator includes a default set of formats derived from the IBAN registry at
    042 * https://www.swift.com/standards/data-standards/iban.
    043 * </p>
    044 * <p>
    045 * This can get out of date, but the set can be adjusted by creating a validator and using the
    046 * {@link #setValidator(String, int, String)} or
    047 * {@link #setValidator(Validator)}
    048 * method to add (or remove) an entry.
    049 * </p>
    050 * <p>
    051 * For example:
    052 * </p>
    053 * <pre>
    054 * IBANValidator ibv = new IBANValidator();
    055 * ibv.setValidator("XX", 12, "XX\\d{10}")
    056 * </pre>
    057 * <p>
    058 * The singleton default instance cannot be modified in this way.
    059 * </p>
    060 *
    061 * @since 1.5.0
    062 */
    063public class IBANValidator {
    064
    065    /**
    066     * The validation class
    067     */
    068    public static class Validator {
    069
    070        /**
    071         * The minimum length does not appear to be defined by the standard.
    072         * Norway is currently the shortest at 15.
    073         *
    074         * There is no standard for BBANs; they vary between countries.
    075         * But a BBAN must consist of a branch id and account number.
    076         * Each of these must be at least 2 chars (generally more) so an absolute minimum is
    077         * 4 characters for the BBAN and 8 for the IBAN.
    078         */
    079        private static final int MIN_LEN = 8;
    080        private static final int MAX_LEN = 34; // defined by [3]
    081
    082        final String countryCode;
    083        final String[] otherCountryCodes;
    084        final RegexValidator regexValidator;
    085
    086        /**
    087         * Used to avoid unnecessary regex matching.
    088         */
    089        private final int ibanLength;
    090
    091        /**
    092         * Creates the validator.
    093         *
    094         * @param countryCode The country code.
    095         * @param ibanLength The length of the IBAN.
    096         * @param regexWithCC The regex to use to check the format, the regex MUST start with the country code.
    097         */
    098        public Validator(final String countryCode, final int ibanLength, final String regexWithCC) {
    099            this(countryCode, ibanLength, regexWithCC.substring(countryCode.length()), new String[] {});
    100        }
    101
    102        /**
    103         * Creates the validator.
    104         *
    105         * @param countryCode The country code.
    106         * @param ibanLength The length of the IBAN.
    107         * @param regexWithoutCC The regex to use to check the format, the regex MUST NOT start with the country code.
    108         */
    109        Validator(final String countryCode, final int ibanLength, final String regexWithoutCC, final String... otherCountryCodes) {
    110            if (!(countryCode.length() == 2 && Character.isUpperCase(countryCode.charAt(0)) && Character.isUpperCase(countryCode.charAt(1)))) {
    111                throw new IllegalArgumentException("Invalid country Code; must be exactly 2 upper-case characters");
    112            }
    113            if (ibanLength > MAX_LEN || ibanLength < MIN_LEN) {
    114                throw new IllegalArgumentException("Invalid length parameter, must be in range " + MIN_LEN + " to " + MAX_LEN + " inclusive: " + ibanLength);
    115            }
    116            this.countryCode = countryCode;
    117            this.otherCountryCodes = otherCountryCodes.clone();
    118            final List<String> regexList = new ArrayList<>(this.otherCountryCodes.length + 1);
    119            regexList.add(countryCode + regexWithoutCC);
    120            for (final String otherCc : otherCountryCodes) {
    121                regexList.add(otherCc + regexWithoutCC);
    122            }
    123            this.ibanLength = ibanLength;
    124            this.regexValidator = new RegexValidator(regexList);
    125        }
    126
    127        /**
    128         * Gets the length.
    129         *
    130         * @return The length.
    131         * @since 1.10.0
    132         */
    133        public int getIbanLength() {
    134            return ibanLength;
    135        }
    136
    137        /**
    138         * Gets the RegexValidator.
    139         *
    140         * @return The RegexValidator.
    141         * @since 1.8
    142         */
    143        public RegexValidator getRegexValidator() {
    144            return regexValidator;
    145        }
    146    }
    147
    148    private static final int SHORT_CODE_LEN = 2;
    149
    150    /*
    151     * Note: the IBAN PDF registry file implies that IBANs can contain lower-case letters.
    152     * However, several other documents state that IBANs must be upper-case only.
    153     * See the comment block following this array.
    154     *
    155     * In the Regexes below, only upper-case is used.
    156     */
    157    private static final Validator[] DEFAULT_VALIDATORS = {
    158            // @formatter:off
    159            new Validator("AD", 24, "AD\\d{10}[A-Z0-9]{12}"),                 // Andorra
    160            new Validator("AE", 23, "AE\\d{21}"),                             // United Arab Emirates (The)
    161            new Validator("AL", 28, "AL\\d{10}[A-Z0-9]{16}"),                 // Albania
    162            new Validator("AT", 20, "AT\\d{18}"),                             // Austria
    163            new Validator("AZ", 28, "AZ\\d{2}[A-Z]{4}[A-Z0-9]{20}"),          // Azerbaijan
    164            new Validator("BA", 20, "BA\\d{18}"),                             // Bosnia and Herzegovina
    165            new Validator("BE", 16, "BE\\d{14}"),                             // Belgium
    166            new Validator("BG", 22, "BG\\d{2}[A-Z]{4}\\d{6}[A-Z0-9]{8}"),     // Bulgaria
    167            new Validator("BH", 22, "BH\\d{2}[A-Z]{4}[A-Z0-9]{14}"),          // Bahrain
    168            new Validator("BI", 27, "BI\\d{25}"),                             // Burundi
    169            new Validator("BR", 29, "BR\\d{25}[A-Z]{1}[A-Z0-9]{1}"),          // Brazil
    170            new Validator("BY", 28, "BY\\d{2}[A-Z0-9]{4}\\d{4}[A-Z0-9]{16}"), // Republic of Belarus
    171            new Validator("CH", 21, "CH\\d{7}[A-Z0-9]{12}"),                  // Switzerland
    172            new Validator("CR", 22, "CR\\d{20}"),                             // Costa Rica
    173            new Validator("CY", 28, "CY\\d{10}[A-Z0-9]{16}"),                 // Cyprus
    174            new Validator("CZ", 24, "CZ\\d{22}"),                             // Czechia
    175            new Validator("DE", 22, "DE\\d{20}"),                             // Germany
    176            new Validator("DJ", 27, "DJ\\d{25}"),                             // Djibouti
    177            new Validator("DK", 18, "DK\\d{16}"),                             // Denmark
    178            new Validator("DO", 28, "DO\\d{2}[A-Z0-9]{4}\\d{20}"),            // Dominican Republic
    179            new Validator("EE", 20, "EE\\d{18}"),                             // Estonia
    180            new Validator("EG", 29, "EG\\d{27}"),                             // Egypt
    181            new Validator("ES", 24, "ES\\d{22}"),                             // Spain
    182            new Validator("FI", 18, "\\d{16}", "AX"),                         // Finland
    183            new Validator("FK", 18, "FK\\d{2}[A-Z]{2}\\d{12}"),               // Falkland Islands, since Jul-23
    184            new Validator("FO", 18, "FO\\d{16}"),                             // Faroe Islands
    185            new Validator("FR", 27, "\\d{12}[A-Z0-9]{11}\\d{2}", "GF", "GP", "MQ", "RE", "PF", "TF", "YT", "NC", "BL", "MF", "PM", "WF"), // France
    186            new Validator("GB", 22, "\\d{2}[A-Z]{4}\\d{14}", "IM", "JE", "GG"), // United Kingdom
    187            new Validator("GE", 22, "GE\\d{2}[A-Z]{2}\\d{16}"),               // Georgia
    188            new Validator("GI", 23, "GI\\d{2}[A-Z]{4}[A-Z0-9]{15}"),          // Gibraltar
    189            new Validator("GL", 18, "GL\\d{16}"),                             // Greenland
    190            new Validator("GR", 27, "GR\\d{9}[A-Z0-9]{16}"),                  // Greece
    191            new Validator("GT", 28, "GT\\d{2}[A-Z0-9]{24}"),                  // Guatemala
    192            new Validator("HN", 28, "HN\\d{2}[A-Z]{4}\\d{20}"),               // Honduras, since Dec-24
    193            new Validator("HR", 21, "HR\\d{19}"),                             // Croatia
    194            new Validator("HU", 28, "HU\\d{26}"),                             // Hungary
    195            new Validator("IE", 22, "IE\\d{2}[A-Z]{4}\\d{14}"),               // Ireland
    196            new Validator("IL", 23, "IL\\d{21}"),                             // Israel
    197            new Validator("IQ", 23, "IQ\\d{2}[A-Z]{4}\\d{15}"),               // Iraq
    198            new Validator("IS", 26, "IS\\d{24}"),                             // Iceland
    199            new Validator("IT", 27, "IT\\d{2}[A-Z]{1}\\d{10}[A-Z0-9]{12}"),   // Italy
    200            new Validator("JO", 30, "JO\\d{2}[A-Z]{4}\\d{4}[A-Z0-9]{18}"),    // Jordan
    201            new Validator("KW", 30, "KW\\d{2}[A-Z]{4}[A-Z0-9]{22}"),          // Kuwait
    202            new Validator("KZ", 20, "KZ\\d{5}[A-Z0-9]{13}"),                  // Kazakhstan
    203            new Validator("LB", 28, "LB\\d{6}[A-Z0-9]{20}"),                  // Lebanon
    204            new Validator("LC", 32, "LC\\d{2}[A-Z]{4}[A-Z0-9]{24}"),          // Saint Lucia
    205            new Validator("LI", 21, "LI\\d{7}[A-Z0-9]{12}"),                  // Liechtenstein
    206            new Validator("LT", 20, "LT\\d{18}"),                             // Lithuania
    207            new Validator("LU", 20, "LU\\d{5}[A-Z0-9]{13}"),                  // Luxembourg
    208            new Validator("LV", 21, "LV\\d{2}[A-Z]{4}[A-Z0-9]{13}"),          // Latvia
    209            new Validator("LY", 25, "LY\\d{23}"),                             // Libya
    210            new Validator("MC", 27, "MC\\d{12}[A-Z0-9]{11}\\d{2}"),           // Monaco
    211            new Validator("MD", 24, "MD\\d{2}[A-Z0-9]{20}"),                  // Moldova
    212            new Validator("ME", 22, "ME\\d{20}"),                             // Montenegro
    213            new Validator("MK", 19, "MK\\d{5}[A-Z0-9]{10}\\d{2}"),            // Macedonia
    214            new Validator("MN", 20, "MN\\d{18}"),                             // Mongolia, since Apr-23
    215            new Validator("MR", 27, "MR\\d{25}"),                             // Mauritania
    216            new Validator("MT", 31, "MT\\d{2}[A-Z]{4}\\d{5}[A-Z0-9]{18}"),    // Malta
    217            new Validator("MU", 30, "MU\\d{2}[A-Z]{4}\\d{19}[A-Z]{3}"),       // Mauritius
    218            new Validator("NI", 28, "NI\\d{2}[A-Z]{4}\\d{20}"),               // Nicaragua, since Apr-23
    219            new Validator("NL", 18, "NL\\d{2}[A-Z]{4}\\d{10}"),               // Netherlands (The)
    220            new Validator("NO", 15, "NO\\d{13}"),                             // Norway
    221            new Validator("OM", 23, "OM\\d{5}[A-Z0-9]{16}"),                  // Oman, since Mar-24
    222            new Validator("PK", 24, "PK\\d{2}[A-Z]{4}[A-Z0-9]{16}"),          // Pakistan
    223            new Validator("PL", 28, "PL\\d{26}"),                             // Poland
    224            new Validator("PS", 29, "PS\\d{2}[A-Z]{4}[A-Z0-9]{21}"),          // Palestine, State of
    225            new Validator("PT", 25, "PT\\d{23}"),                             // Portugal
    226            new Validator("QA", 29, "QA\\d{2}[A-Z]{4}[A-Z0-9]{21}"),          // Qatar
    227            new Validator("RO", 24, "RO\\d{2}[A-Z]{4}[A-Z0-9]{16}"),          // Romania
    228            new Validator("RS", 22, "RS\\d{20}"),                             // Serbia
    229            new Validator("RU", 33, "RU\\d{16}[A-Z0-9]{15}"),                 // Russia
    230            new Validator("SA", 24, "SA\\d{4}[A-Z0-9]{18}"),                  // Saudi Arabia
    231            new Validator("SC", 31, "SC\\d{2}[A-Z]{4}\\d{20}[A-Z]{3}"),       // Seychelles
    232            new Validator("SD", 18, "SD\\d{16}"),                             // Sudan
    233            new Validator("SE", 24, "SE\\d{22}"),                             // Sweden
    234            new Validator("SI", 19, "SI\\d{17}"),                             // Slovenia
    235            new Validator("SK", 24, "SK\\d{22}"),                             // Slovakia
    236            new Validator("SM", 27, "SM\\d{2}[A-Z]{1}\\d{10}[A-Z0-9]{12}"),   // San Marino
    237            new Validator("SO", 23, "SO\\d{21}"),                             // Somalia, since Feb-23
    238            new Validator("ST", 25, "ST\\d{23}"),                             // Sao Tome and Principe
    239            new Validator("SV", 28, "SV\\d{2}[A-Z]{4}\\d{20}"),               // El Salvador
    240            new Validator("TL", 23, "TL\\d{21}"),                             // Timor-Leste
    241            new Validator("TN", 24, "TN\\d{22}"),                             // Tunisia
    242            new Validator("TR", 26, "TR\\d{8}[A-Z0-9]{16}"),                  // Turkey
    243            new Validator("UA", 29, "UA\\d{8}[A-Z0-9]{19}"),                  // Ukraine
    244            new Validator("VA", 22, "VA\\d{20}"),                             // Vatican City State
    245            new Validator("VG", 24, "VG\\d{2}[A-Z]{4}\\d{16}"),               // Virgin Islands
    246            new Validator("XK", 20, "XK\\d{18}"),                             // Kosovo
    247            new Validator("YE", 30, "YE\\d{2}[A-Z]{4}\\d{4}[A-Z0-9]{18}"),    // Yemen
    248            // @formatter:on
    249    };
    250
    251    /*
    252     * Wikipedia [1] says that only uppercase is allowed.
    253     * The SWIFT PDF file [2] implies that lower case is allowed.
    254     * However, there are no examples using lower-case.
    255     * Unfortunately the relevant ISO documents (ISO 13616-1) are not available for free.
    256     * The IBANCheckDigit code treats upper and lower case the same,
    257     * so any case validation has to be done in this class.
    258     *
    259     * Note: the European Payments council has a document [3] which includes a description
    260     * of the IBAN. Section 5 clearly states that only upper case is allowed.
    261     * Also, the maximum length is 34 characters (including the country code),
    262     * and the length is fixed for each country.
    263     *
    264     * It looks like lower-case is permitted in BBANs, but they must be converted to
    265     * upper case for IBANs.
    266     *
    267     * [1] https://en.wikipedia.org/wiki/International_Bank_Account_Number
    268     * [2] http://www.swift.com/dsp/resources/documents/IBAN_Registry.pdf (404)
    269     * => https://www.swift.com/sites/default/files/resources/iban_registry.pdf
    270     * The above is an old version (62, Jan 2016)
    271     * As of May 2020, the current IBAN standards are located at:
    272     * https://www.swift.com/standards/data-standards/iban
    273     * The above page contains links for the PDF and TXT (CSV) versions of the registry
    274     * Warning: these may not agree -- in the past there have been discrepancies.
    275     * The TXT file can be used to determine changes which can be cross-checked in the PDF file.
    276     * [3] http://www.europeanpaymentscouncil.eu/documents/ECBS%20IBAN%20standard%20EBS204_V3.2.pdf
    277     */
    278
    279    /** The singleton instance which uses the default formats */
    280    public static final IBANValidator DEFAULT_IBAN_VALIDATOR = new IBANValidator();
    281
    282    /**
    283     * Gets the singleton instance of the IBAN validator using the default formats.
    284     *
    285     * @return A singleton instance of the IBAN validator.
    286     */
    287    public static IBANValidator getInstance() {
    288        return DEFAULT_IBAN_VALIDATOR;
    289    }
    290
    291    private final ConcurrentMap<String, Validator> validatorMap;
    292
    293    /**
    294     * Create a default IBAN validator.
    295     */
    296    public IBANValidator() {
    297        this(DEFAULT_VALIDATORS);
    298    }
    299
    300    /**
    301     * Create an IBAN validator from the specified map of IBAN formats.
    302     *
    303     * @param validators map of IBAN formats.
    304     */
    305    public IBANValidator(final Validator[] validators) {
    306        this.validatorMap = createValidators(validators);
    307    }
    308
    309    private ConcurrentMap<String, Validator> createValidators(final Validator[] validators) {
    310        final ConcurrentMap<String, Validator> map = new ConcurrentHashMap<>();
    311        for (final Validator validator : validators) {
    312            map.put(validator.countryCode, validator);
    313            for (final String otherCC : validator.otherCountryCodes) {
    314                map.put(otherCC, validator);
    315            }
    316        }
    317        return map;
    318    }
    319
    320    /**
    321     * Gets a copy of the default Validators.
    322     *
    323     * @return A copy of the default Validator array.
    324     */
    325    public Validator[] getDefaultValidators() {
    326        return Arrays.copyOf(DEFAULT_VALIDATORS, DEFAULT_VALIDATORS.length);
    327    }
    328
    329    /**
    330     * Gets the Validator for a given IBAN.
    331     *
    332     * @param code A string starting with the ISO country code (for example, an IBAN).
    333     * @return The validator or {@code null} if there is not one registered.
    334     */
    335    public Validator getValidator(final String code) {
    336        if (code == null || code.length() < SHORT_CODE_LEN) { // ensure we can extract the code
    337            return null;
    338        }
    339        final String key = code.substring(0, SHORT_CODE_LEN);
    340        return validatorMap.get(key);
    341    }
    342
    343    /**
    344     * Tests if the given code has a validator.
    345     *
    346     * @param code The code to check.
    347     * @return true if there is a validator.
    348     */
    349    public boolean hasValidator(final String code) {
    350        return getValidator(code) != null;
    351    }
    352
    353    /**
    354     * Tests an IBAN Code.
    355     *
    356     * @param code The value validation is being performed on.
    357     * @return {@code true} if the value is valid.
    358     */
    359    public boolean isValid(final String code) {
    360        return validate(code) == IBANValidatorStatus.VALID;
    361    }
    362
    363    /**
    364     * Installs a validator. Will replace any existing entry which has the same countryCode.
    365     *
    366     * @param countryCode The country code.
    367     * @param length      The length of the IBAN. Must be &ge; 8 and &le; 32. If the length is &lt; 0, the validator is removed, and the format is not used.
    368     * @param format      The format of the IBAN (as a regular expression).
    369     * @return The previous Validator, or {@code null} if there was none.
    370     * @throws IllegalArgumentException if there is a problem.
    371     * @throws IllegalStateException    if an attempt is made to modify the singleton validator.
    372     */
    373    public Validator setValidator(final String countryCode, final int length, final String format) {
    374        if (this == DEFAULT_IBAN_VALIDATOR) {
    375            throw new IllegalStateException("The singleton validator cannot be modified");
    376        }
    377        if (length < 0) {
    378            final Validator prev = validatorMap.remove(countryCode);
    379            if (prev != null) {
    380                for (final String otherCC : prev.otherCountryCodes) {
    381                    validatorMap.remove(otherCC);
    382                }
    383            }
    384            return prev;
    385        }
    386        return setValidator(new Validator(countryCode, length, format));
    387    }
    388
    389    /**
    390     * Installs a validator. Will replace any existing entry which has the same countryCode.
    391     *
    392     * @param validator The instance to install.
    393     * @return The previous Validator, or {@code null} if there was none.
    394     * @throws IllegalStateException if an attempt is made to modify the singleton validator.
    395     */
    396    public Validator setValidator(final Validator validator) {
    397        if (this == DEFAULT_IBAN_VALIDATOR) {
    398            throw new IllegalStateException("The singleton validator cannot be modified");
    399        }
    400        final Validator prev = validatorMap.put(validator.countryCode, validator);
    401        if (prev != null) {
    402            for (final String otherCC : prev.otherCountryCodes) {
    403                validatorMap.remove(otherCC);
    404            }
    405        }
    406        for (final String otherCC : validator.otherCountryCodes) {
    407            validatorMap.put(otherCC, validator);
    408        }
    409        return prev;
    410    }
    411
    412    /**
    413     * Validate an IBAN Code.
    414     *
    415     * @param code The value validation is being performed on.
    416     * @return {@link IBANValidatorStatus} for validation.
    417     * @since 1.10.0
    418     */
    419    public IBANValidatorStatus validate(final String code) {
    420        final Validator formatValidator = getValidator(code);
    421        if (formatValidator == null) {
    422            return IBANValidatorStatus.UNKNOWN_COUNTRY;
    423        }
    424        if (code.length() != formatValidator.ibanLength) {
    425            return IBANValidatorStatus.INVALID_LENGTH;
    426        }
    427        if (!formatValidator.regexValidator.isValid(code)) {
    428            return IBANValidatorStatus.INVALID_PATTERN;
    429        }
    430        return IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(code) ? IBANValidatorStatus.VALID : IBANValidatorStatus.INVALID_CHECKSUM;
    431    }
    432}