IBAN.java

/*
 * Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
 */
package de.powerstat.validation.values;


import java.math.BigInteger;
import java.util.Objects;
import java.util.regex.Pattern;

import de.powerstat.validation.interfaces.IValueObject;
import de.powerstat.validation.values.impl.IBANVerifierAbstractFactory;


/**
 * IBAN.
 *
 * Probably DSGVO relevant.
 *
 * TODO https://openiban.com/
 * TODO Human format in/out
 */
public final class IBAN implements Comparable<IBAN>, IValueObject
 {
  /* *
   * Cache for singletons.
   */
  // private static final Map<String, IBAN> CACHE = new WeakHashMap<>();

  /**
   * IBAN regexp.
   */
  @SuppressWarnings("java:S5867")
  private static final Pattern IBAN_REGEXP = Pattern.compile("^[A-Z]{2}\\d{2}[0-9A-Z]{11,30}$"); //$NON-NLS-1$

  /**
   * IBAN.
   */
  private final String iban;


  /**
   * Constructor.
   *
   * @param iban IBAN
   * @throws NullPointerException if iban is null
   * @throws IllegalArgumentException if iban is not an correct iban
   */
  private IBAN(final String iban)
   {
    super();
    Objects.requireNonNull(iban, "iban"); //$NON-NLS-1$
    if ((iban.length() < 15) || (iban.length() > 34))
     {
      throw new IllegalArgumentException("IBAN with wrong length"); //$NON-NLS-1$
     }
    if (!IBAN.IBAN_REGEXP.matcher(iban).matches())
     {
      throw new IllegalArgumentException("IBAN with wrong format"); //$NON-NLS-1$
     }
    final var checksum = iban.substring(2, 4);
    if ("00".equals(checksum) || "01".equals(checksum) || "99".equals(checksum)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
     {
      throw new IllegalArgumentException("IBAN with illegal checksum"); //$NON-NLS-1$
     }
    if (!verifyChecksum(iban))
     {
      throw new IllegalArgumentException("IBAN with wrong checksum"); //$NON-NLS-1$
     }
    final var country = Country.of(iban.substring(0, 2));
    if (!IBANVerifierAbstractFactory.createIBANVerifier(country).verify(iban))
     {
      throw new IllegalArgumentException("IBAN not correct in country context: " + iban); //$NON-NLS-1$
     }
    this.iban = iban;
   }


  /**
   * Calculate ISO 7064 mod 97-10 checksum.
   *
   * @param iban IBAN
   * @return true when checksum is correct, false otherwise
   */
  private static boolean verifyChecksum(final String iban)
   {
    final String reordered = iban.substring(4) + iban.substring(0, 2) + iban.substring(2, 4);
    final String replacement = reordered.replace("A", "10").replace("B", "11").replace("C", "12").replace("D", "13").replace("E", "14").replace("F", "15").replace("G", "16").replace("H", "17").replace("I", "18").replace("J", "19").replace("K", "20").replace("L", "21").replace("M", "22").replace("N", "23").replace("O", "24").replace("P", "25").replace("Q", "26").replace("R", "27").replace("S", "28").replace("T", "29").replace("U", "30").replace("V", "31").replace("W", "32").replace("X", "33").replace("Y", "34").replace("Z", "35"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$ //$NON-NLS-12$ //$NON-NLS-13$ //$NON-NLS-14$ //$NON-NLS-15$ //$NON-NLS-16$ //$NON-NLS-17$ //$NON-NLS-18$ //$NON-NLS-19$ //$NON-NLS-20$ //$NON-NLS-21$ //$NON-NLS-22$ //$NON-NLS-23$ //$NON-NLS-24$ //$NON-NLS-25$ //$NON-NLS-26$ //$NON-NLS-27$ //$NON-NLS-28$ //$NON-NLS-29$ //$NON-NLS-30$ //$NON-NLS-31$ //$NON-NLS-32$ //$NON-NLS-33$ //$NON-NLS-34$ //$NON-NLS-35$ //$NON-NLS-36$ //$NON-NLS-37$ //$NON-NLS-38$ //$NON-NLS-39$ //$NON-NLS-40$ //$NON-NLS-41$ //$NON-NLS-42$ //$NON-NLS-43$ //$NON-NLS-44$ //$NON-NLS-45$ //$NON-NLS-46$ //$NON-NLS-47$ //$NON-NLS-48$ //$NON-NLS-49$ //$NON-NLS-50$ //$NON-NLS-51$ //$NON-NLS-52$
    final var num = new BigInteger(replacement);
    final BigInteger result = num.remainder(BigInteger.valueOf(97));
    return result.longValue() == 1;
   }


  /**
   * IBAN factory.
   *
   * @param iban IBAN
   * @return IBAN object
   */
  public static IBAN of(final String iban)
   {
    /*
    synchronized (IBAN.class)
     {
      IBAN obj = IBAN.CACHE.get(iban);
      if (obj != null)
       {
        return obj;
       }
      obj = new IBAN(iban);
      IBAN.CACHE.put(iban, obj);
      return obj;
     }
    */
    return new IBAN(iban);
   }


  /**
   * Returns the value of this IBAN as a string.
   *
   * @return The text value represented by this object after conversion to type string.
   */
  @Override
  public String stringValue()
   {
    return this.iban;
   }


  /**
   * Calculate hash code.
   *
   * @return Hash
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
   {
    return this.iban.hashCode();
   }


  /**
   * Is equal with another object.
   *
   * @param obj Object
   * @return true when equal, false otherwise
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(final Object obj)
   {
    if (this == obj)
     {
      return true;
     }
    if (!(obj instanceof IBAN))
     {
      return false;
     }
    final IBAN other = (IBAN)obj;
    return this.iban.equals(other.iban);
   }


  /**
   * Returns the string representation of this IBAN.
   *
   * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
   *
   * "IBAN[iban=DE68210501700012345678]"
   *
   * @return String representation of this IBAN
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
   {
    final var builder = new StringBuilder();
    builder.append("IBAN[iban=").append(this.iban).append(']'); //$NON-NLS-1$
    return builder.toString();
   }


  /**
   * Compare with another object.
   *
   * @param obj Object to compare with
   * @return 0: equal; 1: greater; -1: smaller
   * @see java.lang.Comparable#compareTo(java.lang.Object)
   */
  @Override
  public int compareTo(final IBAN obj)
   {
    Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
    return this.iban.compareTo(obj.iban);
   }

 }