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