001// SECTION-START[License Header] 002// <editor-fold defaultstate="collapsed" desc=" Generated License "> 003/* 004 * jDTAUS ⁑ ISO-13616 005 * Copyright (C) Christian Schulte, 2013-222 006 * 007 * Permission to use, copy, modify, and/or distribute this software for any 008 * purpose with or without fee is hereby granted, provided that the above 009 * copyright notice and this permission notice appear in all copies. 010 * 011 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 012 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 013 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 014 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 015 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 016 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 017 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 018 * 019 * $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ 020 * 021 */ 022// </editor-fold> 023// SECTION-END 024package org.jdtaus.iso13616; 025 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.Serializable; 029import java.lang.ref.Reference; 030import java.lang.ref.SoftReference; 031import java.math.BigInteger; 032import java.net.URL; 033import java.text.ParseException; 034import java.text.ParsePosition; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Collections; 038import java.util.Formattable; 039import java.util.Formatter; 040import java.util.HashMap; 041import java.util.IllegalFormatFlagsException; 042import java.util.List; 043import java.util.Map; 044import java.util.Properties; 045import java.util.concurrent.ConcurrentHashMap; 046 047import static java.util.FormattableFlags.ALTERNATE; 048import static java.util.FormattableFlags.LEFT_JUSTIFY; 049import static java.util.FormattableFlags.UPPERCASE; 050 051// SECTION-START[Documentation] 052// <editor-fold defaultstate="collapsed" desc=" Generated Documentation "> 053/** 054 * 055 * International Bank Account Number. 056 * <p>The IBAN structure is defined in ISO 13616-1 and consists of a two-letter ISO 3166-1 country code, followed by two 057 * check digits and up to thirty alphanumeric characters for a BBAN (Basic Bank Account Number) which has a fixed length 058 * per country and, included within it, a bank identifier with a fixed position and a fixed length per country. The 059 * check digits are calculated based on the scheme defined in ISO/IEC 7064 (MOD97-10). The Society for Worldwide 060 * Interbank Financial Telecommunication SCRL, SWIFT, has been designated by the ISO Technical Management Board to act 061 * as the Registration Authority for ISO 13616. Nationally-agreed, ISO 13616-compliant IBAN formats are submitted to the 062 * registration authority exclusively by the National Standards Body or the National Central Bank of the country. For 063 * further information see the <a href="../../../doc-files/IBAN-Registry_Release-78-August-2017.pdf">IBAN REGISTRY</a>. 064 * An updated version of the document may be found at <a href="http://www.swift.com">SWIFT</a>.</p> 065 * 066 * <dl> 067 * <dt><b>Identifier:</b></dt><dd>jDTAUS ⁑ ISO-13616 ⁑ IBAN</dd> 068 * <dt><b>Name:</b></dt><dd>jDTAUS ⁑ ISO-13616 ⁑ IBAN</dd> 069 * <dt><b>Abstract:</b></dt><dd>No</dd> 070 * <dt><b>Final:</b></dt><dd>Yes</dd> 071 * <dt><b>Stateless:</b></dt><dd>No</dd> 072 * </dl> 073 * 074 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 075 * @version 1.1 076 */ 077// </editor-fold> 078// SECTION-END 079// SECTION-START[Annotations] 080// <editor-fold defaultstate="collapsed" desc=" Generated Annotations "> 081@javax.annotation.Generated( value = "org.jomc.tools.SourceFileProcessor 1.9", comments = "See http://www.jomc.org/jomc/1.9/jomc-tools-1.9" ) 082// </editor-fold> 083// SECTION-END 084public final class IBAN implements CharSequence, Comparable<IBAN>, Formattable, Serializable 085{ 086 // SECTION-START[IBAN] 087 088 /** 089 * International bank account number structure. 090 * <p>The following character representations are used: 091 * <table border="0"> 092 * <tr><td>a</td><td>Upper case letters (alphabetic characters A-Z only)</td></tr> 093 * <tr><td>c</td><td>Upper and lower case alphanumeric characters (A-Z, a-z and 0-9)</td></tr> 094 * <tr><td>e</td><td>Blank space</td></tr> 095 * <tr><td>n</td><td>Digits (numeric characters 0 to 9 only)</td></tr> 096 * </table></p> 097 * <p>The following length indications are used: 098 * <table border="0"> 099 * <tr><td>nn!</td><td>fixed length</td></tr> 100 * <tr><td>nn</td><td>maximum length</td></tr> 101 * </table></p> 102 * 103 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 104 * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ 105 */ 106 private static final class Structure 107 { 108 109 /** 110 * Part of a structure. 111 * 112 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 113 * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ 114 */ 115 private static final class Part 116 { 117 118 /** Type of the part. */ 119 private final char type; 120 121 /** The literal of the part or {@code null}. */ 122 private final String literal; 123 124 /** The fixed length of the part or {@code null}. */ 125 private final Integer fixedLength; 126 127 /** The maximum length of the part or {@code null}. */ 128 private final Integer maximumLength; 129 130 /** 131 * Creates a new {@code Part} instance. 132 * 133 * @param type The type of the part. 134 * @param literal The literal of the part or {@code null}. 135 * @param fixedLength The fixed length of the part or {@code null}. 136 * @param maximumLength The maximum length of the part or {@code null}. 137 */ 138 private Part( final char type, final String literal, final Integer fixedLength, 139 final Integer maximumLength ) 140 { 141 super(); 142 this.type = type; 143 this.literal = literal; 144 this.fixedLength = fixedLength; 145 this.maximumLength = maximumLength; 146 } 147 148 /** 149 * Gets the type of the part. 150 * <p>The following types are used: 151 * <table border="0"> 152 * <tr><td>a</td><td>Upper case letters (alphabetic characters A-Z only)</td></tr> 153 * <tr><td>c</td><td>Upper and lower case alphanumeric characters (A-Z, a-z and 0-9)</td></tr> 154 * <tr><td>e</td><td>Blank space</td></tr> 155 * <tr><td>n</td><td>Digits (numeric characters 0 to 9 only)</td></tr> 156 * <tr><td>l</td><td>Literal</td></tr> 157 * </table></p> 158 * 159 * @return The type of the part. 160 */ 161 private char getType() 162 { 163 return this.type; 164 } 165 166 /** 167 * Gets the literal of the part. 168 * 169 * @return The literal of the part or {@code null}, if the type of the part is not equal to {@code l}. 170 */ 171 private String getLiteral() 172 { 173 return this.literal; 174 } 175 176 /** 177 * Gets the fixed length of the part. 178 * 179 * @return The fixed length of the part or {@code null}, if the part has no fixed length but a maximum 180 * length. 181 */ 182 private Integer getFixedLength() 183 { 184 return this.fixedLength; 185 } 186 187 /** 188 * Gets the maximum length of the part. 189 * 190 * @return The maximum length of the part or {@code null}, if the part has no maximum length but a fixed 191 * length. 192 */ 193 private Integer getMaximumLength() 194 { 195 return this.maximumLength; 196 } 197 198 } 199 200 /** The parts of the structure. */ 201 private final List<Part> parts; 202 203 /** 204 * Creates a new {@code Structure} instance. 205 * 206 * @param parts The parts of the structure. 207 */ 208 private Structure( final List<Part> parts ) 209 { 210 super(); 211 this.parts = parts; 212 } 213 214 /** 215 * Gets the parts of the structure. 216 * 217 * @return The parts of the structure. 218 */ 219 private List<Part> getParts() 220 { 221 return this.parts; 222 } 223 224 } 225 226 /** 227 * Context of a parse operation. 228 * 229 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 230 * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ 231 */ 232 private static final class ParseContext 233 { 234 235 /** The text to parse. */ 236 private final String text; 237 238 /** The {@code ParsePosition} of the context. */ 239 private final ParsePosition parsePosition; 240 241 /** The format to parse. */ 242 private final IbanFormat format; 243 244 /** The number of electronic format characters parsed. */ 245 private int length; 246 247 /** The previous character parsed. */ 248 private Character previous; 249 250 /** 251 * Creates a new {@code ParseContext} instance. 252 * 253 * @param text The text to parse. 254 * @param pos The {@code ParsePosition} backing the parse. 255 * @param format The format to parse. 256 */ 257 private ParseContext( final String text, final ParsePosition pos, final IbanFormat format ) 258 { 259 super(); 260 this.text = text; 261 this.parsePosition = pos; 262 this.format = format; 263 this.length = 0; 264 } 265 266 } 267 268 /** Serial version UID for backwards compatibility with 2007.45.x classes. */ 269 private static final long serialVersionUID = -8123668345632147105L; 270 271 /** Used to cache instances. */ 272 private static volatile Reference<Map<String, IBAN>> cacheReference = new SoftReference<Map<String, IBAN>>( null ); 273 274 /** Mappings of two-letter ISO 3166-1 country codes to SEPA country flags. */ 275 private static final Map<String, Boolean> SEPA_COUNTRY_FLAGS = new HashMap<String, Boolean>( 128 ); 276 277 /** Mappings of two-letter ISO 3166-1 country codes to BBAN structures. */ 278 private static final Map<String, Structure> BBAN_STRUCTURES = new HashMap<String, Structure>( 128 ); 279 280 /** Mappings of two-letter ISO 3166-1 country codes to BBAN bank identifier indices. */ 281 private static final Map<String, List<Integer>> BBAN_BANK_ID_INDICES = new HashMap<String, List<Integer>>( 128 ); 282 283 /** Mappings of two-letter ISO 3166-1 country codes to BBAN branch identifier indices. */ 284 private static final Map<String, List<Integer>> BBAN_BRANCH_ID_INDICES = new HashMap<String, List<Integer>>( 128 ); 285 286 /** Mappings of two-letter ISO 3166-1 country codes to IBAN structures. */ 287 private static final Map<String, Structure> IBAN_STRUCTURES = new HashMap<String, Structure>( 128 ); 288 289 /** Mappings of two-letter ISO 3166-1 country codes to IBAN country codes. */ 290 private static final Map<String, String> IBAN_COUNTRY_CODES = new HashMap<String, String>( 128 ); 291 292 /** List of supported two-letter ISO 3166-1 country codes. */ 293 private static final List<String> COUNTRIES = new ArrayList<String>( 128 ); 294 295 /** {@code BigInteger} constant {@code 97}. */ 296 private static final BigInteger INTEGER_97 = BigInteger.valueOf( 97L ); 297 298 /** {@code BigInteger} constant {@code 98}. */ 299 private static final BigInteger INTEGER_98 = BigInteger.valueOf( 98L ); 300 301 static 302 { 303 InputStream in = null; 304 305 try 306 { 307 final Properties properties = new Properties(); 308 final URL resource = IBAN.class.getResource( "IBAN.properties" ); 309 assert resource != null : "Expected resource 'IBAN.properties' to exist."; 310 311 if ( resource != null ) 312 { 313 in = resource.openStream(); 314 properties.load( in ); 315 in.close(); 316 in = null; 317 318 if ( properties.containsKey( "countries" ) ) 319 { 320 final String[] countries = properties.getProperty( "countries" ).split( "\\|" ); 321 COUNTRIES.addAll( Arrays.asList( countries ) ); 322 323 for ( int i = 0, s0 = countries.length; i < s0; i++ ) 324 { 325 if ( countries[i].length() != 2 ) 326 { 327 throw new AssertionError( countries[i] ); 328 } 329 330 final String bbanStructure = properties.getProperty( countries[i] + ".bban.structure" ); 331 final String bbanBankIds = properties.getProperty( countries[i] + ".bban.bankid.indices" ); 332 final String bbanBranchIds = properties.getProperty( countries[i] + ".bban.branchid.indices" ); 333 final String ibanCountryCode = properties.getProperty( countries[i] + ".iban.countrycode" ); 334 final String ibanStructure = properties.getProperty( countries[i] + ".iban.structure" ); 335 final String sepa = properties.getProperty( countries[i] + ".sepa" ); 336 337 SEPA_COUNTRY_FLAGS.put( countries[i], Boolean.valueOf( sepa ) ); 338 339 if ( ibanCountryCode != null ) 340 { 341 IBAN_COUNTRY_CODES.put( countries[i], ibanCountryCode ); 342 } 343 344 if ( bbanStructure != null ) 345 { 346 BBAN_STRUCTURES.put( countries[i], parseStructure( bbanStructure ) ); 347 348 if ( bbanBankIds != null ) 349 { 350 BBAN_BANK_ID_INDICES.put( countries[i], parseIndices( bbanBankIds ) ); 351 } 352 353 if ( bbanBranchIds != null ) 354 { 355 BBAN_BRANCH_ID_INDICES.put( countries[i], parseIndices( bbanBranchIds ) ); 356 } 357 } 358 359 if ( ibanStructure != null ) 360 { 361 IBAN_STRUCTURES.put( countries[i], parseStructure( ibanStructure ) ); 362 } 363 } 364 } 365 } 366 } 367 catch ( final ParseException e ) 368 { 369 throw new AssertionError( e ); 370 } 371 catch ( final IOException e ) 372 { 373 throw new AssertionError( e ); 374 } 375 finally 376 { 377 try 378 { 379 if ( in != null ) 380 { 381 in.close(); 382 } 383 } 384 catch ( final IOException e ) 385 { 386 throw new AssertionError( e ); 387 } 388 } 389 } 390 391 /** Maximum number of characters of an {@code IBAN}. */ 392 public static final int MAX_CHARACTERS = 42; 393 394 /** 395 * The two-letter ISO 3166-1 country code of the IBAN. 396 * @serial 397 */ 398 private String countryCode; 399 400 /** 401 * Flag indicating the country of the IBAN is part of the jurisdictional scope of the SEPA Schemes. 402 * @serial 403 */ 404 private boolean sepaCountry; 405 406 /** 407 * The bank identifier part of the BBAN of the IBAN. 408 * @serial 409 */ 410 private String bankIdentifier; 411 412 /** 413 * The branch identifier part of the BBAN of the IBAN. 414 * @serial 415 */ 416 private String branchIdentifier; 417 418 /** 419 * The electronic format of the IBAN. 420 * @serial 421 */ 422 private String electronicFormat; 423 424 /** 425 * The letter format of the IBAN. 426 * @serial 427 */ 428 private String letterFormat; 429 430 /** 431 * The parts of the IBAN. 432 * @serial 433 */ 434 private Comparable[] parts; 435 436 /** Cached string representation of the instance. */ 437 private transient String string; 438 439 /** 440 * Creates a new {@code IBAN} instance. 441 * 442 * @param countryCode The two-letter ISO 3166-1 country code of the IBAN. 443 * @param sepaCountry Flag indicating the country is part of the jurisdictional scope of the SEPA Schemes. 444 * @param bankIdentifier The bank identifier part of the BBAN of the IBAN. 445 * @param branchIdentifier The branch identifier part of the BBAN of the IBAN or {@code null}. 446 * @param electronicFormat The electronic format representation of the IBAN. 447 * @param letterFormat The letter format representation of the IBAN. 448 * @param parts The parts of the IBAN. 449 * 450 * @see #parse(String, ParsePosition) 451 */ 452 private IBAN( final String countryCode, final boolean sepaCountry, final String bankIdentifier, 453 final String branchIdentifier, final String electronicFormat, final String letterFormat, 454 final Comparable[] parts ) 455 { 456 super(); 457 this.countryCode = countryCode; 458 this.sepaCountry = sepaCountry; 459 this.bankIdentifier = bankIdentifier; 460 this.branchIdentifier = branchIdentifier; 461 this.electronicFormat = electronicFormat; 462 this.letterFormat = letterFormat; 463 this.parts = parts; 464 getCache().put( electronicFormat, this ); 465 getCache().put( letterFormat, this ); 466 } 467 468 /** 469 * Gets an array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN 470 * standard. 471 * 472 * @return An array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN 473 * standard. 474 */ 475 public static String[] getCountryCodes() 476 { 477 return COUNTRIES.toArray( new String[ COUNTRIES.size() ] ); 478 } 479 480 /** 481 * Tests a given two-letter ISO 3166-1 country code to identify a country that is part of the jurisdictional scope 482 * of the Single Euro Payment Area (SEPA) Schemes. 483 * The <a href="../../../doc-files/EPC409-09_EPC_List_of_SEPA_Scheme_Countries_v2_4_-_April_2016.pdf"> 484 * EPC List of SEPA Scheme Countries</a> document lists the countries and territories which are part of the 485 * jurisdictional scope of the Single Euro Payment Area (SEPA) Schemes. An updated version of the document may be 486 * found at <a href="http://www.europeanpaymentscouncil.eu">The European Payments Council (EPC)</a>. 487 * 488 * @param countryCode The two-letter ISO 3166-1 country code to test. 489 * 490 * @return {@code true}, if the country identified by {@code countryCode} is part of the jurisdictional scope of the 491 * Single Euro Payment Area (SEPA) Schemes; {@code false}, if not. 492 * 493 * @throws NullPointerException if {@code countryCode} is {@code null}. 494 * 495 * @see #getCountryCodes() 496 * @since 2007.46 497 */ 498 public static boolean isSepaCountry( final String countryCode ) 499 { 500 if ( countryCode == null ) 501 { 502 throw new NullPointerException( "countryCode" ); 503 } 504 505 final Boolean sepaCountry = SEPA_COUNTRY_FLAGS.get( countryCode ); 506 return sepaCountry != null ? sepaCountry : false; 507 } 508 509 /** 510 * Gets the two-letter ISO 3166-1 country code identifying the country the IBAN belongs to. 511 * 512 * @return The two-letter ISO 3166-1 country code identifying the country the IBAN belongs to. 513 * 514 * @see #getCountryCodes() 515 */ 516 public String getCountryCode() 517 { 518 return this.countryCode; 519 } 520 521 /** 522 * Gets a flag indicating the country of the IBAN to be part of the jurisdictional scope of the Single Euro Payment 523 * Area (SEPA) Schemes. 524 * 525 * @return {@code true}, if the country of the IBAN is part of the jurisdictional scope of the Single Euro Payment 526 * Area (SEPA) Schemes; {@code false}, else. 527 * 528 * @see #getCountryCode() 529 * @see #isSepaCountry(java.lang.String) 530 */ 531 public boolean isSepaCountry() 532 { 533 return this.sepaCountry; 534 } 535 536 /** 537 * Gets the bank identifier part of the BBAN of the IBAN. 538 * 539 * @return The bank identifier part of the BBAN of the IBAN or {@code null}. 540 */ 541 public String getBankIdentifier() 542 { 543 return this.bankIdentifier; 544 } 545 546 /** 547 * Gets the branch identifier part of the BBAN of the IBAN. 548 * 549 * @return The branch identifier part of the BBAN of the IBAN or {@code null}. 550 */ 551 public String getBranchIdentifier() 552 { 553 return this.branchIdentifier; 554 } 555 556 /** 557 * Parses text from a BBAN string to produce an {@code IBAN} instance. 558 * 559 * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create. 560 * @param bban A string to parse BBAN characters from. 561 * 562 * @return The parsed value. 563 * 564 * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}. 565 * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN 566 * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes}. 567 * @throws IbanSyntaxException if the parse fails or the length of {@code bban} is invalid. 568 * 569 * @see #getCountryCodes() 570 * @see #valueOf(java.lang.String, java.lang.String) 571 */ 572 public static IBAN parse( final String countryCode, final String bban ) throws IbanSyntaxException 573 { 574 if ( countryCode == null ) 575 { 576 throw new NullPointerException( "countryCode" ); 577 } 578 if ( bban == null ) 579 { 580 throw new NullPointerException( "bban" ); 581 } 582 583 final String ibanCountryCode = toIbanCountryCode( countryCode ); 584 final Structure structure = BBAN_STRUCTURES.get( ibanCountryCode ); 585 586 if ( structure == null ) 587 { 588 throw new IllegalArgumentException( countryCode ); 589 } 590 591 final List<Integer> bankIdIndices = BBAN_BANK_ID_INDICES.get( ibanCountryCode ); 592 final List<Integer> branchIdIndices = BBAN_BRANCH_ID_INDICES.get( ibanCountryCode ); 593 final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( countryCode ); 594 595 // Parse the parts. 596 final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); 597 final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS ); 598 final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS ); 599 final List<Comparable<?>> comparables = new ArrayList<Comparable<?>>( structure.getParts().size() + 2 ); 600 final ParseContext context = 601 new ParseContext( bban, new ParsePosition( 0 ), bban.length() > 4 && bban.charAt( 4 ) == ' ' 602 ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); 603 604 for ( int p = 0, s0 = structure.getParts().size(); p < s0 && context.parsePosition.getErrorIndex() < 0; p++ ) 605 { 606 final Structure.Part part = structure.getParts().get( p ); 607 final String chars = parsePart( context, part ); 608 609 if ( context.parsePosition.getErrorIndex() < 0 ) 610 { 611 if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) 612 { // Fixed length mismatch. 613 throw new IbanSyntaxException( bban, context.parsePosition.getIndex() ); 614 } 615 616 electronic_format_builder.append( chars ); 617 618 switch ( part.getType() ) 619 { 620 case 'n': 621 comparables.add( new BigInteger( chars ) ); 622 break; 623 default: 624 comparables.add( chars ); 625 break; 626 } 627 } 628 else 629 { // Invalid part. 630 throw new IbanSyntaxException( bban, context.parsePosition.getErrorIndex() ); 631 } 632 } 633 634 // Extract bank id and branch id. 635 if ( bankIdIndices != null ) 636 { 637 bank_id_builder.append( electronic_format_builder.subSequence( bankIdIndices.get( 0 ), 638 bankIdIndices.get( 1 ) ) ); 639 640 } 641 642 if ( branchIdIndices != null ) 643 { 644 branch_id_builder.append( electronic_format_builder.subSequence( branchIdIndices.get( 0 ), 645 branchIdIndices.get( 1 ) ) ); 646 647 } 648 649 // Calculate checksum. 650 final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); 651 appendIso7064Digits( integer_builder, electronic_format_builder.toString() ); 652 appendIso7064Digits( integer_builder, ibanCountryCode ); 653 appendIso7064Digits( integer_builder, "00" ); 654 655 final BigInteger integer = new BigInteger( integer_builder.toString() ); 656 final BigInteger checksum = INTEGER_98.subtract( integer.remainder( INTEGER_97 ) ); 657 final String checksumPart = checksum.compareTo( BigInteger.TEN ) < 0 ? "0" + checksum : checksum.toString(); 658 659 comparables.add( 0, checksumPart ); 660 comparables.add( 0, ibanCountryCode ); 661 662 electronic_format_builder.insert( 0, checksumPart ); 663 electronic_format_builder.insert( 0, ibanCountryCode ); 664 665 return new IBAN( countryCode, sepa_country, bank_id_builder.length() > 0 ? bank_id_builder.toString() : null, 666 branch_id_builder.length() > 0 ? branch_id_builder.toString() : null, 667 electronic_format_builder.toString(), toLetterFormat( electronic_format_builder.toString() ), 668 comparables.toArray( new Comparable<?>[ comparables.size() ] ) ); 669 670 } 671 672 /** 673 * Parses text from a string to produce an {@code IBAN} instance. 674 * <p>The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the 675 * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all 676 * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to 677 * indicate the starting point for the next call to this method.</p> 678 * 679 * @param text A string to parse IBAN characters from. 680 * @param pos A {@code ParsePosition} object with index and error index information as described above. 681 * 682 * @return The parsed value or {@code null}, if the parse fails. 683 * 684 * @throws NullPointerException if {@code text} or {@code pos} is {@code null}. 685 * @throws IbanCheckDigitsException if check digits validation of the parsed value fails. 686 */ 687 public static IBAN parse( final String text, final ParsePosition pos ) throws IbanCheckDigitsException 688 { 689 if ( text == null ) 690 { 691 throw new NullPointerException( "text" ); 692 } 693 if ( pos == null ) 694 { 695 throw new NullPointerException( "pos" ); 696 } 697 698 IBAN ret = null; 699 final int begin_index = pos.getIndex(); 700 701 // Extract the country code to query properties. 702 if ( text.length() > begin_index + 1 ) 703 { 704 final String country_code = text.substring( begin_index, begin_index + 2 ); 705 706 if ( !isLiteral( country_code.charAt( 0 ) ) ) 707 { 708 pos.setIndex( begin_index ); 709 pos.setErrorIndex( begin_index ); 710 } 711 else if ( !isLiteral( country_code.charAt( 1 ) ) ) 712 { 713 pos.setIndex( begin_index ); 714 pos.setErrorIndex( begin_index + 1 ); 715 } 716 else 717 { 718 final Structure structure = IBAN_STRUCTURES.get( country_code ); 719 720 if ( structure != null ) 721 { // Parse the parts. 722 final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( country_code ); 723 final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); 724 final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS ); 725 final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS ); 726 final List<Comparable<?>> comparables = new ArrayList<Comparable<?>>( structure.getParts().size() ); 727 final ParseContext context = 728 new ParseContext( text, pos, 729 text.length() > begin_index + 4 && text.charAt( begin_index + 4 ) == ' ' 730 ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); 731 732 for ( int p = 0, s0 = structure.getParts().size(); 733 p < s0 && context.parsePosition.getErrorIndex() < 0; 734 p++ ) 735 { 736 final Structure.Part part = structure.getParts().get( p ); 737 final String chars = parsePart( context, part ); 738 739 if ( context.parsePosition.getErrorIndex() < 0 ) 740 { 741 if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) 742 { // Fixed length mismatch. 743 context.parsePosition.setErrorIndex( context.parsePosition.getIndex() ); 744 context.parsePosition.setIndex( begin_index ); 745 break; 746 } 747 748 electronic_format_builder.append( chars ); 749 750 switch ( part.getType() ) 751 { 752 case 'n': 753 comparables.add( new BigInteger( chars ) ); 754 break; 755 default: 756 comparables.add( chars ); 757 break; 758 } 759 } 760 } 761 762 if ( context.parsePosition.getErrorIndex() < 0 ) 763 { // Validate checksum. 764 final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); 765 appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) ); 766 appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) ); 767 768 final BigInteger integer = new BigInteger( integer_builder.toString() ); 769 770 if ( integer.remainder( INTEGER_97 ).equals( BigInteger.ONE ) ) 771 { 772 final List<Integer> bankIdIndices = BBAN_BANK_ID_INDICES.get( country_code ); 773 final List<Integer> branchIdIndices = BBAN_BRANCH_ID_INDICES.get( country_code ); 774 775 if ( bankIdIndices != null ) 776 { 777 bank_id_builder.append( electronic_format_builder.subSequence( 778 bankIdIndices.get( 0 ) + 4, bankIdIndices.get( 1 ) + 4 ) ); 779 780 } 781 782 if ( branchIdIndices != null ) 783 { 784 branch_id_builder.append( electronic_format_builder.subSequence( 785 branchIdIndices.get( 0 ) + 4, branchIdIndices.get( 1 ) + 4 ) ); 786 787 } 788 789 ret = new IBAN( country_code, sepa_country, 790 bank_id_builder.length() > 0 ? bank_id_builder.toString() : null, 791 branch_id_builder.length() > 0 ? branch_id_builder.toString() : null, 792 electronic_format_builder.toString(), 793 toLetterFormat( electronic_format_builder.toString() ), 794 comparables.toArray( new Comparable<?>[ comparables.size() ] ) ); 795 796 } 797 else 798 { // Invalid checksum. 799 context.parsePosition.setIndex( begin_index ); 800 context.parsePosition.setErrorIndex( begin_index + 2 ); 801 802 throw new IbanCheckDigitsException( electronic_format_builder.toString(), 803 (Number) comparables.get( 1 ) ); 804 805 } 806 } 807 } 808 else 809 { // Unsupported country code. 810 pos.setIndex( begin_index ); 811 pos.setErrorIndex( begin_index + 1 ); 812 } 813 } 814 } 815 else 816 { // No country code provided. 817 pos.setIndex( begin_index ); 818 pos.setErrorIndex( begin_index ); 819 820 if ( begin_index < text.length() && isLiteral( text.charAt( begin_index ) ) ) 821 { 822 pos.setErrorIndex( begin_index + 1 ); 823 } 824 } 825 826 return ret; 827 } 828 829 /** 830 * Parses text from the beginning of the given string to produce an {@code IBAN} instance. 831 * <p>Unlike the {@link #parse(String, ParsePosition)} method this method throws an {@code IbanSyntaxException} if 832 * {@code text} cannot be parsed or is of invalid length.</p> 833 * 834 * @param text A string to parse IBAN characters from. 835 * 836 * @return The parsed value. 837 * 838 * @throws NullPointerException if {@code text} is {@code null}. 839 * @throws IbanSyntaxException if the parse fails or the length of {@code text} is invalid. 840 * @throws IbanCheckDigitsException if check digits validation of the parsed value fails. 841 * 842 * @see #valueOf(java.lang.String) 843 */ 844 public static IBAN parse( final String text ) throws IbanSyntaxException, IbanCheckDigitsException 845 { 846 if ( text == null ) 847 { 848 throw new NullPointerException( "text" ); 849 } 850 851 IBAN iban = getCache().get( text ); 852 853 if ( iban == null ) 854 { 855 final ParsePosition pos = new ParsePosition( 0 ); 856 iban = IBAN.parse( text, pos ); 857 858 if ( iban == null || pos.getErrorIndex() != -1 || pos.getIndex() < text.length() ) 859 { 860 throw new IbanSyntaxException( text, pos.getErrorIndex() != -1 861 ? pos.getErrorIndex() 862 : pos.getIndex() ); 863 864 } 865 } 866 867 return iban; 868 } 869 870 /** 871 * Parses text from the beginning of the given string to produce an {@code IBAN} instance. 872 * <p>Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if 873 * {@code text} cannot be parsed, is of invalid length or check digits validation fails.</p> 874 * 875 * @param text A string to parse IBAN characters from. 876 * 877 * @return The parsed value. 878 * 879 * @throws NullPointerException if {@code text} is {@code null}. 880 * @throws IllegalArgumentException if the parse fails, the length of {@code text} is invalid or if check digits 881 * validation of the parsed value fails. 882 * 883 * @see #parse(java.lang.String) 884 */ 885 public static IBAN valueOf( final String text ) 886 { 887 if ( text == null ) 888 { 889 throw new NullPointerException( "text" ); 890 } 891 892 try 893 { 894 return IBAN.parse( text ); 895 } 896 catch ( final IbanSyntaxException e ) 897 { 898 throw new IllegalArgumentException( text, e ); 899 } 900 catch ( final IbanCheckDigitsException e ) 901 { 902 throw new IllegalArgumentException( text, e ); 903 } 904 } 905 906 /** 907 * Parses text from a BBAN string to produce an {@code IBAN} instance. 908 * <p>Unlike the {@link #parse(String, String)} method this method throws an {@code IllegalArgumentException} if the 909 * parse fails or the length of {@code bban} is invalid.</p> 910 * 911 * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create. 912 * @param bban A string to parse BBAN characters from. 913 * 914 * @return The parsed value. 915 * 916 * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}. 917 * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN 918 * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes}, 919 * if the parse fails, or if the length of {@code bban} is invalid. 920 * 921 * @see #parse(java.lang.String, java.lang.String) 922 * @since 2007.46 923 */ 924 public static IBAN valueOf( final String countryCode, final String bban ) 925 { 926 if ( countryCode == null ) 927 { 928 throw new NullPointerException( "countryCode" ); 929 } 930 if ( bban == null ) 931 { 932 throw new NullPointerException( "bban" ); 933 } 934 935 try 936 { 937 return IBAN.parse( countryCode, bban ); 938 } 939 catch ( final IbanSyntaxException e ) 940 { 941 throw new IllegalArgumentException( bban, e ); 942 } 943 } 944 945 /** 946 * Checks a given character to belong to the IBAN alphabet. 947 * 948 * @param c The character to check. 949 * 950 * @return {@code true} if {@code c} is a character of the IBAN alphabet; {@code false}, else. 951 */ 952 public static boolean isIbanAlphabet( final char c ) 953 { 954 return ( c >= 'A' && c <= 'Z' ) || ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ); 955 } 956 957 /** 958 * Parses text from a string to peek at a national {@code IBAN} format. 959 * <p>This method peeks at the given string to test whether it denotes a possibly valid national IBAN format.</p> 960 * 961 * @param text A string to peek at. 962 * 963 * @return {@code true}, if {@code text} denotes a (possibly partially) valid national IBAN format; {@code false}, 964 * if {@code text} does not denote a valid national IBAN format. 965 * 966 * @throws NullPointerException if {@code text} is {@code null}. 967 * @throws IbanCheckDigitsException if check digits validation fails. 968 */ 969 public static boolean peekIban( final String text ) throws IbanCheckDigitsException 970 { 971 if ( text == null ) 972 { 973 throw new NullPointerException( "text" ); 974 } 975 976 boolean valid = true; 977 boolean complete = true; 978 979 // Extract the country code to query properties. 980 if ( text.length() > 1 ) 981 { 982 final String country_code = text.substring( 0, 2 ); 983 final Structure structure = IBAN_STRUCTURES.get( country_code ); 984 985 if ( structure != null ) 986 { // Parse the parts. 987 final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); 988 final ParseContext context = 989 new ParseContext( text, new ParsePosition( 0 ), text.length() > 4 && text.charAt( 4 ) == ' ' 990 ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); 991 992 for ( int p = 0, s0 = structure.getParts().size(); 993 p < s0 && context.parsePosition.getErrorIndex() < 0 && complete; 994 p++ ) 995 { 996 final Structure.Part part = structure.getParts().get( p ); 997 final String chars = parsePart( context, part ); 998 999 if ( context.parsePosition.getErrorIndex() < 0 ) 1000 { 1001 if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) 1002 { // Fixed length mismatch. 1003 complete = false; 1004 break; 1005 } 1006 1007 electronic_format_builder.append( chars ); 1008 } 1009 } 1010 1011 if ( context.parsePosition.getErrorIndex() < 0 ) 1012 { 1013 if ( complete ) 1014 { // Validate checksum. 1015 final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); 1016 appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) ); 1017 appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) ); 1018 1019 final BigInteger integer = new BigInteger( integer_builder.toString() ); 1020 1021 valid = integer.remainder( INTEGER_97 ).equals( BigInteger.ONE ); 1022 1023 if ( !valid ) 1024 { 1025 throw new IbanCheckDigitsException( 1026 electronic_format_builder.toString(), 1027 new BigInteger( electronic_format_builder.substring( 2, 4 ) ) ); 1028 1029 } 1030 1031 if ( context.parsePosition.getIndex() != text.length() ) 1032 { // Unexpected length. 1033 valid = false; 1034 } 1035 } 1036 } 1037 else 1038 { // Invalid part. 1039 valid = false; 1040 } 1041 } 1042 else 1043 { // Unsupported country code. 1044 valid = false; 1045 } 1046 } 1047 else if ( text.length() > 0 && !( text.charAt( 0 ) >= 'A' && text.charAt( 0 ) <= 'Z' ) ) 1048 { // Illegal first character. 1049 valid = false; 1050 } 1051 1052 return valid; 1053 } 1054 1055 /** 1056 * Formats an IBAN and appends the resulting text to a given {@code Appendable}. 1057 * 1058 * @param format The format to use. 1059 * @param appendable The {@code Appendable} to append the formatted IBAN to. 1060 * 1061 * @return The value passed in as {@code appendable}. 1062 * 1063 * @throws NullPointerException if {@code format} or {@code appendable} is {@code null}. 1064 * @throws IOException if appending fails. 1065 */ 1066 public Appendable append( final IbanFormat format, final Appendable appendable ) throws IOException 1067 { 1068 if ( format == null ) 1069 { 1070 throw new NullPointerException( "format" ); 1071 } 1072 if ( appendable == null ) 1073 { 1074 throw new NullPointerException( "appendable" ); 1075 } 1076 1077 switch ( format ) 1078 { 1079 case ELECTRONIC: 1080 return appendable.append( this.electronicFormat ); 1081 case PRINT: 1082 return appendable.append( this.letterFormat ); 1083 default: 1084 throw new AssertionError( format ); 1085 } 1086 } 1087 1088 /** 1089 * Formats an IBAN to produce a string. 1090 * 1091 * @param format The format to use. 1092 * 1093 * @return The formatted string. 1094 * 1095 * @throws NullPointerException if {@code format} is {@code null}. 1096 */ 1097 public String toString( final IbanFormat format ) 1098 { 1099 if ( format == null ) 1100 { 1101 throw new NullPointerException( "format" ); 1102 } 1103 1104 switch ( format ) 1105 { 1106 case ELECTRONIC: 1107 return this.electronicFormat; 1108 case PRINT: 1109 return this.letterFormat; 1110 default: 1111 throw new AssertionError( format ); 1112 } 1113 } 1114 1115 /** 1116 * Formats the object using the provided {@code Formatter}. 1117 * <p>This method uses the {@code ELECTRONIC} format by default. The {@code PRINT} format can be used by specifying 1118 * the alternate form flag ({@code #}) in a format specifier.</p> 1119 * 1120 * @param formatter The {@code Formatter}. 1121 * @param flags The flags to modify the output format. 1122 * @param width The minimum number of characters to be written to the output. 1123 * @param precision The maximum number of characters to be written to the output. 1124 * 1125 * @throws IllegalFormatFlagsException if the {@code UPPERCASE} flag is set. 1126 */ 1127 public void formatTo( final Formatter formatter, final int flags, final int width, final int precision ) 1128 { 1129 if ( formatter == null ) 1130 { 1131 throw new NullPointerException( "formatter" ); 1132 } 1133 if ( ( flags & UPPERCASE ) == UPPERCASE ) 1134 { 1135 final StringBuilder flagsBuilder = new StringBuilder( 3 ); 1136 1137 if ( ( flags & ALTERNATE ) == ALTERNATE ) 1138 { 1139 flagsBuilder.append( "#" ); 1140 } 1141 if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY ) 1142 { 1143 flagsBuilder.append( "-" ); 1144 } 1145 1146 flagsBuilder.append( "^" ); 1147 1148 throw new IllegalFormatFlagsException( flagsBuilder.toString() ); 1149 } 1150 1151 final IbanFormat format = ( flags & ALTERNATE ) == ALTERNATE ? IbanFormat.PRINT : IbanFormat.ELECTRONIC; 1152 1153 String str = this.toString( format ); 1154 if ( precision != -1 && precision < str.length() ) 1155 { 1156 str = str.substring( 0, precision ); 1157 } 1158 1159 final StringBuilder stringBuilder = new StringBuilder( str ); 1160 1161 if ( width != -1 ) 1162 { 1163 final int len = width - stringBuilder.length(); 1164 1165 if ( len > 0 ) 1166 { 1167 final char[] pad = new char[ len ]; 1168 Arrays.fill( pad, ' ' ); 1169 1170 if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY ) 1171 { 1172 stringBuilder.append( pad ); 1173 } 1174 else 1175 { 1176 stringBuilder.insert( 0, pad ); 1177 } 1178 } 1179 } 1180 1181 formatter.format( stringBuilder.toString() ); 1182 } 1183 1184 /** 1185 * Returns the length of this character sequence. 1186 * <p>The length is the number of 16-bit {@code char}s in the sequence.</p> 1187 * 1188 * @return The number of {@code char}s in this sequence. 1189 */ 1190 public int length() 1191 { 1192 return this.electronicFormat.length(); 1193 } 1194 1195 /** 1196 * Returns the {@code char} value at the specified index. 1197 * <p>An index ranges from zero to {@code length() - 1}. The first {@code char} value of the sequence is at index 1198 * zero, the next at index one, and so on, as for array indexing.</p> 1199 * <p>If the {@code char} value specified by the index is a surrogate, the surrogate value is returned.</p> 1200 * 1201 * @param index The index of the {@code char} value to return. 1202 * 1203 * @return The {@code char} value at {@code index}. 1204 * 1205 * @throws IndexOutOfBoundsException if {@code index} is negative or not less than the length of the character 1206 * sequence. 1207 * 1208 * @see #length() 1209 */ 1210 public char charAt( final int index ) 1211 { 1212 return this.electronicFormat.charAt( index ); 1213 } 1214 1215 /** 1216 * Returns a new {@code CharSequence} that is a subsequence of this sequence. 1217 * <p>The subsequence starts with the {@code char} value at the specified index and ends with the {@code char} value 1218 * at index {@code end - 1}. The length (in {@code char}s) of the returned sequence is {@code end - start}, so if 1219 * {@code start == end} then an empty sequence is returned.</p> 1220 * 1221 * @param start The start index, inclusive. 1222 * @param end The end index, exclusive. 1223 * 1224 * @return The subsequence starting at {@code start} up to {@code end -1}. 1225 * 1226 * @throws IndexOutOfBoundsException if {@code start} or {@code end} are negative, if {@code end} is greater than 1227 * the length of the character sequence, or if {@code start} is greater than {@code end}. 1228 */ 1229 public CharSequence subSequence( final int start, final int end ) 1230 { 1231 return this.electronicFormat.subSequence( start, end ); 1232 } 1233 1234 /** 1235 * Gets the hash code value of the object. 1236 * 1237 * @return The hash code value of the object. 1238 */ 1239 @Override 1240 public int hashCode() 1241 { 1242 return this.electronicFormat.hashCode(); 1243 } 1244 1245 /** 1246 * Indicates whether some other object is equal to this one. 1247 * 1248 * @param o The reference object with which to compare. 1249 * 1250 * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise. 1251 */ 1252 @Override 1253 public boolean equals( final Object o ) 1254 { 1255 boolean equal = this == o; 1256 1257 if ( !equal && ( o instanceof IBAN ) ) 1258 { 1259 equal = this.electronicFormat.equals( ( (IBAN) o ).electronicFormat ); 1260 } 1261 1262 return equal; 1263 } 1264 1265 /** 1266 * Gets a string representation of the object. 1267 * 1268 * @return A string representation of the object. 1269 */ 1270 @Override 1271 public String toString() 1272 { 1273 return super.toString() + this.internalString(); 1274 } 1275 1276 /** 1277 * Compares this object with the specified object for order. 1278 * <p>IBAN comparison respects the national IBAN formats when comparing IBANs of the same country. Any country codes 1279 * and check digits are ignored.</p> 1280 * 1281 * @param o The Object to be compared. 1282 * 1283 * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than 1284 * the specified object. 1285 * 1286 * @throws NullPointerException if {@code o} is {@code null}. 1287 */ 1288 public int compareTo( final IBAN o ) 1289 { 1290 if ( o == null ) 1291 { 1292 throw new NullPointerException( "o" ); 1293 } 1294 1295 int r = this.getCountryCode().compareTo( o.getCountryCode() ); 1296 for ( int i = 2, s0 = this.parts.length; r == 0 && i < s0; r = this.parts[i].compareTo( o.parts[i] ), i++ ); 1297 return r; 1298 } 1299 1300 /** 1301 * Gets the current cache instance. 1302 * 1303 * @return Current cache instance. 1304 */ 1305 private static Map<String, IBAN> getCache() 1306 { 1307 Map<String, IBAN> cache = cacheReference.get(); 1308 1309 if ( cache == null ) 1310 { 1311 cache = new ConcurrentHashMap<String, IBAN>( 1024 ); 1312 cacheReference = new SoftReference<Map<String, IBAN>>( cache ); 1313 } 1314 1315 return cache; 1316 } 1317 1318 /** 1319 * Creates a string representing the properties of the instance. 1320 * 1321 * @return A string representing the properties of the instance. 1322 */ 1323 private String internalString() 1324 { 1325 if ( this.string == null ) 1326 { 1327 final StringBuilder b = new StringBuilder( 500 ).append( "{" ); 1328 b.append( "countryCode=" ).append( this.countryCode ). 1329 append( ", sepaCountry=" ).append( this.sepaCountry ). 1330 append( ", bankIdentifier=" ).append( this.bankIdentifier ). 1331 append( ", branchIdentifier=" ).append( this.branchIdentifier ). 1332 append( ", electronicFormat=" ).append( this.electronicFormat ). 1333 append( ", letterFormat=" ).append( this.letterFormat ). 1334 append( ", parts={" ); 1335 1336 final StringBuilder partBuilder = new StringBuilder(); 1337 for ( int i = 0, s0 = this.parts.length; i < s0; i++ ) 1338 { 1339 partBuilder.append( ",[" ).append( i ).append( "]=" ).append( this.parts[i] ); 1340 } 1341 1342 this.string = b.append( partBuilder.substring( 1 ) ).append( "}}" ).toString(); 1343 } 1344 1345 return this.string; 1346 } 1347 1348 /** 1349 * Parses a given structure string to produce a list of {@code Part}s. 1350 * 1351 * @param structure The structure string to parse. 1352 * 1353 * @return The parsed {@code Structure}. 1354 * 1355 * @throws ParseException if parsing {@code structure} fails. 1356 */ 1357 private static Structure parseStructure( final String structure ) throws ParseException 1358 { 1359 boolean in_literal = false; 1360 boolean in_part = false; 1361 boolean fixed_length_part = false; 1362 final List<Structure.Part> parts = new ArrayList<Structure.Part>( IBAN.MAX_CHARACTERS ); 1363 final StringBuilder literalBuilder = new StringBuilder( IBAN.MAX_CHARACTERS ); 1364 final StringBuilder numberBuilder = new StringBuilder( IBAN.MAX_CHARACTERS ); 1365 1366 for ( int i = 0, s0 = structure.length(); i < s0; i++ ) 1367 { 1368 final char c = structure.charAt( i ); 1369 1370 if ( in_part ) 1371 { 1372 // Expect number or "(n|a|c|e)|!(n|a|c|e)" 1373 if ( isDigit( c ) ) 1374 { 1375 if ( fixed_length_part ) 1376 { // Expect type after length indicator. 1377 throw new ParseException( structure, i ); 1378 } 1379 numberBuilder.append( c ); 1380 } 1381 else if ( isTypeIdentifier( c ) ) 1382 { 1383 if ( fixed_length_part ) 1384 { 1385 parts.add( new Structure.Part( c, null, Integer.parseInt( numberBuilder.toString() ), null ) ); 1386 } 1387 else 1388 { 1389 parts.add( new Structure.Part( c, null, null, Integer.parseInt( numberBuilder.toString() ) ) ); 1390 } 1391 numberBuilder.setLength( 0 ); 1392 in_part = false; 1393 fixed_length_part = false; 1394 } 1395 else if ( isLengthIndicator( c ) ) 1396 { 1397 if ( fixed_length_part ) 1398 { // Expect type after length indicator. 1399 throw new ParseException( structure, i ); 1400 } 1401 fixed_length_part = true; 1402 } 1403 else 1404 { 1405 throw new ParseException( structure, i ); 1406 } 1407 } 1408 else if ( in_literal ) 1409 { 1410 // Expect literal or number starting a part. 1411 if ( isLiteral( c ) ) 1412 { 1413 literalBuilder.append( c ); 1414 } 1415 else if ( isDigit( c ) ) 1416 { 1417 parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) ); 1418 numberBuilder.append( c ); 1419 literalBuilder.setLength( 0 ); 1420 in_part = true; 1421 in_literal = false; 1422 } 1423 else 1424 { 1425 throw new ParseException( structure, i ); 1426 } 1427 } 1428 else 1429 { 1430 if ( fixed_length_part ) 1431 { // Expect type after length indicator. 1432 throw new ParseException( structure, i ); 1433 } 1434 1435 // Expect number starting a part or upper-case letter starting a literal. 1436 if ( isDigit( c ) ) 1437 { 1438 numberBuilder.append( c ); 1439 in_part = true; 1440 } 1441 else if ( isLiteral( c ) ) 1442 { 1443 literalBuilder.append( c ); 1444 in_literal = true; 1445 } 1446 else 1447 { 1448 throw new ParseException( structure, i ); 1449 } 1450 } 1451 } 1452 1453 if ( fixed_length_part ) 1454 { // Expect type after length indicator. 1455 throw new ParseException( structure, structure.length() ); 1456 } 1457 1458 if ( in_part ) 1459 { // Unclosed part. 1460 throw new ParseException( structure, structure.length() ); 1461 } 1462 1463 if ( in_literal ) 1464 { // Literal at end of structure. 1465 parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) ); 1466 } 1467 1468 return new Structure( parts ); 1469 } 1470 1471 /** 1472 * Parses text from a string according to a given {@code Part} to produce a {@code String} instance. 1473 * 1474 * @param context The context of the parse. 1475 * @param part The {@code Part} to parse. 1476 * 1477 * @return The parsed value, or {@code null} if the parse fails. 1478 */ 1479 private static String parsePart( final ParseContext context, final Structure.Part part ) 1480 { 1481 final StringBuilder partBuilder = new StringBuilder( MAX_CHARACTERS ); 1482 final int start_index = context.parsePosition.getIndex(); 1483 1484 next: 1485 for ( int index = context.parsePosition.getIndex(), text_len = context.text.length(), literal_index = 0, 1486 part_len = part.getFixedLength() != null 1487 ? part.getFixedLength().intValue() : part.getMaximumLength().intValue(); 1488 index < text_len && index - start_index < part_len; index++, context.parsePosition.setIndex( index ) ) 1489 { 1490 final char current = context.text.charAt( index ); 1491 1492 if ( current == ' ' && part.getType() != 'e' ) 1493 { 1494 if ( context.format == IbanFormat.PRINT && context.length % 4 == 0 1495 && ( context.previous == null || context.previous.charValue() != ' ' ) ) 1496 { // Skip letter format separator. 1497 part_len++; 1498 context.previous = Character.valueOf( current ); 1499 continue next; 1500 } 1501 else 1502 { // Unexpected letter format separator. 1503 context.parsePosition.setIndex( start_index ); 1504 context.parsePosition.setErrorIndex( index ); 1505 break next; 1506 } 1507 } 1508 1509 switch ( part.getType() ) 1510 { 1511 case 'a': 1512 // Upper case letters (alphabetic characters A-Z only) 1513 if ( current >= 'A' && current <= 'Z' ) 1514 { 1515 partBuilder.append( current ); 1516 context.length++; 1517 } 1518 else 1519 { 1520 context.parsePosition.setIndex( start_index ); 1521 context.parsePosition.setErrorIndex( index ); 1522 } 1523 break; 1524 case 'c': 1525 // Upper and lower case alphanumeric characters (A-Z, a-z and 0-9) 1526 if ( ( current >= 'A' && current <= 'Z' ) 1527 || ( current >= 'a' && current <= 'z' ) 1528 || ( current >= '0' && current <= '9' ) ) 1529 { 1530 partBuilder.append( current ); 1531 context.length++; 1532 } 1533 else 1534 { 1535 context.parsePosition.setIndex( start_index ); 1536 context.parsePosition.setErrorIndex( index ); 1537 } 1538 break; 1539 case 'e': 1540 // blank space 1541 if ( current == ' ' ) 1542 { 1543 partBuilder.append( current ); 1544 context.length++; 1545 } 1546 else 1547 { 1548 context.parsePosition.setIndex( start_index ); 1549 context.parsePosition.setErrorIndex( index ); 1550 } 1551 break; 1552 case 'n': 1553 // Digits (numeric characters 0 to 9 only) 1554 if ( current >= '0' && current <= '9' ) 1555 { 1556 partBuilder.append( current ); 1557 context.length++; 1558 } 1559 else 1560 { 1561 context.parsePosition.setIndex( start_index ); 1562 context.parsePosition.setErrorIndex( index ); 1563 } 1564 break; 1565 case 'l': 1566 // Literal 1567 if ( current == part.getLiteral().charAt( literal_index++ ) ) 1568 { 1569 context.length++; 1570 partBuilder.append( current ); 1571 } 1572 else 1573 { 1574 context.parsePosition.setIndex( start_index ); 1575 context.parsePosition.setErrorIndex( index ); 1576 } 1577 break; 1578 default: 1579 context.parsePosition.setIndex( start_index ); 1580 context.parsePosition.setErrorIndex( index ); 1581 break next; 1582 } 1583 1584 context.previous = Character.valueOf( current ); 1585 } 1586 1587 return context.parsePosition.getErrorIndex() < 0 ? partBuilder.toString() : null; 1588 } 1589 1590 /** 1591 * Tests a given character to conform to a literal. 1592 * 1593 * @param c The character to test. 1594 * 1595 * @return {@code true}, if {@code c} conforms to a literal; {@code false}, else. 1596 */ 1597 private static boolean isLiteral( final char c ) 1598 { 1599 return ( c >= 'A' && c <= 'Z' ); 1600 } 1601 1602 /** 1603 * Tests a given character to conform to a digit. 1604 * 1605 * @param c The character to test. 1606 * 1607 * @return {@code true}, if {@code c} conforms to a digit; {@code false}, else. 1608 */ 1609 private static boolean isDigit( final char c ) 1610 { 1611 return ( c >= '0' && c <= '9' ); 1612 } 1613 1614 /** 1615 * Tests a given character to conform to a type identifier. 1616 * 1617 * @param c The character to test. 1618 * 1619 * @return {@code true}, if {@code c} conforms to a type identifier; {@code false}, else. 1620 */ 1621 private static boolean isTypeIdentifier( final char c ) 1622 { 1623 return c == 'a' || c == 'c' || c == 'e' || c == 'n'; 1624 } 1625 1626 /** 1627 * Tests a given character to conform to a length indicator. 1628 * 1629 * @param c The character to test. 1630 * 1631 * @return {@code true}, if {@code c} conforms to a length indicator; {@code false}, else. 1632 */ 1633 private static boolean isLengthIndicator( final char c ) 1634 { 1635 return c == '!'; 1636 } 1637 1638 /** 1639 * Parses a given bank/branch id indices {@code String} like {@code 1-5} to produce a list of bank/branch id indices 1640 * compatible to {@link CharSequence#subSequence(int, int)}. 1641 * 1642 * @param str The string to parse. 1643 * 1644 * @return The indices of {@code str}. 1645 */ 1646 private static List<Integer> parseIndices( final String str ) 1647 { 1648 final String[] parts = str.split( "-" ); 1649 assert parts.length == 2 : "Unexpected bank/branch id indices '" + str + "'."; 1650 final List<Integer> indices = new ArrayList<Integer>( parts.length ); 1651 indices.add( 0, Integer.valueOf( parts[0] ) - 1 ); 1652 indices.add( 1, Integer.valueOf( parts[1] ) ); 1653 return Collections.unmodifiableList( indices ); 1654 } 1655 1656 /** 1657 * Translates characters to digits according to the MOD 97-10 (ISO 7064) algorithm. 1658 * 1659 * @param buffer The buffer to append digits to. 1660 * @param chars The characters to translate. 1661 */ 1662 private static void appendIso7064Digits( final StringBuilder buffer, final String chars ) 1663 { 1664 for ( int i = 0, l0 = chars.length(); i < l0; i++ ) 1665 { 1666 final char c = chars.charAt( i ); 1667 1668 if ( c >= 'A' && c <= 'Z' ) 1669 { 1670 buffer.append( Integer.toString( ( c - 'A' ) + 10 ) ); 1671 } 1672 else if ( c >= 'a' && c <= 'z' ) 1673 { 1674 buffer.append( Integer.toString( ( c - 'a' ) + 10 ) ); 1675 } 1676 else if ( c >= '0' && c <= '9' ) 1677 { 1678 buffer.append( c ); 1679 } 1680 else 1681 { 1682 throw new AssertionError( c ); 1683 } 1684 } 1685 } 1686 1687 /** 1688 * Formats an electronic format IBAN to a letter format IBAN. 1689 * 1690 * @param electronicFormat An electronic format IBAN. 1691 * 1692 * @return The given IBAN formatted to the letter format representation. 1693 */ 1694 private static String toLetterFormat( final String electronicFormat ) 1695 { 1696 final StringBuilder letter_format_builder = new StringBuilder( electronicFormat.length() ); 1697 1698 for ( int i = 0, l0 = electronicFormat.length(); i < l0; i++ ) 1699 { 1700 if ( i > 0 && i % 4 == 0 ) 1701 { 1702 letter_format_builder.append( ' ' ); 1703 } 1704 1705 letter_format_builder.append( electronicFormat.charAt( i ) ); 1706 } 1707 1708 return letter_format_builder.toString(); 1709 } 1710 1711 private static String toIbanCountryCode( final String countryCode ) 1712 { 1713 final String ibanCountryCode = IBAN_COUNTRY_CODES.get( countryCode ); 1714 return ibanCountryCode != null ? ibanCountryCode : countryCode; 1715 } 1716 1717 // SECTION-END 1718 // SECTION-START[Dependencies] 1719 // SECTION-END 1720 // SECTION-START[Properties] 1721 // SECTION-END 1722 // SECTION-START[Messages] 1723 // SECTION-END 1724}