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