View Javadoc
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    *      http://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 static org.junit.Assert.assertFalse;
20  import static org.junit.Assert.assertNotNull;
21  import static org.junit.Assert.assertNull;
22  import static org.junit.Assert.assertTrue;
23  import static org.junit.Assert.fail;
24  
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.InputStreamReader;
28  import java.io.Reader;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.csv.CSVFormat;
33  import org.apache.commons.csv.CSVParser;
34  import org.apache.commons.csv.CSVRecord;
35  import org.apache.commons.validator.routines.IBANValidator.Validator;
36  import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit;
37  import org.junit.Test;
38  
39  /**
40   * IBANValidator Test Case.
41   * @since 1.5.0
42   */
43  public class IBANValidatorTest {
44  
45      // It's not clear whether IBANs can contain lower case characters
46      // so we test for both where possible
47      // Note that the BIC near the start of the code is always upper case or digits
48      private final String[] validIBANFormat = new String[] {
49              "AD1200012030200359100100",
50              "AE070331234567890123456",
51              "AL47212110090000000235698741",
52              "AT611904300234573201",
53              "AZ21NABZ00000000137010001944",
54              "BA391290079401028494",
55              "BE68539007547034",
56              "BG80BNBG96611020345678",
57              "BH67BMAG00001299123456",
58              "BR1800000000141455123924100C2",
59              "BR1800360305000010009795493C1",
60              "BR9700360305000010009795493P1",
61              "BY13NBRB3600900000002Z00AB00",
62              "CH9300762011623852957",
63              "CR05015202001026284066",
64              "CY17002001280000001200527600",
65              "CZ6508000000192000145399",
66              "CZ9455000000001011038930",
67              "DE89370400440532013000",
68              "DK5000400440116243",
69              "DO28BAGR00000001212453611324",
70              "EE382200221020145685",
71              "EG380019000500000000263180002",
72              "ES9121000418450200051332",
73              "FI2112345600000785",
74              "FI5542345670000081",
75              "FO6264600001631634",
76              "FR1420041010050500013M02606",
77              "GB29NWBK60161331926819",
78              "GE29NB0000000101904917",
79              "GI75NWBK000000007099453",
80              "GL8964710001000206",
81              "GR1601101250000000012300695",
82              "GT82TRAJ01020000001210029690",
83              "HR1210010051863000160",
84              "HU42117730161111101800000000",
85              "IE29AIBK93115212345678",
86              "IL620108000000099999999",
87              "IQ98NBIQ850123456789012",
88              "IS140159260076545510730339",
89              "IT60X0542811101000000123456",
90              "JO94CBJO0010000000000131000302",
91              "KW81CBKU0000000000001234560101",
92              "KZ86125KZT5004100100",
93              "LB62099900000001001901229114",
94              "LC55HEMM000100010012001200023015",
95              "LI21088100002324013AA",
96              "LT121000011101001000",
97              "LU280019400644750000",
98              "LV80BANK0000435195001",
99              "MC5811222000010123456789030",
100             "MD24AG000225100013104168",
101             "ME25505000012345678951",
102             "MK07250120000058984",
103             "MR1300020001010000123456753",
104             "MT84MALT011000012345MTLCAST001S",
105             "MU17BOMM0101101030300200000MUR",
106             "NL91ABNA0417164300",
107             "NO9386011117947",
108             "PK36SCBL0000001123456702",
109             "PL61109010140000071219812874",
110             "PS92PALS000000000400123456702",
111             "PT50000201231234567890154",
112             "QA58DOHB00001234567890ABCDEFG",
113             "RO49AAAA1B31007593840000",
114             "RS35260005601001611379",
115             "SA0380000000608010167519",
116             "SC18SSCB11010000000000001497USD",
117             "SE4550000000058398257466",
118             "SI56191000000123438",
119             "SI56263300012039086",
120             "SK3112000000198742637541",
121             "SM86U0322509800000000270100",
122             "ST68000100010051845310112",
123             "SV62CENR00000000000000700025",
124             "SV43ACAT00000000000000123123",
125             "TL380080012345678910157",
126             "TN5910006035183598478831",
127             "TR330006100519786457841326",
128             "UA213223130000026007233566001",
129             "UA213996220000026007233566001",
130             "VA59001123000012345678",
131             "VG96VPVG0000012345678901",
132             "XK051212012345678906",
133     };
134 
135     private final String[] invalidIBANFormat = new String[] {
136             "",                        // empty
137             "   ",                     // empty
138             "A",                       // too short
139             "AB",                      // too short
140             "FR1420041010050500013m02606", // lowercase version
141             "MT84MALT011000012345mtlcast001s", // lowercase version
142             "LI21088100002324013aa", // lowercase version
143             "QA58DOHB00001234567890abcdefg", // lowercase version
144             "RO49AAAA1b31007593840000", // lowercase version
145             "LC62HEMM000100010012001200023015", // wrong in SWIFT
146             "BY00NBRB3600000000000Z00AB00", // Wrong in SWIFT v73
147             "ST68000200010192194210112", // ditto
148             "SV62CENR0000000000000700025", // ditto
149     };
150 
151     private static final IBANValidator VALIDATOR = IBANValidator.getInstance();
152 
153     @Test
154     public void testValid() {
155         for(String f : validIBANFormat) {
156             assertTrue("Checksum fail: "+f, IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(f));
157             assertTrue("Missing validator: "+f, VALIDATOR.hasValidator(f));
158             assertTrue(f, VALIDATOR.isValid(f));
159         }
160     }
161 
162     @Test
163     public void testInValid() {
164         for(String f : invalidIBANFormat) {
165             assertFalse(f, VALIDATOR.isValid(f));
166         }
167     }
168 
169     @Test
170     public void testNull() {
171         assertFalse("isValid(null)",  VALIDATOR.isValid(null));
172     }
173 
174     @Test
175     public void testHasValidator() {
176         assertTrue("GB", VALIDATOR.hasValidator("GB"));
177         assertFalse("gb", VALIDATOR.hasValidator("gb"));
178     }
179 
180     @Test
181     public void testGetValidator() {
182         assertNotNull("GB", VALIDATOR.getValidator("GB"));
183         assertNull("gb", VALIDATOR.getValidator("gb"));        
184     }
185 
186     @Test(expected=IllegalStateException.class)
187     public void testSetDefaultValidator1() {
188         assertNotNull(VALIDATOR.setValidator("GB", 15, "GB"));
189     }
190 
191     @Test(expected=IllegalStateException.class)
192     public void testSetDefaultValidator2() {
193         assertNotNull(VALIDATOR.setValidator("GB", -1, "GB"));
194     }
195 
196     @Test(expected=IllegalArgumentException.class)
197     public void testSetValidatorLC() {
198         IBANValidator validator = new IBANValidator();
199         assertNotNull(validator.setValidator("gb", 15, "GB"));
200     }
201 
202     @Test(expected=IllegalArgumentException.class)
203     public void testSetValidatorLen7() {
204         IBANValidator validator = new IBANValidator();
205         assertNotNull(validator.setValidator("GB", 7, "GB"));
206     }
207 
208     @Test(expected=IllegalArgumentException.class)
209     public void testSetValidatorLen35() {
210         IBANValidator validator = new IBANValidator();
211         assertNotNull(validator.setValidator("GB", 35, "GB")); // valid params, but immutable validator
212     }
213 
214     @Test
215     public void testSetValidatorLen_1() {
216         IBANValidator validator = new IBANValidator();
217         assertNotNull("should be present",validator.setValidator("GB", -1, ""));
218         assertNull("no longer present",validator.setValidator("GB", -1, ""));
219     }
220 
221     @Test
222     public void testSorted() {
223         IBANValidator validator = new IBANValidator();
224         Validator[] vals = validator.getDefaultValidators();
225         assertNotNull(vals);
226         for(int i=1; i < vals.length; i++) {
227             if (vals[i].countryCode.compareTo(vals[i-1].countryCode) <= 0) {
228                 fail("Not sorted: "+vals[i].countryCode+ " <= " + vals[i-1].countryCode);
229             }
230         }
231     }
232 
233     private static int checkIBAN(File file, IBANValidator val) throws Exception {
234         // The IBAN Registry (TXT) file is a TAB-separated file
235         // Rows are the entry types, columns are the countries
236         CSVFormat format = CSVFormat.DEFAULT.withDelimiter('\t');
237         Reader rdr = new InputStreamReader(new FileInputStream(file), "ISO_8859_1");
238         CSVParser p = new CSVParser(rdr, format);
239         CSVRecord country = null;
240         CSVRecord cc = null;
241         CSVRecord structure = null;
242         CSVRecord length = null;
243         for (CSVRecord o : p) {
244             String item = o.get(0);
245             if ("Name of country".equals(item)) {
246                 country = o;
247             } else if ("IBAN prefix country code (ISO 3166)".equals(item)) {
248                 cc = o;
249             } else if ("IBAN structure".equals(item)) {
250                 structure = o;
251             } else if ("IBAN length".equals(item)) {
252                 length = o;
253             }
254         }
255         for (int i=1; i < country.size(); i++) {
256           try {
257 
258             String newLength = length.get(i).split("!")[0]; // El Salvador currently has "28!n"
259             String newRE = fmtRE(structure.get(i), Integer.parseInt(newLength));
260             final Validator valre = val.getValidator(cc.get(i));
261             if (valre == null) {
262                 System.out.println("// Missing entry:");
263                 printEntry(
264                         cc.get(i),
265                         newLength,
266                         newRE,
267                         country.get(i));              
268             } else {
269                 String currentLength = Integer.toString(valre.lengthOfIBAN);
270                 String currentRE = valre.validator.toString()
271                         .replaceAll("^.+?\\{(.+)}","$1") // Extract RE from RegexValidator{re} string
272                         .replaceAll("\\\\d","\\\\\\\\d"); // convert \d to \\d
273                 // The above assumes that the RegexValidator contains a single Regex
274                 if (currentRE.equals(newRE) && currentLength.equals(newLength)) {
275 
276                 } else {
277                     System.out.println("// Expected: " + newRE + ", " + newLength + " Actual: " + currentRE + ", " + currentLength);
278                     printEntry(
279                             cc.get(i),
280                             newLength,
281                             newRE,
282                             country.get(i));              
283                 }
284 
285             }
286 
287           } catch (IllegalArgumentException e) {
288             e.printStackTrace();
289           }
290         }
291         p.close();
292         return country.size();
293     }
294 
295     private static void printEntry(String ccode, String length, String ib, String country) {
296         String fmt = String.format("\"%s\"", ib);
297         System.out.printf("            new Validator(\"%s\", %s, %-40s), // %s\n",
298                 ccode,
299                 length,
300                 fmt,
301                 country);        
302     }
303 
304     // Unfortunately Java only returns the last match of repeated patterns
305     // Use a manual repeat instead
306     private static final String IBAN_PART = "(?:(\\d+)!([acn]))"; // Assume all parts are fixed length
307     private static final Pattern IBAN_PAT = Pattern.compile(
308             "([A-Z]{2})"+IBAN_PART+IBAN_PART+IBAN_PART+IBAN_PART+"?"+IBAN_PART+"?"+IBAN_PART+"?"+IBAN_PART+"?");
309 
310     // convert IBAN type string and length to regex
311     private static String formatToRE(String type, int len) {
312         char ctype = type.charAt(0); // assume type.length() == 1
313         switch(ctype) {
314         case 'n':
315             return String.format("\\\\d{%d}",len);
316         case 'a':
317             return String.format("[A-Z]{%d}",len);
318         case 'c':
319             return String.format("[A-Z0-9]{%d}",len);
320         default:
321             throw new IllegalArgumentException("Unexpected type " + type);
322         }
323     }
324 
325     private static String fmtRE(String iban_pat, int iban_len) {
326         Matcher m = IBAN_PAT.matcher(iban_pat);
327         if (m.matches()) {
328             StringBuilder sb = new StringBuilder();
329             String cc = m.group(1); // country code
330             int totalLen = cc.length();
331             sb.append(cc);
332             int len = Integer.parseInt(m.group(2)); // length of part
333             String curType = m.group(3); // part type
334             for (int i = 4; i <= m.groupCount(); i += 2) {
335                 if (m.group(i) == null) { // reached an optional group
336                     break;
337                 }
338                 int count = Integer.parseInt(m.group(i));
339                 String type = m.group(i+1);
340                 if (type.equals(curType)) { // more of the same type
341                     len += count;                    
342                 } else {
343                     sb.append(formatToRE(curType,len));
344                     totalLen += len;
345                     curType = type;
346                     len = count;
347                 }
348             }
349             sb.append(formatToRE(curType,len));
350             totalLen += len;
351             if (iban_len != totalLen) {
352                 throw new IllegalArgumentException("IBAN pattern " + iban_pat + " does not match length " + iban_len);                
353             }
354             return sb.toString();
355         } else {
356             throw new IllegalArgumentException("Unexpected IBAN pattern " + iban_pat);
357         }
358     }
359 
360     public static void main(String [] a) throws Exception {
361         IBANValidator validator = new IBANValidator();
362         File iban_tsv = new File("target","iban-registry.tsv");
363         int countries = 0;
364         if (iban_tsv.canRead()) {
365             countries = checkIBAN(iban_tsv, validator);
366         } else {
367             System.out.println("Please load the file " + iban_tsv.getCanonicalPath() + " from https://www.swift.com/standards/data-standards/iban");
368         }
369         System.out.println("Processed " + countries + " countries.");
370     }
371 }