All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jdtaus.iso13616.IBAN Maven / Gradle / Ivy

// SECTION-START[License Header]
// 
/*
 *   jDTAUS ⁑ ISO-13616
 *   Copyright (C) Christian Schulte, 2013-222
 *
 *   Permission to use, copy, modify, and/or distribute this software for any
 *   purpose with or without fee is hereby granted, provided that the above
 *   copyright notice and this permission notice appear in all copies.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 *   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 *   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 *   $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $
 *
 */
// 
// SECTION-END
package org.jdtaus.iso13616;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.math.BigInteger;
import java.net.URL;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formattable;
import java.util.Formatter;
import java.util.HashMap;
import java.util.IllegalFormatFlagsException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.FormattableFlags.ALTERNATE;
import static java.util.FormattableFlags.LEFT_JUSTIFY;
import static java.util.FormattableFlags.UPPERCASE;

// SECTION-START[Documentation]
// 
/**
 *
 * International Bank Account Number.
 * 

The IBAN structure is defined in ISO 13616-1 and consists of a two-letter ISO 3166-1 country code, followed by two * check digits and up to thirty alphanumeric characters for a BBAN (Basic Bank Account Number) which has a fixed length * per country and, included within it, a bank identifier with a fixed position and a fixed length per country. The * check digits are calculated based on the scheme defined in ISO/IEC 7064 (MOD97-10). The Society for Worldwide * Interbank Financial Telecommunication SCRL, SWIFT, has been designated by the ISO Technical Management Board to act * as the Registration Authority for ISO 13616. Nationally-agreed, ISO 13616-compliant IBAN formats are submitted to the * registration authority exclusively by the National Standards Body or the National Central Bank of the country. For * further information see the IBAN REGISTRY. * An updated version of the document may be found at SWIFT.

* *
*
Identifier:
jDTAUS ⁑ ISO-13616 ⁑ IBAN
*
Name:
jDTAUS ⁑ ISO-13616 ⁑ IBAN
*
Abstract:
No
*
Final:
Yes
*
Stateless:
No
*
* * @author Christian Schulte * @version 1.1 */ //
// SECTION-END // SECTION-START[Annotations] // @javax.annotation.Generated( value = "org.jomc.tools.SourceFileProcessor 1.9", comments = "See http://www.jomc.org/jomc/1.9/jomc-tools-1.9" ) // // SECTION-END public final class IBAN implements CharSequence, Comparable, Formattable, Serializable { // SECTION-START[IBAN] /** * International bank account number structure. *

The following character representations are used: *

* * * * *
aUpper case letters (alphabetic characters A-Z only)
cUpper and lower case alphanumeric characters (A-Z, a-z and 0-9)
eBlank space
nDigits (numeric characters 0 to 9 only)

*

The following length indications are used: *

* * *
nn!fixed length
nnmaximum length

* * @author Christian Schulte * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ */ private static final class Structure { /** * Part of a structure. * * @author Christian Schulte * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ */ private static final class Part { /** Type of the part. */ private final char type; /** The literal of the part or {@code null}. */ private final String literal; /** The fixed length of the part or {@code null}. */ private final Integer fixedLength; /** The maximum length of the part or {@code null}. */ private final Integer maximumLength; /** * Creates a new {@code Part} instance. * * @param type The type of the part. * @param literal The literal of the part or {@code null}. * @param fixedLength The fixed length of the part or {@code null}. * @param maximumLength The maximum length of the part or {@code null}. */ private Part( final char type, final String literal, final Integer fixedLength, final Integer maximumLength ) { super(); this.type = type; this.literal = literal; this.fixedLength = fixedLength; this.maximumLength = maximumLength; } /** * Gets the type of the part. *

The following types are used: *

* * * * * *
aUpper case letters (alphabetic characters A-Z only)
cUpper and lower case alphanumeric characters (A-Z, a-z and 0-9)
eBlank space
nDigits (numeric characters 0 to 9 only)
lLiteral

* * @return The type of the part. */ private char getType() { return this.type; } /** * Gets the literal of the part. * * @return The literal of the part or {@code null}, if the type of the part is not equal to {@code l}. */ private String getLiteral() { return this.literal; } /** * Gets the fixed length of the part. * * @return The fixed length of the part or {@code null}, if the part has no fixed length but a maximum * length. */ private Integer getFixedLength() { return this.fixedLength; } /** * Gets the maximum length of the part. * * @return The maximum length of the part or {@code null}, if the part has no maximum length but a fixed * length. */ private Integer getMaximumLength() { return this.maximumLength; } } /** The parts of the structure. */ private final List parts; /** * Creates a new {@code Structure} instance. * * @param parts The parts of the structure. */ private Structure( final List parts ) { super(); this.parts = parts; } /** * Gets the parts of the structure. * * @return The parts of the structure. */ private List getParts() { return this.parts; } } /** * Context of a parse operation. * * @author Christian Schulte * @version $JDTAUS: IBAN.java 8925 2017-09-03 04:47:22Z schulte $ */ private static final class ParseContext { /** The text to parse. */ private final String text; /** The {@code ParsePosition} of the context. */ private final ParsePosition parsePosition; /** The format to parse. */ private final IbanFormat format; /** The number of electronic format characters parsed. */ private int length; /** The previous character parsed. */ private Character previous; /** * Creates a new {@code ParseContext} instance. * * @param text The text to parse. * @param pos The {@code ParsePosition} backing the parse. * @param format The format to parse. */ private ParseContext( final String text, final ParsePosition pos, final IbanFormat format ) { super(); this.text = text; this.parsePosition = pos; this.format = format; this.length = 0; } } /** Serial version UID for backwards compatibility with 2007.45.x classes. */ private static final long serialVersionUID = -8123668345632147105L; /** Used to cache instances. */ private static volatile Reference> cacheReference = new SoftReference>( null ); /** Mappings of two-letter ISO 3166-1 country codes to SEPA country flags. */ private static final Map SEPA_COUNTRY_FLAGS = new HashMap( 128 ); /** Mappings of two-letter ISO 3166-1 country codes to BBAN structures. */ private static final Map BBAN_STRUCTURES = new HashMap( 128 ); /** Mappings of two-letter ISO 3166-1 country codes to BBAN bank identifier indices. */ private static final Map> BBAN_BANK_ID_INDICES = new HashMap>( 128 ); /** Mappings of two-letter ISO 3166-1 country codes to BBAN branch identifier indices. */ private static final Map> BBAN_BRANCH_ID_INDICES = new HashMap>( 128 ); /** Mappings of two-letter ISO 3166-1 country codes to IBAN structures. */ private static final Map IBAN_STRUCTURES = new HashMap( 128 ); /** Mappings of two-letter ISO 3166-1 country codes to IBAN country codes. */ private static final Map IBAN_COUNTRY_CODES = new HashMap( 128 ); /** List of supported two-letter ISO 3166-1 country codes. */ private static final List COUNTRIES = new ArrayList( 128 ); /** {@code BigInteger} constant {@code 97}. */ private static final BigInteger INTEGER_97 = BigInteger.valueOf( 97L ); /** {@code BigInteger} constant {@code 98}. */ private static final BigInteger INTEGER_98 = BigInteger.valueOf( 98L ); static { InputStream in = null; try { final Properties properties = new Properties(); final URL resource = IBAN.class.getResource( "IBAN.properties" ); assert resource != null : "Expected resource 'IBAN.properties' to exist."; if ( resource != null ) { in = resource.openStream(); properties.load( in ); in.close(); in = null; if ( properties.containsKey( "countries" ) ) { final String[] countries = properties.getProperty( "countries" ).split( "\\|" ); COUNTRIES.addAll( Arrays.asList( countries ) ); for ( int i = 0, s0 = countries.length; i < s0; i++ ) { if ( countries[i].length() != 2 ) { throw new AssertionError( countries[i] ); } final String bbanStructure = properties.getProperty( countries[i] + ".bban.structure" ); final String bbanBankIds = properties.getProperty( countries[i] + ".bban.bankid.indices" ); final String bbanBranchIds = properties.getProperty( countries[i] + ".bban.branchid.indices" ); final String ibanCountryCode = properties.getProperty( countries[i] + ".iban.countrycode" ); final String ibanStructure = properties.getProperty( countries[i] + ".iban.structure" ); final String sepa = properties.getProperty( countries[i] + ".sepa" ); SEPA_COUNTRY_FLAGS.put( countries[i], Boolean.valueOf( sepa ) ); if ( ibanCountryCode != null ) { IBAN_COUNTRY_CODES.put( countries[i], ibanCountryCode ); } if ( bbanStructure != null ) { BBAN_STRUCTURES.put( countries[i], parseStructure( bbanStructure ) ); if ( bbanBankIds != null ) { BBAN_BANK_ID_INDICES.put( countries[i], parseIndices( bbanBankIds ) ); } if ( bbanBranchIds != null ) { BBAN_BRANCH_ID_INDICES.put( countries[i], parseIndices( bbanBranchIds ) ); } } if ( ibanStructure != null ) { IBAN_STRUCTURES.put( countries[i], parseStructure( ibanStructure ) ); } } } } } catch ( final ParseException e ) { throw new AssertionError( e ); } catch ( final IOException e ) { throw new AssertionError( e ); } finally { try { if ( in != null ) { in.close(); } } catch ( final IOException e ) { throw new AssertionError( e ); } } } /** Maximum number of characters of an {@code IBAN}. */ public static final int MAX_CHARACTERS = 42; /** * The two-letter ISO 3166-1 country code of the IBAN. * @serial */ private String countryCode; /** * Flag indicating the country of the IBAN is part of the jurisdictional scope of the SEPA Schemes. * @serial */ private boolean sepaCountry; /** * The bank identifier part of the BBAN of the IBAN. * @serial */ private String bankIdentifier; /** * The branch identifier part of the BBAN of the IBAN. * @serial */ private String branchIdentifier; /** * The electronic format of the IBAN. * @serial */ private String electronicFormat; /** * The letter format of the IBAN. * @serial */ private String letterFormat; /** * The parts of the IBAN. * @serial */ private Comparable[] parts; /** Cached string representation of the instance. */ private transient String string; /** * Creates a new {@code IBAN} instance. * * @param countryCode The two-letter ISO 3166-1 country code of the IBAN. * @param sepaCountry Flag indicating the country is part of the jurisdictional scope of the SEPA Schemes. * @param bankIdentifier The bank identifier part of the BBAN of the IBAN. * @param branchIdentifier The branch identifier part of the BBAN of the IBAN or {@code null}. * @param electronicFormat The electronic format representation of the IBAN. * @param letterFormat The letter format representation of the IBAN. * @param parts The parts of the IBAN. * * @see #parse(String, ParsePosition) */ private IBAN( final String countryCode, final boolean sepaCountry, final String bankIdentifier, final String branchIdentifier, final String electronicFormat, final String letterFormat, final Comparable[] parts ) { super(); this.countryCode = countryCode; this.sepaCountry = sepaCountry; this.bankIdentifier = bankIdentifier; this.branchIdentifier = branchIdentifier; this.electronicFormat = electronicFormat; this.letterFormat = letterFormat; this.parts = parts; getCache().put( electronicFormat, this ); getCache().put( letterFormat, this ); } /** * Gets an array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN * standard. * * @return An array holding two-letter ISO 3166-1 country codes of all countries that have implemented the IBAN * standard. */ public static String[] getCountryCodes() { return COUNTRIES.toArray( new String[ COUNTRIES.size() ] ); } /** * Tests a given two-letter ISO 3166-1 country code to identify a country that is part of the jurisdictional scope * of the Single Euro Payment Area (SEPA) Schemes. * The * EPC List of SEPA Scheme Countries document lists the countries and territories which are part of the * jurisdictional scope of the Single Euro Payment Area (SEPA) Schemes. An updated version of the document may be * found at The European Payments Council (EPC). * * @param countryCode The two-letter ISO 3166-1 country code to test. * * @return {@code true}, if the country identified by {@code countryCode} is part of the jurisdictional scope of the * Single Euro Payment Area (SEPA) Schemes; {@code false}, if not. * * @throws NullPointerException if {@code countryCode} is {@code null}. * * @see #getCountryCodes() * @since 2007.46 */ public static boolean isSepaCountry( final String countryCode ) { if ( countryCode == null ) { throw new NullPointerException( "countryCode" ); } final Boolean sepaCountry = SEPA_COUNTRY_FLAGS.get( countryCode ); return sepaCountry != null ? sepaCountry : false; } /** * Gets the two-letter ISO 3166-1 country code identifying the country the IBAN belongs to. * * @return The two-letter ISO 3166-1 country code identifying the country the IBAN belongs to. * * @see #getCountryCodes() */ public String getCountryCode() { return this.countryCode; } /** * Gets a flag indicating the country of the IBAN to be part of the jurisdictional scope of the Single Euro Payment * Area (SEPA) Schemes. * * @return {@code true}, if the country of the IBAN is part of the jurisdictional scope of the Single Euro Payment * Area (SEPA) Schemes; {@code false}, else. * * @see #getCountryCode() * @see #isSepaCountry(java.lang.String) */ public boolean isSepaCountry() { return this.sepaCountry; } /** * Gets the bank identifier part of the BBAN of the IBAN. * * @return The bank identifier part of the BBAN of the IBAN or {@code null}. */ public String getBankIdentifier() { return this.bankIdentifier; } /** * Gets the branch identifier part of the BBAN of the IBAN. * * @return The branch identifier part of the BBAN of the IBAN or {@code null}. */ public String getBranchIdentifier() { return this.branchIdentifier; } /** * Parses text from a BBAN string to produce an {@code IBAN} instance. * * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create. * @param bban A string to parse BBAN characters from. * * @return The parsed value. * * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}. * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes}. * @throws IbanSyntaxException if the parse fails or the length of {@code bban} is invalid. * * @see #getCountryCodes() * @see #valueOf(java.lang.String, java.lang.String) */ public static IBAN parse( final String countryCode, final String bban ) throws IbanSyntaxException { if ( countryCode == null ) { throw new NullPointerException( "countryCode" ); } if ( bban == null ) { throw new NullPointerException( "bban" ); } final String ibanCountryCode = toIbanCountryCode( countryCode ); final Structure structure = BBAN_STRUCTURES.get( ibanCountryCode ); if ( structure == null ) { throw new IllegalArgumentException( countryCode ); } final List bankIdIndices = BBAN_BANK_ID_INDICES.get( ibanCountryCode ); final List branchIdIndices = BBAN_BRANCH_ID_INDICES.get( ibanCountryCode ); final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( countryCode ); // Parse the parts. final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS ); final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS ); final List> comparables = new ArrayList>( structure.getParts().size() + 2 ); final ParseContext context = new ParseContext( bban, new ParsePosition( 0 ), bban.length() > 4 && bban.charAt( 4 ) == ' ' ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); for ( int p = 0, s0 = structure.getParts().size(); p < s0 && context.parsePosition.getErrorIndex() < 0; p++ ) { final Structure.Part part = structure.getParts().get( p ); final String chars = parsePart( context, part ); if ( context.parsePosition.getErrorIndex() < 0 ) { if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) { // Fixed length mismatch. throw new IbanSyntaxException( bban, context.parsePosition.getIndex() ); } electronic_format_builder.append( chars ); switch ( part.getType() ) { case 'n': comparables.add( new BigInteger( chars ) ); break; default: comparables.add( chars ); break; } } else { // Invalid part. throw new IbanSyntaxException( bban, context.parsePosition.getErrorIndex() ); } } // Extract bank id and branch id. if ( bankIdIndices != null ) { bank_id_builder.append( electronic_format_builder.subSequence( bankIdIndices.get( 0 ), bankIdIndices.get( 1 ) ) ); } if ( branchIdIndices != null ) { branch_id_builder.append( electronic_format_builder.subSequence( branchIdIndices.get( 0 ), branchIdIndices.get( 1 ) ) ); } // Calculate checksum. final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); appendIso7064Digits( integer_builder, electronic_format_builder.toString() ); appendIso7064Digits( integer_builder, ibanCountryCode ); appendIso7064Digits( integer_builder, "00" ); final BigInteger integer = new BigInteger( integer_builder.toString() ); final BigInteger checksum = INTEGER_98.subtract( integer.remainder( INTEGER_97 ) ); final String checksumPart = checksum.compareTo( BigInteger.TEN ) < 0 ? "0" + checksum : checksum.toString(); comparables.add( 0, checksumPart ); comparables.add( 0, ibanCountryCode ); electronic_format_builder.insert( 0, checksumPart ); electronic_format_builder.insert( 0, ibanCountryCode ); return new IBAN( countryCode, sepa_country, bank_id_builder.length() > 0 ? bank_id_builder.toString() : null, branch_id_builder.length() > 0 ? branch_id_builder.toString() : null, electronic_format_builder.toString(), toLetterFormat( electronic_format_builder.toString() ), comparables.toArray( new Comparable[ comparables.size() ] ) ); } /** * Parses text from a string to produce an {@code IBAN} instance. *

The method attempts to parse text starting at the index given by {@code pos}. If parsing succeeds, then the * index of {@code pos} is updated to the index after the last character used (parsing does not necessarily use all * characters up to the end of the string), and the parsed value is returned. The updated {@code pos} can be used to * indicate the starting point for the next call to this method.

* * @param text A string to parse IBAN characters from. * @param pos A {@code ParsePosition} object with index and error index information as described above. * * @return The parsed value or {@code null}, if the parse fails. * * @throws NullPointerException if {@code text} or {@code pos} is {@code null}. * @throws IbanCheckDigitsException if check digits validation of the parsed value fails. */ public static IBAN parse( final String text, final ParsePosition pos ) throws IbanCheckDigitsException { if ( text == null ) { throw new NullPointerException( "text" ); } if ( pos == null ) { throw new NullPointerException( "pos" ); } IBAN ret = null; final int begin_index = pos.getIndex(); // Extract the country code to query properties. if ( text.length() > begin_index + 1 ) { final String country_code = text.substring( begin_index, begin_index + 2 ); if ( !isLiteral( country_code.charAt( 0 ) ) ) { pos.setIndex( begin_index ); pos.setErrorIndex( begin_index ); } else if ( !isLiteral( country_code.charAt( 1 ) ) ) { pos.setIndex( begin_index ); pos.setErrorIndex( begin_index + 1 ); } else { final Structure structure = IBAN_STRUCTURES.get( country_code ); if ( structure != null ) { // Parse the parts. final boolean sepa_country = SEPA_COUNTRY_FLAGS.get( country_code ); final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); final StringBuilder bank_id_builder = new StringBuilder( MAX_CHARACTERS ); final StringBuilder branch_id_builder = new StringBuilder( MAX_CHARACTERS ); final List> comparables = new ArrayList>( structure.getParts().size() ); final ParseContext context = new ParseContext( text, pos, text.length() > begin_index + 4 && text.charAt( begin_index + 4 ) == ' ' ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); for ( int p = 0, s0 = structure.getParts().size(); p < s0 && context.parsePosition.getErrorIndex() < 0; p++ ) { final Structure.Part part = structure.getParts().get( p ); final String chars = parsePart( context, part ); if ( context.parsePosition.getErrorIndex() < 0 ) { if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) { // Fixed length mismatch. context.parsePosition.setErrorIndex( context.parsePosition.getIndex() ); context.parsePosition.setIndex( begin_index ); break; } electronic_format_builder.append( chars ); switch ( part.getType() ) { case 'n': comparables.add( new BigInteger( chars ) ); break; default: comparables.add( chars ); break; } } } if ( context.parsePosition.getErrorIndex() < 0 ) { // Validate checksum. final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) ); appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) ); final BigInteger integer = new BigInteger( integer_builder.toString() ); if ( integer.remainder( INTEGER_97 ).equals( BigInteger.ONE ) ) { final List bankIdIndices = BBAN_BANK_ID_INDICES.get( country_code ); final List branchIdIndices = BBAN_BRANCH_ID_INDICES.get( country_code ); if ( bankIdIndices != null ) { bank_id_builder.append( electronic_format_builder.subSequence( bankIdIndices.get( 0 ) + 4, bankIdIndices.get( 1 ) + 4 ) ); } if ( branchIdIndices != null ) { branch_id_builder.append( electronic_format_builder.subSequence( branchIdIndices.get( 0 ) + 4, branchIdIndices.get( 1 ) + 4 ) ); } ret = new IBAN( country_code, sepa_country, bank_id_builder.length() > 0 ? bank_id_builder.toString() : null, branch_id_builder.length() > 0 ? branch_id_builder.toString() : null, electronic_format_builder.toString(), toLetterFormat( electronic_format_builder.toString() ), comparables.toArray( new Comparable[ comparables.size() ] ) ); } else { // Invalid checksum. context.parsePosition.setIndex( begin_index ); context.parsePosition.setErrorIndex( begin_index + 2 ); throw new IbanCheckDigitsException( electronic_format_builder.toString(), (Number) comparables.get( 1 ) ); } } } else { // Unsupported country code. pos.setIndex( begin_index ); pos.setErrorIndex( begin_index + 1 ); } } } else { // No country code provided. pos.setIndex( begin_index ); pos.setErrorIndex( begin_index ); if ( begin_index < text.length() && isLiteral( text.charAt( begin_index ) ) ) { pos.setErrorIndex( begin_index + 1 ); } } return ret; } /** * Parses text from the beginning of the given string to produce an {@code IBAN} instance. *

Unlike the {@link #parse(String, ParsePosition)} method this method throws an {@code IbanSyntaxException} if * {@code text} cannot be parsed or is of invalid length.

* * @param text A string to parse IBAN characters from. * * @return The parsed value. * * @throws NullPointerException if {@code text} is {@code null}. * @throws IbanSyntaxException if the parse fails or the length of {@code text} is invalid. * @throws IbanCheckDigitsException if check digits validation of the parsed value fails. * * @see #valueOf(java.lang.String) */ public static IBAN parse( final String text ) throws IbanSyntaxException, IbanCheckDigitsException { if ( text == null ) { throw new NullPointerException( "text" ); } IBAN iban = getCache().get( text ); if ( iban == null ) { final ParsePosition pos = new ParsePosition( 0 ); iban = IBAN.parse( text, pos ); if ( iban == null || pos.getErrorIndex() != -1 || pos.getIndex() < text.length() ) { throw new IbanSyntaxException( text, pos.getErrorIndex() != -1 ? pos.getErrorIndex() : pos.getIndex() ); } } return iban; } /** * Parses text from the beginning of the given string to produce an {@code IBAN} instance. *

Unlike the {@link #parse(String)} method this method throws an {@code IllegalArgumentException} if * {@code text} cannot be parsed, is of invalid length or check digits validation fails.

* * @param text A string to parse IBAN characters from. * * @return The parsed value. * * @throws NullPointerException if {@code text} is {@code null}. * @throws IllegalArgumentException if the parse fails, the length of {@code text} is invalid or if check digits * validation of the parsed value fails. * * @see #parse(java.lang.String) */ public static IBAN valueOf( final String text ) { if ( text == null ) { throw new NullPointerException( "text" ); } try { return IBAN.parse( text ); } catch ( final IbanSyntaxException e ) { throw new IllegalArgumentException( text, e ); } catch ( final IbanCheckDigitsException e ) { throw new IllegalArgumentException( text, e ); } } /** * Parses text from a BBAN string to produce an {@code IBAN} instance. *

Unlike the {@link #parse(String, String)} method this method throws an {@code IllegalArgumentException} if the * parse fails or the length of {@code bban} is invalid.

* * @param countryCode The two-letter ISO 3166-1 country code of the IBAN to create. * @param bban A string to parse BBAN characters from. * * @return The parsed value. * * @throws NullPointerException if {@code countryCode} or {@code bban} is {@code null}. * @throws IllegalArgumentException if the country identified by {@code countryCode} has not implemented the IBAN * standard, that is, {@code countryCode} is not contained in the array returned by method {@code getCountryCodes}, * if the parse fails, or if the length of {@code bban} is invalid. * * @see #parse(java.lang.String, java.lang.String) * @since 2007.46 */ public static IBAN valueOf( final String countryCode, final String bban ) { if ( countryCode == null ) { throw new NullPointerException( "countryCode" ); } if ( bban == null ) { throw new NullPointerException( "bban" ); } try { return IBAN.parse( countryCode, bban ); } catch ( final IbanSyntaxException e ) { throw new IllegalArgumentException( bban, e ); } } /** * Checks a given character to belong to the IBAN alphabet. * * @param c The character to check. * * @return {@code true} if {@code c} is a character of the IBAN alphabet; {@code false}, else. */ public static boolean isIbanAlphabet( final char c ) { return ( c >= 'A' && c <= 'Z' ) || ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ); } /** * Parses text from a string to peek at a national {@code IBAN} format. *

This method peeks at the given string to test whether it denotes a possibly valid national IBAN format.

* * @param text A string to peek at. * * @return {@code true}, if {@code text} denotes a (possibly partially) valid national IBAN format; {@code false}, * if {@code text} does not denote a valid national IBAN format. * * @throws NullPointerException if {@code text} is {@code null}. * @throws IbanCheckDigitsException if check digits validation fails. */ public static boolean peekIban( final String text ) throws IbanCheckDigitsException { if ( text == null ) { throw new NullPointerException( "text" ); } boolean valid = true; boolean complete = true; // Extract the country code to query properties. if ( text.length() > 1 ) { final String country_code = text.substring( 0, 2 ); final Structure structure = IBAN_STRUCTURES.get( country_code ); if ( structure != null ) { // Parse the parts. final StringBuilder electronic_format_builder = new StringBuilder( MAX_CHARACTERS ); final ParseContext context = new ParseContext( text, new ParsePosition( 0 ), text.length() > 4 && text.charAt( 4 ) == ' ' ? IbanFormat.PRINT : IbanFormat.ELECTRONIC ); for ( int p = 0, s0 = structure.getParts().size(); p < s0 && context.parsePosition.getErrorIndex() < 0 && complete; p++ ) { final Structure.Part part = structure.getParts().get( p ); final String chars = parsePart( context, part ); if ( context.parsePosition.getErrorIndex() < 0 ) { if ( part.getFixedLength() != null && chars.length() != part.getFixedLength().intValue() ) { // Fixed length mismatch. complete = false; break; } electronic_format_builder.append( chars ); } } if ( context.parsePosition.getErrorIndex() < 0 ) { if ( complete ) { // Validate checksum. final StringBuilder integer_builder = new StringBuilder( MAX_CHARACTERS * 2 ); appendIso7064Digits( integer_builder, electronic_format_builder.substring( 4 ) ); appendIso7064Digits( integer_builder, electronic_format_builder.substring( 0, 4 ) ); final BigInteger integer = new BigInteger( integer_builder.toString() ); valid = integer.remainder( INTEGER_97 ).equals( BigInteger.ONE ); if ( !valid ) { throw new IbanCheckDigitsException( electronic_format_builder.toString(), new BigInteger( electronic_format_builder.substring( 2, 4 ) ) ); } if ( context.parsePosition.getIndex() != text.length() ) { // Unexpected length. valid = false; } } } else { // Invalid part. valid = false; } } else { // Unsupported country code. valid = false; } } else if ( text.length() > 0 && !( text.charAt( 0 ) >= 'A' && text.charAt( 0 ) <= 'Z' ) ) { // Illegal first character. valid = false; } return valid; } /** * Formats an IBAN and appends the resulting text to a given {@code Appendable}. * * @param format The format to use. * @param appendable The {@code Appendable} to append the formatted IBAN to. * * @return The value passed in as {@code appendable}. * * @throws NullPointerException if {@code format} or {@code appendable} is {@code null}. * @throws IOException if appending fails. */ public Appendable append( final IbanFormat format, final Appendable appendable ) throws IOException { if ( format == null ) { throw new NullPointerException( "format" ); } if ( appendable == null ) { throw new NullPointerException( "appendable" ); } switch ( format ) { case ELECTRONIC: return appendable.append( this.electronicFormat ); case PRINT: return appendable.append( this.letterFormat ); default: throw new AssertionError( format ); } } /** * Formats an IBAN to produce a string. * * @param format The format to use. * * @return The formatted string. * * @throws NullPointerException if {@code format} is {@code null}. */ public String toString( final IbanFormat format ) { if ( format == null ) { throw new NullPointerException( "format" ); } switch ( format ) { case ELECTRONIC: return this.electronicFormat; case PRINT: return this.letterFormat; default: throw new AssertionError( format ); } } /** * Formats the object using the provided {@code Formatter}. *

This method uses the {@code ELECTRONIC} format by default. The {@code PRINT} format can be used by specifying * the alternate form flag ({@code #}) in a format specifier.

* * @param formatter The {@code Formatter}. * @param flags The flags to modify the output format. * @param width The minimum number of characters to be written to the output. * @param precision The maximum number of characters to be written to the output. * * @throws IllegalFormatFlagsException if the {@code UPPERCASE} flag is set. */ public void formatTo( final Formatter formatter, final int flags, final int width, final int precision ) { if ( formatter == null ) { throw new NullPointerException( "formatter" ); } if ( ( flags & UPPERCASE ) == UPPERCASE ) { final StringBuilder flagsBuilder = new StringBuilder( 3 ); if ( ( flags & ALTERNATE ) == ALTERNATE ) { flagsBuilder.append( "#" ); } if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY ) { flagsBuilder.append( "-" ); } flagsBuilder.append( "^" ); throw new IllegalFormatFlagsException( flagsBuilder.toString() ); } final IbanFormat format = ( flags & ALTERNATE ) == ALTERNATE ? IbanFormat.PRINT : IbanFormat.ELECTRONIC; String str = this.toString( format ); if ( precision != -1 && precision < str.length() ) { str = str.substring( 0, precision ); } final StringBuilder stringBuilder = new StringBuilder( str ); if ( width != -1 ) { final int len = width - stringBuilder.length(); if ( len > 0 ) { final char[] pad = new char[ len ]; Arrays.fill( pad, ' ' ); if ( ( flags & LEFT_JUSTIFY ) == LEFT_JUSTIFY ) { stringBuilder.append( pad ); } else { stringBuilder.insert( 0, pad ); } } } formatter.format( stringBuilder.toString() ); } /** * Returns the length of this character sequence. *

The length is the number of 16-bit {@code char}s in the sequence.

* * @return The number of {@code char}s in this sequence. */ public int length() { return this.electronicFormat.length(); } /** * Returns the {@code char} value at the specified index. *

An index ranges from zero to {@code length() - 1}. The first {@code char} value of the sequence is at index * zero, the next at index one, and so on, as for array indexing.

*

If the {@code char} value specified by the index is a surrogate, the surrogate value is returned.

* * @param index The index of the {@code char} value to return. * * @return The {@code char} value at {@code index}. * * @throws IndexOutOfBoundsException if {@code index} is negative or not less than the length of the character * sequence. * * @see #length() */ public char charAt( final int index ) { return this.electronicFormat.charAt( index ); } /** * Returns a new {@code CharSequence} that is a subsequence of this sequence. *

The subsequence starts with the {@code char} value at the specified index and ends with the {@code char} value * at index {@code end - 1}. The length (in {@code char}s) of the returned sequence is {@code end - start}, so if * {@code start == end} then an empty sequence is returned.

* * @param start The start index, inclusive. * @param end The end index, exclusive. * * @return The subsequence starting at {@code start} up to {@code end -1}. * * @throws IndexOutOfBoundsException if {@code start} or {@code end} are negative, if {@code end} is greater than * the length of the character sequence, or if {@code start} is greater than {@code end}. */ public CharSequence subSequence( final int start, final int end ) { return this.electronicFormat.subSequence( start, end ); } /** * Gets the hash code value of the object. * * @return The hash code value of the object. */ @Override public int hashCode() { return this.electronicFormat.hashCode(); } /** * Indicates whether some other object is equal to this one. * * @param o The reference object with which to compare. * * @return {@code true} if this object is the same as {@code o}; {@code false} otherwise. */ @Override public boolean equals( final Object o ) { boolean equal = this == o; if ( !equal && ( o instanceof IBAN ) ) { equal = this.electronicFormat.equals( ( (IBAN) o ).electronicFormat ); } return equal; } /** * Gets a string representation of the object. * * @return A string representation of the object. */ @Override public String toString() { return super.toString() + this.internalString(); } /** * Compares this object with the specified object for order. *

IBAN comparison respects the national IBAN formats when comparing IBANs of the same country. Any country codes * and check digits are ignored.

* * @param o The Object to be compared. * * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or greater than * the specified object. * * @throws NullPointerException if {@code o} is {@code null}. */ public int compareTo( final IBAN o ) { if ( o == null ) { throw new NullPointerException( "o" ); } int r = this.getCountryCode().compareTo( o.getCountryCode() ); for ( int i = 2, s0 = this.parts.length; r == 0 && i < s0; r = this.parts[i].compareTo( o.parts[i] ), i++ ); return r; } /** * Gets the current cache instance. * * @return Current cache instance. */ private static Map getCache() { Map cache = cacheReference.get(); if ( cache == null ) { cache = new ConcurrentHashMap( 1024 ); cacheReference = new SoftReference>( cache ); } return cache; } /** * Creates a string representing the properties of the instance. * * @return A string representing the properties of the instance. */ private String internalString() { if ( this.string == null ) { final StringBuilder b = new StringBuilder( 500 ).append( "{" ); b.append( "countryCode=" ).append( this.countryCode ). append( ", sepaCountry=" ).append( this.sepaCountry ). append( ", bankIdentifier=" ).append( this.bankIdentifier ). append( ", branchIdentifier=" ).append( this.branchIdentifier ). append( ", electronicFormat=" ).append( this.electronicFormat ). append( ", letterFormat=" ).append( this.letterFormat ). append( ", parts={" ); final StringBuilder partBuilder = new StringBuilder(); for ( int i = 0, s0 = this.parts.length; i < s0; i++ ) { partBuilder.append( ",[" ).append( i ).append( "]=" ).append( this.parts[i] ); } this.string = b.append( partBuilder.substring( 1 ) ).append( "}}" ).toString(); } return this.string; } /** * Parses a given structure string to produce a list of {@code Part}s. * * @param structure The structure string to parse. * * @return The parsed {@code Structure}. * * @throws ParseException if parsing {@code structure} fails. */ private static Structure parseStructure( final String structure ) throws ParseException { boolean in_literal = false; boolean in_part = false; boolean fixed_length_part = false; final List parts = new ArrayList( IBAN.MAX_CHARACTERS ); final StringBuilder literalBuilder = new StringBuilder( IBAN.MAX_CHARACTERS ); final StringBuilder numberBuilder = new StringBuilder( IBAN.MAX_CHARACTERS ); for ( int i = 0, s0 = structure.length(); i < s0; i++ ) { final char c = structure.charAt( i ); if ( in_part ) { // Expect number or "(n|a|c|e)|!(n|a|c|e)" if ( isDigit( c ) ) { if ( fixed_length_part ) { // Expect type after length indicator. throw new ParseException( structure, i ); } numberBuilder.append( c ); } else if ( isTypeIdentifier( c ) ) { if ( fixed_length_part ) { parts.add( new Structure.Part( c, null, Integer.parseInt( numberBuilder.toString() ), null ) ); } else { parts.add( new Structure.Part( c, null, null, Integer.parseInt( numberBuilder.toString() ) ) ); } numberBuilder.setLength( 0 ); in_part = false; fixed_length_part = false; } else if ( isLengthIndicator( c ) ) { if ( fixed_length_part ) { // Expect type after length indicator. throw new ParseException( structure, i ); } fixed_length_part = true; } else { throw new ParseException( structure, i ); } } else if ( in_literal ) { // Expect literal or number starting a part. if ( isLiteral( c ) ) { literalBuilder.append( c ); } else if ( isDigit( c ) ) { parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) ); numberBuilder.append( c ); literalBuilder.setLength( 0 ); in_part = true; in_literal = false; } else { throw new ParseException( structure, i ); } } else { if ( fixed_length_part ) { // Expect type after length indicator. throw new ParseException( structure, i ); } // Expect number starting a part or upper-case letter starting a literal. if ( isDigit( c ) ) { numberBuilder.append( c ); in_part = true; } else if ( isLiteral( c ) ) { literalBuilder.append( c ); in_literal = true; } else { throw new ParseException( structure, i ); } } } if ( fixed_length_part ) { // Expect type after length indicator. throw new ParseException( structure, structure.length() ); } if ( in_part ) { // Unclosed part. throw new ParseException( structure, structure.length() ); } if ( in_literal ) { // Literal at end of structure. parts.add( new Structure.Part( 'l', literalBuilder.toString(), literalBuilder.length(), null ) ); } return new Structure( parts ); } /** * Parses text from a string according to a given {@code Part} to produce a {@code String} instance. * * @param context The context of the parse. * @param part The {@code Part} to parse. * * @return The parsed value, or {@code null} if the parse fails. */ private static String parsePart( final ParseContext context, final Structure.Part part ) { final StringBuilder partBuilder = new StringBuilder( MAX_CHARACTERS ); final int start_index = context.parsePosition.getIndex(); next: for ( int index = context.parsePosition.getIndex(), text_len = context.text.length(), literal_index = 0, part_len = part.getFixedLength() != null ? part.getFixedLength().intValue() : part.getMaximumLength().intValue(); index < text_len && index - start_index < part_len; index++, context.parsePosition.setIndex( index ) ) { final char current = context.text.charAt( index ); if ( current == ' ' && part.getType() != 'e' ) { if ( context.format == IbanFormat.PRINT && context.length % 4 == 0 && ( context.previous == null || context.previous.charValue() != ' ' ) ) { // Skip letter format separator. part_len++; context.previous = Character.valueOf( current ); continue next; } else { // Unexpected letter format separator. context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); break next; } } switch ( part.getType() ) { case 'a': // Upper case letters (alphabetic characters A-Z only) if ( current >= 'A' && current <= 'Z' ) { partBuilder.append( current ); context.length++; } else { context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); } break; case 'c': // Upper and lower case alphanumeric characters (A-Z, a-z and 0-9) if ( ( current >= 'A' && current <= 'Z' ) || ( current >= 'a' && current <= 'z' ) || ( current >= '0' && current <= '9' ) ) { partBuilder.append( current ); context.length++; } else { context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); } break; case 'e': // blank space if ( current == ' ' ) { partBuilder.append( current ); context.length++; } else { context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); } break; case 'n': // Digits (numeric characters 0 to 9 only) if ( current >= '0' && current <= '9' ) { partBuilder.append( current ); context.length++; } else { context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); } break; case 'l': // Literal if ( current == part.getLiteral().charAt( literal_index++ ) ) { context.length++; partBuilder.append( current ); } else { context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); } break; default: context.parsePosition.setIndex( start_index ); context.parsePosition.setErrorIndex( index ); break next; } context.previous = Character.valueOf( current ); } return context.parsePosition.getErrorIndex() < 0 ? partBuilder.toString() : null; } /** * Tests a given character to conform to a literal. * * @param c The character to test. * * @return {@code true}, if {@code c} conforms to a literal; {@code false}, else. */ private static boolean isLiteral( final char c ) { return ( c >= 'A' && c <= 'Z' ); } /** * Tests a given character to conform to a digit. * * @param c The character to test. * * @return {@code true}, if {@code c} conforms to a digit; {@code false}, else. */ private static boolean isDigit( final char c ) { return ( c >= '0' && c <= '9' ); } /** * Tests a given character to conform to a type identifier. * * @param c The character to test. * * @return {@code true}, if {@code c} conforms to a type identifier; {@code false}, else. */ private static boolean isTypeIdentifier( final char c ) { return c == 'a' || c == 'c' || c == 'e' || c == 'n'; } /** * Tests a given character to conform to a length indicator. * * @param c The character to test. * * @return {@code true}, if {@code c} conforms to a length indicator; {@code false}, else. */ private static boolean isLengthIndicator( final char c ) { return c == '!'; } /** * Parses a given bank/branch id indices {@code String} like {@code 1-5} to produce a list of bank/branch id indices * compatible to {@link CharSequence#subSequence(int, int)}. * * @param str The string to parse. * * @return The indices of {@code str}. */ private static List parseIndices( final String str ) { final String[] parts = str.split( "-" ); assert parts.length == 2 : "Unexpected bank/branch id indices '" + str + "'."; final List indices = new ArrayList( parts.length ); indices.add( 0, Integer.valueOf( parts[0] ) - 1 ); indices.add( 1, Integer.valueOf( parts[1] ) ); return Collections.unmodifiableList( indices ); } /** * Translates characters to digits according to the MOD 97-10 (ISO 7064) algorithm. * * @param buffer The buffer to append digits to. * @param chars The characters to translate. */ private static void appendIso7064Digits( final StringBuilder buffer, final String chars ) { for ( int i = 0, l0 = chars.length(); i < l0; i++ ) { final char c = chars.charAt( i ); if ( c >= 'A' && c <= 'Z' ) { buffer.append( Integer.toString( ( c - 'A' ) + 10 ) ); } else if ( c >= 'a' && c <= 'z' ) { buffer.append( Integer.toString( ( c - 'a' ) + 10 ) ); } else if ( c >= '0' && c <= '9' ) { buffer.append( c ); } else { throw new AssertionError( c ); } } } /** * Formats an electronic format IBAN to a letter format IBAN. * * @param electronicFormat An electronic format IBAN. * * @return The given IBAN formatted to the letter format representation. */ private static String toLetterFormat( final String electronicFormat ) { final StringBuilder letter_format_builder = new StringBuilder( electronicFormat.length() ); for ( int i = 0, l0 = electronicFormat.length(); i < l0; i++ ) { if ( i > 0 && i % 4 == 0 ) { letter_format_builder.append( ' ' ); } letter_format_builder.append( electronicFormat.charAt( i ) ); } return letter_format_builder.toString(); } private static String toIbanCountryCode( final String countryCode ) { final String ibanCountryCode = IBAN_COUNTRY_CODES.get( countryCode ); return ibanCountryCode != null ? ibanCountryCode : countryCode; } // SECTION-END // SECTION-START[Dependencies] // SECTION-END // SECTION-START[Properties] // SECTION-END // SECTION-START[Messages] // SECTION-END }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy