diff --git a/CHANGELOG.md b/CHANGELOG.md index a9dccde1..0d2adf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Document alternatives to jbanking (#164). +- Make `CreditorIdentifier#REGEX` public (as part of #172). ### Changed - (**breaking change**) Make `CreditorIdentifier` final (#116). - (**breaking change**) Rename `Bic#BIC_REGEX` to `Bic#REGEX` and change it to not accept lower-case character anymore (as part of #170). -- Get rid of regexes to validate BICs (#170). This significantly increased the performances of BIC validation (x3) and +- Get rid of regexes to validate BICs (#170). This significantly increased the performances of validation (x3) and creation (x4). +- Get rid of regexes to validate Creditor Identifiers (#172). This significantly increased the performances of + validation (x3) and creation (x4). +- Improve javadoc (as part of #170 and #172). ### Fixed diff --git a/src/main/java/fr/marcwrobel/jbanking/creditor/CreditorIdentifier.java b/src/main/java/fr/marcwrobel/jbanking/creditor/CreditorIdentifier.java index 4efacbb7..28ab3205 100644 --- a/src/main/java/fr/marcwrobel/jbanking/creditor/CreditorIdentifier.java +++ b/src/main/java/fr/marcwrobel/jbanking/creditor/CreditorIdentifier.java @@ -2,9 +2,9 @@ import fr.marcwrobel.jbanking.IsoCountry; import fr.marcwrobel.jbanking.iban.IbanCheckDigit; +import fr.marcwrobel.jbanking.internal.AsciiCharacters; import java.io.Serializable; import java.util.Optional; -import java.util.regex.Pattern; /** * A Creditor Identifier (CI) code as specified by the @@ -54,9 +54,13 @@ public final class CreditorIdentifier implements Serializable { */ private static final long serialVersionUID = 0; - private static final String BASIC_REGEX = "[A-Za-z]{2}\\d{2}[A-Za-z0-9]{3}[A-Za-z0-9]+"; - private static final Pattern BASIC_PATTERN = Pattern.compile(BASIC_REGEX); + /** + * A simple regex that validate well-formed Creditor Identifiers. + */ + @SuppressWarnings("unused") // kept for documentation purposes + public static final String REGEX = "[A-Z]{2}[0-9]{2}[A-Z0-9]{3}[A-Z0-9]+"; + private static final int CREDITOR_IDENTIFIER_MIN_LENGTH = 8; private static final int COUNTRY_CODE_INDEX = 0; private static final int COUNTRY_CODE_LENGTH = 2; private static final int CHECK_DIGITS_INDEX = COUNTRY_CODE_INDEX + COUNTRY_CODE_LENGTH; @@ -68,18 +72,51 @@ public final class CreditorIdentifier implements Serializable { /** * The normalized form of this Creditor Identifier. */ - private final String creditorId; + private final String normalizedCi; + + /** + * Create a new Creditor Identifier from the given string. + * + * @param creditorId A non-null String. + * @throws IllegalArgumentException if the given string is {@code null} + * @throws CreditorIdentifierFormatException if the given string does not match {@value #REGEX} or if its country code is not + * known in {@link fr.marcwrobel.jbanking.IsoCountry} or if its check digit is wrong + */ + public CreditorIdentifier(String creditorId) { + if (creditorId == null) { + throw new IllegalArgumentException("the creditor identifier argument cannot be null"); + } + + String normalizedCreditorId = normalize(creditorId); + + if (!isWellFormatted(normalizedCreditorId)) { + throw CreditorIdentifierFormatException.forNotProperlyFormattedInput(normalizedCreditorId); + } + + Optional country = findCountryFor(normalizedCreditorId); + if (!country.isPresent()) { + throw CreditorIdentifierFormatException.forUnknownCountry(creditorId); + } + + String normalizedCreditorIdWithoutBusinessCode = removeBusinessCode(normalizedCreditorId); + if (!IbanCheckDigit.INSTANCE.validate(normalizedCreditorIdWithoutBusinessCode)) { + throw CreditorIdentifierFormatException.forIncorrectCheckDigits(creditorId); + } + + this.normalizedCi = normalizedCreditorId; + } /** * Create a new Creditor Identifier from the given country code, the creditor business code and the creditor national id. * + *

+ * The check digit is automatically calculated. + * * @param country A non-null IsoCountry. * @param businessCode A non-null String. * @param creditorNationalId A non-null String. - * @throws IllegalArgumentException if either the IsoCountry or BBAN is {@code null} - * @throws fr.marcwrobel.jbanking.creditor.CreditorIdentifierFormatException if a valid Creditor Identifier could not be - * calculated using the given IsoCountry, business code and - * creditor national id + * @throws IllegalArgumentException if either of the given strings is null + * @throws CreditorIdentifierFormatException if the resulting creditor identifier does not match {@value #REGEX}. */ public CreditorIdentifier(IsoCountry country, String businessCode, String creditorNationalId) { if (country == null) { @@ -97,90 +134,51 @@ public CreditorIdentifier(IsoCountry country, String businessCode, String credit String normalizedNationalId = normalize(creditorNationalId); String normalizedCreditorId = country.getAlpha2Code() + "00" + normalizedNationalId; - if (isNotWellFormatted(normalizedCreditorId)) { + if (!isWellFormatted(normalizedCreditorId)) { throw CreditorIdentifierFormatException.forNotProperlyFormattedInput(creditorNationalId); } String checkDigits = IbanCheckDigit.INSTANCE.calculate(normalizedCreditorId); - this.creditorId = country.getAlpha2Code() + checkDigits + businessCode + normalizedNationalId; + this.normalizedCi = country.getAlpha2Code() + checkDigits + businessCode + normalizedNationalId; } - /** - * Create a new creditor identifier from the given string. - * - * @param creditorId a non-null String. - */ - public CreditorIdentifier(String creditorId) { - if (creditorId == null) { - throw new IllegalArgumentException("the creditor identifier argument cannot be null"); - } - - String normalizedCreditorId = normalize(creditorId); + private static String normalize(String creditorIdentifier) { + return creditorIdentifier.replaceAll("\\s+", "").toUpperCase(); + } - if (isNotWellFormatted(normalizedCreditorId)) { - throw CreditorIdentifierFormatException.forNotProperlyFormattedInput(normalizedCreditorId); + private static boolean isWellFormatted(String s) { + int length = s.length(); + if (length < CREDITOR_IDENTIFIER_MIN_LENGTH) { + return false; } - Optional country = findCountryFor(normalizedCreditorId); - if (!country.isPresent()) { - throw CreditorIdentifierFormatException.forUnknownCountry(creditorId); + for (int i = COUNTRY_CODE_INDEX; i < COUNTRY_CODE_LENGTH; i++) { + if (!AsciiCharacters.isAlphabetic(s.charAt(i))) { + return false; + } } - String normalizedCreditorIdWithoutBusinessCode = removeBusinessCode(normalizedCreditorId); - if (!IbanCheckDigit.INSTANCE.validate(normalizedCreditorIdWithoutBusinessCode)) { - throw CreditorIdentifierFormatException.forIncorrectCheckDigits(creditorId); + for (int i = CHECK_DIGITS_INDEX; i < CHECK_DIGITS_INDEX + CHECK_DIGITS_LENGTH; i++) { + if (!AsciiCharacters.isNumeric(s.charAt(i))) { + return false; + } } - this.creditorId = normalizedCreditorId; - } - - /** - * Returns a normalized string representation of the given Creditor Identifier. - * - *

- * Normalized means the string is: - * - *

- */ - private static String normalize(String creditorIdentifier) { - return creditorIdentifier.replaceAll("\\s+", "").toUpperCase(); - } + for (int i = CREDITOR_BUSINESS_CODE_INDEX; i < length; i++) { + if (!AsciiCharacters.isAlphanumeric(s.charAt(i))) { + return false; + } + } - /** - * Check if the given string matches the basic format of a Creditor Identifier. - * - *

- * Returns {@code true} if the given strings matches the following pattern: - * - *

- */ - private static boolean isNotWellFormatted(String creditorIdentifier) { - return !BASIC_PATTERN.matcher(creditorIdentifier).matches(); + return true; } - /** - * Returns the {@code Country} reference from the given Creditor Identifier string. - * - *

- * Returns null if not found. - */ private static Optional findCountryFor(String creditorIdentifier) { return IsoCountry.fromAlpha2Code( creditorIdentifier.substring(COUNTRY_CODE_INDEX, COUNTRY_CODE_INDEX + COUNTRY_CODE_LENGTH)); } - /** - * Removes the business code part from the given Creditor Identifier string. - */ private static String removeBusinessCode(String creditorIdentifier) { return creditorIdentifier.substring(COUNTRY_CODE_INDEX, CREDITOR_BUSINESS_CODE_INDEX) + creditorIdentifier.substring(CREDITOR_NATIONAL_ID_INDEX); @@ -199,7 +197,7 @@ public static boolean isValid(String creditorIdentifier) { String normalizedCreditorId = normalize(creditorIdentifier); - if (isNotWellFormatted(normalizedCreditorId)) { + if (!isWellFormatted(normalizedCreditorId)) { return false; } @@ -228,7 +226,7 @@ public String getCountryCode() { * @return A non-null {@link IsoCountry}. */ public IsoCountry getCountry() { - return findCountryFor(creditorId).orElseThrow(() -> new IllegalStateException("a valid CI should have a country code")); + return findCountryFor(normalizedCi).orElseThrow(() -> new IllegalStateException("a valid CI should have a country code")); } /** @@ -237,7 +235,7 @@ public IsoCountry getCountry() { * @return A non-null string representing this Creditor Identifier check digit. */ public String getCheckDigit() { - return creditorId.substring(CHECK_DIGITS_INDEX, CHECK_DIGITS_INDEX + CHECK_DIGITS_LENGTH); + return normalizedCi.substring(CHECK_DIGITS_INDEX, CHECK_DIGITS_INDEX + CHECK_DIGITS_LENGTH); } /** @@ -246,7 +244,7 @@ public String getCheckDigit() { * @return A non-null string representing this Creditor Identifier business code. */ public String getBusinessCode() { - return creditorId.substring(CREDITOR_BUSINESS_CODE_INDEX, + return normalizedCi.substring(CREDITOR_BUSINESS_CODE_INDEX, CREDITOR_BUSINESS_CODE_INDEX + CREDITOR_BUSINESS_CODE_LENGTH); } @@ -256,12 +254,12 @@ public String getBusinessCode() { * @return A non-null string representing this Creditor Identifier National ID. */ public String getNationalIdentifier() { - return creditorId.substring(CREDITOR_NATIONAL_ID_INDEX); + return normalizedCi.substring(CREDITOR_NATIONAL_ID_INDEX); } @Override public String toString() { - return creditorId; + return normalizedCi; } @Override @@ -276,11 +274,11 @@ public boolean equals(Object o) { CreditorIdentifier other = (CreditorIdentifier) o; - return creditorId.equals(other.creditorId); + return normalizedCi.equals(other.normalizedCi); } @Override public int hashCode() { - return creditorId.hashCode(); + return normalizedCi.hashCode(); } }