1 /*
2 * Copyright (C) 2020-2023 Dipl.-Inform. Kai Hofmann. All rights reserved!
3 */
4 package de.powerstat.validation.values;
5
6
7 import java.math.BigInteger;
8 import java.util.Objects;
9 import java.util.regex.Pattern;
10
11 import de.powerstat.validation.interfaces.IValueObject;
12 import de.powerstat.validation.values.impl.IBANVerifierAbstractFactory;
13
14
15 /**
16 * IBAN.
17 *
18 * Probably DSGVO relevant.
19 *
20 * TODO https://openiban.com/
21 * TODO Human format in/out
22 */
23 public final class IBAN implements Comparable<IBAN>, IValueObject
24 {
25 /* *
26 * Cache for singletons.
27 */
28 // private static final Map<String, IBAN> CACHE = new WeakHashMap<>();
29
30 /**
31 * IBAN regexp.
32 */
33 @SuppressWarnings("java:S5867")
34 private static final Pattern IBAN_REGEXP = Pattern.compile("^[A-Z]{2}\\d{2}[0-9A-Z]{11,30}$"); //$NON-NLS-1$
35
36 /**
37 * IBAN.
38 */
39 private final String iban;
40
41
42 /**
43 * Constructor.
44 *
45 * @param iban IBAN
46 * @throws NullPointerException if iban is null
47 * @throws IllegalArgumentException if iban is not an correct iban
48 */
49 private IBAN(final String iban)
50 {
51 super();
52 Objects.requireNonNull(iban, "iban"); //$NON-NLS-1$
53 if ((iban.length() < 15) || (iban.length() > 34))
54 {
55 throw new IllegalArgumentException("IBAN with wrong length"); //$NON-NLS-1$
56 }
57 if (!IBAN.IBAN_REGEXP.matcher(iban).matches())
58 {
59 throw new IllegalArgumentException("IBAN with wrong format"); //$NON-NLS-1$
60 }
61 final var checksum = iban.substring(2, 4);
62 if ("00".equals(checksum) || "01".equals(checksum) || "99".equals(checksum)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
63 {
64 throw new IllegalArgumentException("IBAN with illegal checksum"); //$NON-NLS-1$
65 }
66 if (!verifyChecksum(iban))
67 {
68 throw new IllegalArgumentException("IBAN with wrong checksum"); //$NON-NLS-1$
69 }
70 final var country = Country.of(iban.substring(0, 2));
71 if (!IBANVerifierAbstractFactory.createIBANVerifier(country).verify(iban))
72 {
73 throw new IllegalArgumentException("IBAN not correct in country context: " + iban); //$NON-NLS-1$
74 }
75 this.iban = iban;
76 }
77
78
79 /**
80 * Calculate ISO 7064 mod 97-10 checksum.
81 *
82 * @param iban IBAN
83 * @return true when checksum is correct, false otherwise
84 */
85 private static boolean verifyChecksum(final String iban)
86 {
87 final String reordered = iban.substring(4) + iban.substring(0, 2) + iban.substring(2, 4);
88 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$
89 final var num = new BigInteger(replacement);
90 final BigInteger result = num.remainder(BigInteger.valueOf(97));
91 return result.longValue() == 1;
92 }
93
94
95 /**
96 * IBAN factory.
97 *
98 * @param iban IBAN
99 * @return IBAN object
100 */
101 public static IBAN of(final String iban)
102 {
103 /*
104 synchronized (IBAN.class)
105 {
106 IBAN obj = IBAN.CACHE.get(iban);
107 if (obj != null)
108 {
109 return obj;
110 }
111 obj = new IBAN(iban);
112 IBAN.CACHE.put(iban, obj);
113 return obj;
114 }
115 */
116 return new IBAN(iban);
117 }
118
119
120 /**
121 * Returns the value of this IBAN as a string.
122 *
123 * @return The text value represented by this object after conversion to type string.
124 */
125 @Override
126 public String stringValue()
127 {
128 return this.iban;
129 }
130
131
132 /**
133 * Calculate hash code.
134 *
135 * @return Hash
136 * @see java.lang.Object#hashCode()
137 */
138 @Override
139 public int hashCode()
140 {
141 return this.iban.hashCode();
142 }
143
144
145 /**
146 * Is equal with another object.
147 *
148 * @param obj Object
149 * @return true when equal, false otherwise
150 * @see java.lang.Object#equals(java.lang.Object)
151 */
152 @Override
153 public boolean equals(final Object obj)
154 {
155 if (this == obj)
156 {
157 return true;
158 }
159 if (!(obj instanceof IBAN))
160 {
161 return false;
162 }
163 final IBAN other = (IBAN)obj;
164 return this.iban.equals(other.iban);
165 }
166
167
168 /**
169 * Returns the string representation of this IBAN.
170 *
171 * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
172 *
173 * "IBAN[iban=DE68210501700012345678]"
174 *
175 * @return String representation of this IBAN
176 * @see java.lang.Object#toString()
177 */
178 @Override
179 public String toString()
180 {
181 final var builder = new StringBuilder();
182 builder.append("IBAN[iban=").append(this.iban).append(']'); //$NON-NLS-1$
183 return builder.toString();
184 }
185
186
187 /**
188 * Compare with another object.
189 *
190 * @param obj Object to compare with
191 * @return 0: equal; 1: greater; -1: smaller
192 * @see java.lang.Comparable#compareTo(java.lang.Object)
193 */
194 @Override
195 public int compareTo(final IBAN obj)
196 {
197 Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
198 return this.iban.compareTo(obj.iban);
199 }
200
201 }