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