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

net.codecrete.qrbill.generator.Payments Maven / Gradle / Ivy

There is a newer version: 3.3.1
Show newest version
//
// Swiss QR Bill Generator
// Copyright (c) 2018 Manuel Bleichenbacher
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//

package net.codecrete.qrbill.generator;

import java.text.Normalizer;

/**
 * Field validations related to Swiss Payment standards
 */
public class Payments {

    private Payments() {
        // Do not create instances
    }

    /**
     * Result of cleaning a string value
     */
    static class CleaningResult {
        /**
         * Cleaned string
         */
        String cleanedString;
        /**
         * Flag indicating that unsupported characters have been replaced
         */
        boolean replacedUnsupportedChars;
    }

    /**
     * Cleans a string value to make it viable for the Swiss Payment Standards 2018.
     * 

* Unsupported characters (according to Swiss Payment Standards 2018, ch. 2.4.1 * and appendix D) are replaced with spaces (unsupported whitespace) or dots * (all other unsupported characters). Leading and trailing whitespace is * removed. *

*

* If characters beyond 0xff are detected, the string is first normalized such * that letters with umlauts or accents expressed with two code points are * merged into a single code point (if possible), some of which might become * valid. *

*

* If the resulting strings is all white space, {@code null} is returned. *

* * @param value string value to clean * @param result result to be filled with cleaned string and flag */ static void cleanValue(String value, CleaningResult result) { result.cleanedString = null; result.replacedUnsupportedChars = false; cleanValue(value, result, false); if (result.cleanedString != null && result.cleanedString.length() == 0) result.cleanedString = null; } private static void cleanValue(String value, CleaningResult result, boolean isNormalized) { /* This code has cognitive complexity 30. Deal with it. */ if (value == null) return; int len = value.length(); // length of value boolean justProcessedSpace = false; // flag indicating whether we've just processed a space character StringBuilder sb = null; // String builder for result int lastCopiedPos = 0; // last position (excluding) copied to the result // String processing pattern: Iterate all characters and focus on runs of valid // characters that can simply be copied. If all characters are valid, no memory // is allocated. int pos = 0; while (pos < len) { char ch = value.charAt(pos); // current character if (Payments.isValidQRBillCharacter(ch)) { justProcessedSpace = ch == ' '; pos++; continue; } // Check for normalization if (ch > 0xff && !isNormalized) { isNormalized = Normalizer.isNormalized(value, Normalizer.Form.NFC); if (!isNormalized) { // Normalize string and start over value = Normalizer.normalize(value, Normalizer.Form.NFC); cleanValue(value, result, true); return; } } if (sb == null) sb = new StringBuilder(value.length()); // copy processed characters to result before taking care of the invalid // character if (pos > lastCopiedPos) sb.append(value, lastCopiedPos, pos); if (Character.isHighSurrogate(ch)) { // Proper Unicode handling to prevent surrogates and combining characters // from being replaced with multiples periods. int codePoint = value.codePointAt(pos); if (Character.getType(codePoint) != Character.COMBINING_SPACING_MARK) sb.append('.'); justProcessedSpace = false; pos++; } else { if (ch <= ' ') { if (!justProcessedSpace) sb.append(' '); justProcessedSpace = true; } else { sb.append('.'); justProcessedSpace = false; } } pos++; lastCopiedPos = pos; } if (sb == null) { result.cleanedString = value.trim(); return; } if (lastCopiedPos < len) sb.append(value, lastCopiedPos, len); result.cleanedString = sb.toString().trim(); result.replacedUnsupportedChars = true; } /** * Validates if the string is a valid IBAN number *

* The string is checked for valid characters, valid length and for a valid * check digit. White space is ignored. *

* * @param iban IBAN to validate * @return {@code true} if the IBAN is valid, {@code false} otherwise */ public static boolean isValidIBAN(String iban) { iban = Strings.whiteSpaceRemoved(iban); int len = iban.length(); if (len < 5) return false; if (!isAlphaNumeric(iban)) return false; // Check for country code if (!Character.isLetter(iban.charAt(0)) || !Character.isLetter(iban.charAt(1))) return false; // Check for check digits if (!Character.isDigit(iban.charAt(2)) || !Character.isDigit(iban.charAt(3))) return false; String checkDigits = iban.substring(2, 4); if ("00".equals(checkDigits) || "01".equals(checkDigits) || "99".equals(checkDigits)) return false; return hasValidMod97CheckDigits(iban); } /** * Formats an IBAN or creditor reference by inserting spaces. *

* Spaces are inserted to form groups of 4 letters/digits. If a group of less * than 4 letters/digits is needed, it appears at the end. *

* * @param iban IBAN or creditor reference without spaces * @return formatted IBAN or creditor reference */ public static String formatIBAN(String iban) { StringBuilder sb = new StringBuilder(25); int len = iban.length(); for (int pos = 0; pos < len; pos += 4) { int endPos = pos + 4; if (endPos > len) endPos = len; sb.append(iban, pos, endPos); if (endPos != len) sb.append(' '); } return sb.toString(); } /** * Validates if the string is a valid ISO 11649 reference number. *

* The string is checked for valid characters, valid length and a valid check * digit. White space is ignored. *

* * @param reference ISO 11649 creditor reference to validate * @return {@code true} if the creditor reference is valid, {@code false} * otherwise */ public static boolean isValidISO11649Reference(String reference) { reference = Strings.whiteSpaceRemoved(reference); if (reference.length() < 5 || reference.length() > 25) return false; if (!isAlphaNumeric(reference)) return false; if (reference.charAt(0) != 'R' || reference.charAt(1) != 'F') return false; if (!Character.isDigit(reference.charAt(2)) || !Character.isDigit(reference.charAt(3))) return false; return hasValidMod97CheckDigits(reference); } /** * Creates a ISO11649 creditor reference from a raw string by prefixing the * string with "RF" and the modulo 97 checksum. *

* Whitespace is removed from the reference *

* * @param rawReference The raw string * @return ISO11649 creditor reference * @throws IllegalArgumentException if {@code rawReference} contains invalid * characters */ public static String createISO11649Reference(String rawReference) { final String whiteSpaceRemoved = Strings.whiteSpaceRemoved(rawReference); final int modulo = Payments.calculateMod97("RF00" + whiteSpaceRemoved); return String.format("RF%02d", 98 - modulo) + whiteSpaceRemoved; } private static boolean hasValidMod97CheckDigits(String number) { return calculateMod97(number) == 1; } /** * Calculate the reference's modulo 97 checksum according to ISO11649 and IBAN * standard. *

* The string may only contains digits, letters ('A' to 'Z' and 'a' to 'z', no * accents). It must not contain white space. *

* * @param reference the reference * @return the checksum (0 to 96) * @throws IllegalArgumentException thrown if the reference contains an invalid * character */ private static int calculateMod97(String reference) { int len = reference.length(); if (len < 5) throw new IllegalArgumentException("Insufficient characters for checksum calculation"); String rearranged = reference.substring(4) + reference.substring(0, 4); int sum = 0; for (int i = 0; i < len; i++) { char ch = rearranged.charAt(i); if (ch >= '0' && ch <= '9') { sum = sum * 10 + (ch - '0'); } else if (ch >= 'A' && ch <= 'Z') { sum = sum * 100 + (ch - 'A' + 10); } else if (ch >= 'a' && ch <= 'z') { sum = sum * 100 + (ch - 'a' + 10); } else { throw new IllegalArgumentException("Invalid character in reference: " + ch); } if (sum > 9999999) sum = sum % 97; } sum = sum % 97; return sum; } private static final int[] MOD_10 = { 0, 9, 4, 6, 8, 2, 7, 1, 3, 5 }; /** * Validates if the string is a valid QR reference. *

* A valid QR reference is a valid ISR reference. *

*

* The string is checked for valid characters, valid length and a valid check * digit. White space is ignored. *

* * @param reference QR reference number to validate * @return {@code true} if the reference number is valid, {@code false} * otherwise */ public static boolean isValidQRReference(String reference) { reference = Strings.whiteSpaceRemoved(reference); if (!isNumeric(reference)) return false; int len = reference.length(); if (len != 27) return false; return calculateMod10(reference) == 0; } /** * Creates a QR reference from a raw string by appending the checksum digit * and prepending zeros to make it the correct length. *

* Whitespace is removed from the reference *

* * @param rawReference The raw string (digits and whitespace only) * @return QR reference * @throws IllegalArgumentException if {@code rawReference} contains invalid * characters */ public static String createQRReference(String rawReference) { final String rawRef = Strings.whiteSpaceRemoved(rawReference); if (!isNumeric(rawRef)) throw new IllegalArgumentException("Invalid character in reference (digits allowed only)"); if (rawRef.length() > 26) throw new IllegalArgumentException("Reference number is too long"); int mod10 = calculateMod10(rawRef); return "00000000000000000000000000".substring(0, 26 - rawRef.length()) + rawRef + (char)('0' + mod10); } private static int calculateMod10(String reference) { int len = reference.length(); int carry = 0; for (int i = 0; i < len; i++) { int digit = reference.charAt(i) - '0'; carry = MOD_10[(carry + digit) % 10]; } return (10 - carry) % 10; } /** * Formats a QR reference number by inserting spaces. *

* Spaces are inserted to create groups of 5 digits. If a group of less than 5 * digits is needed, it appears at the start of the formatted reference number. *

* * @param refNo reference number without white space * @return formatted reference number */ public static String formatQRReferenceNumber(String refNo) { int len = refNo.length(); StringBuilder sb = new StringBuilder(); int t = 0; while (t < len) { int n = t + (len - t - 1) % 5 + 1; if (t != 0) sb.append(" "); sb.append(refNo, t, n); t = n; } return sb.toString(); } private static boolean isNumeric(String value) { int len = value.length(); for (int i = 0; i < len; i++) { char ch = value.charAt(i); if (ch < '0' || ch > '9') return false; } return true; } static boolean isAlphaNumeric(String value) { int len = value.length(); for (int i = 0; i < len; i++) { char ch = value.charAt(i); if (ch >= '0' && ch <= '9') continue; if (ch >= 'A' && ch <= 'Z') continue; if (ch >= 'a' && ch <= 'z') continue; return false; } return true; } static boolean isAlpha(String value) { int len = value.length(); for (int i = 0; i < len; i++) { char ch = value.charAt(i); if (ch >= 'A' && ch <= 'Z') continue; if (ch >= 'a' && ch <= 'z') continue; return false; } return true; } private static boolean isValidQRBillCharacter(char ch) { if (ch < 0x20) return false; if (ch == 0x5e) return false; if (ch <= 0x7e) return true; if (ch == 0xa3 || ch == 0xb4) return true; if (ch < 0xc0 || ch > 0xfd) return false; if (ch == 0xc3 || ch == 0xc5 || ch == 0xc6) return false; if (ch == 0xd0 || ch == 0xd5 || ch == 0xd7 || ch == 0xd8) return false; if (ch == 0xdd || ch == 0xde) return false; if (ch == 0xe3 || ch == 0xe5 || ch == 0xe6) return false; if (ch == 0xf0 || ch == 0xf5 || ch == 0xf8) return false; return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy