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