View Javadoc
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.util.Locale;
8   import java.util.Objects;
9   import java.util.regex.Pattern;
10  
11  import de.powerstat.validation.interfaces.IValueObject;
12  
13  
14  /**
15   * IP V6 address.
16   *
17   * DSGVO relevant.
18   *
19   * TODO ping ok?
20   */
21  public final class IPV6Address implements Comparable<IPV6Address>, IValueObject
22   {
23    /* *
24     * Logger.
25     */
26    // private static final Logger LOGGER = LogManager.getLogger(IPV6Address.class);
27  
28    /* *
29     * Cache for singletons.
30     */
31    // private static final Map<String, IPV6Address> CACHE = new WeakHashMap<>();
32  
33    /**
34     * IP V6 regexp.
35     */
36    private static final Pattern IPV6_REGEXP = Pattern.compile("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"); //$NON-NLS-1$
37  
38    /**
39     * IPV6 zero block.
40     */
41    private static final String BLOCK_ZERO = "0000"; //$NON-NLS-1$
42  
43    /**
44     * Hex output format.
45     */
46    private static final String HEX_OUTPUT = "%02x"; //$NON-NLS-1$
47  
48    /**
49     * IPV6 block expansion.
50     */
51    private static final String IPV6_EXP = "::"; //$NON-NLS-1$
52  
53    /**
54     * IPV6 block separator.
55     */
56    private static final String IV6_SEP = ":"; //$NON-NLS-1$
57  
58    /**
59     * IP V6 address.
60     */
61    private final String address;
62  
63    /**
64     * IP V6 address parts.
65     */
66    private final String[] blocks;
67  
68  
69    /**
70     * Constructor.
71     *
72     * @param address IP V6 address
73     * @throws NullPointerException if address is null
74     * @throws IllegalArgumentException if address is not an ip v6 address
75     */
76    private IPV6Address(final String address)
77     {
78      super();
79      Objects.requireNonNull(address, "address"); //$NON-NLS-1$
80      if ((address.length() < 2) || (address.length() > 45)) // 39, ipv4 embedding
81       {
82        throw new IllegalArgumentException("To short or long for an IP V6 address"); //$NON-NLS-1$
83       }
84      String expandedAddress = address.toLowerCase(Locale.getDefault());
85      expandedAddress = expandIPV4Address(expandedAddress);
86      expandedAddress = expandExpansionBlock(expandedAddress);
87      if (!IPV6Address.IPV6_REGEXP.matcher(expandedAddress).matches())
88       {
89        throw new IllegalArgumentException("Not an IP V6 address"); //$NON-NLS-1$
90       }
91      expandedAddress = normalizeIPV6Address(expandedAddress);
92      this.address = expandedAddress;
93      this.blocks = expandedAddress.split(IPV6Address.IV6_SEP);
94     }
95  
96  
97    /**
98     * Expand a possibly embedded IP V4 address.
99     *
100    * @param address IP V6 address
101    * @return IP V6 address
102    * @throws NullPointerException if address is null
103    * @throws IllegalArgumentException if address is not an ip v4 address
104    */
105   private static String expandIPV4Address(final String address)
106    {
107     final int ipv4pos = address.indexOf('.');
108     if (ipv4pos == -1)
109      {
110       return address;
111      }
112     final int blockStart = address.lastIndexOf(':', ipv4pos);
113     final var ipv4 = address.substring(blockStart + 1);
114     /* final IPV4Address ipv4address = */ IPV4Address.of(ipv4); // TODO use IPV4Address to ip v6 conversion method
115     final var newAddress = address.substring(0, blockStart + 1);
116     final String[] parts = ipv4.split("\\."); //$NON-NLS-1$
117     final int block1 = Integer.parseInt(parts[0]);
118     final int block2 = Integer.parseInt(parts[1]);
119     final int block3 = Integer.parseInt(parts[2]);
120     final int block4 = Integer.parseInt(parts[3]);
121     return newAddress + Integer.toHexString(block1) + String.format(IPV6Address.HEX_OUTPUT, block2) + ':' + Integer.toHexString(block3) + String.format(IPV6Address.HEX_OUTPUT, block4);
122    }
123 
124 
125   /**
126    * Count colons.
127    *
128    * @param str String to count coolons in
129    * @return Numbe rof colons found
130    */
131   private static int countColons(final String str)
132    {
133     int colons = 0;
134     int expPos = -1;
135     do
136      {
137       expPos = str.indexOf(':', expPos + 1);
138       if (expPos > -1)
139        {
140         ++colons;
141        }
142      }
143     while (expPos > -1);
144     return colons;
145    }
146 
147 
148   /**
149    * Expand possible expansion block.
150    *
151    * @param address IP V6 address
152    * @return IP V6 address
153    */
154   private static String expandExpansionBlock(final String address)
155    {
156     final int expPos = address.indexOf(IPV6Address.IPV6_EXP);
157     if ((expPos == -1))
158      {
159       return address;
160      }
161     if (address.indexOf(IPV6Address.IPV6_EXP, expPos + 1) != -1)
162      {
163       throw new IllegalArgumentException("Not an IP V6 address (more than one expansion block)"); //$NON-NLS-1$
164      }
165     final var start = address.substring(0, expPos);
166     final var end = address.substring(expPos + 2);
167     int blocks = 8;
168     if (start.length() > 0)
169      {
170       blocks -= countColons(start) + 1;
171      }
172     if (end.length() > 0)
173      {
174       blocks -= countColons(end) + 1;
175      }
176     final var replace = new StringBuilder();
177     if (start.length() > 0)
178      {
179       replace.append(':');
180      }
181     while (blocks > 0)
182      {
183       replace.append(IPV6Address.BLOCK_ZERO);
184       --blocks;
185       if (blocks > 0)
186        {
187         replace.append(':');
188        }
189      }
190     if (end.length() > 0)
191      {
192       replace.append(':');
193      }
194     replace.append(end);
195     replace.insert(0, start);
196     return replace.toString();
197    }
198 
199 
200   /**
201    * Normalize IP V6 address.
202    *
203    * @param address IP V6 address
204    * @return Normalized IP V6 address
205    */
206   private static String normalizeIPV6Address(final String address)
207    {
208     final String[] parts = address.split(IPV6Address.IV6_SEP);
209     final var normalizedAddress = new StringBuilder();
210     for (final String part : parts)
211      {
212       normalizedAddress.append(IPV6Address.BLOCK_ZERO.substring(part.length())).append(part).append(':');
213      }
214     normalizedAddress.setLength(normalizedAddress.length() - 1);
215     return normalizedAddress.toString();
216    }
217 
218 
219   /**
220    * IPV6Address factory.
221    *
222    * @param address IP V6 address
223    * @return IPV6address object
224    */
225   public static IPV6Address of(final String address)
226    {
227     /*
228     synchronized (IPV6Address.class)
229      {
230       IPV6Address obj = IPV6Address.CACHE.get(address);
231       if (obj != null)
232        {
233         return obj;
234        }
235       obj = new IPV6Address(address);
236       IPV6Address.CACHE.put(address, obj);
237       return obj;
238      }
239     */
240     return new IPV6Address(address);
241    }
242 
243 
244   /**
245    * Is an IP V6 private address.
246    *
247    * fc Unique Local Unicast
248    * fd Unique Local Unicast
249    * fe:80:00:00:00:00:00:00 Link-Local
250    *
251    * @return true if private, false otherwise
252    */
253   @SuppressWarnings("java:S1313")
254   public boolean isPrivate()
255    {
256     return ("00fe:0080:0000:0000:0000:0000:0000:0000".equals(this.address) || // Link-Local //$NON-NLS-1$
257             "00fc".equals(this.blocks[0]) || "00fd".equals(this.blocks[0]) // Unique Local Unicast //$NON-NLS-1$ //$NON-NLS-2$
258            );
259    }
260 
261 
262   /**
263    * Is an IP V6 special address.
264    *
265    * 0:0:0:0:0:0:0:0 default route
266    * 0:0:0:0:0:0:0:1 loopback
267    * ff Multicast
268    *
269    * @return true if special, false otherwise
270    */
271   public boolean isSpecial()
272    {
273     return ("0000:0000:0000:0000:0000:0000:0000:0000".equals(this.address) || "0000:0000:0000:0000:0000:0000:0000:0001".equals(this.address) || // default route, loopback //$NON-NLS-1$ //$NON-NLS-2$
274             "00ff".equals(this.blocks[0]) // Multicast //$NON-NLS-1$
275            );
276    }
277 
278 
279   /**
280    * Is an IP V6 public address.
281    *
282    * 0:0:0:0:0:ffff::/96 IPv4 mapped (abgebildete) IPv6 Adressen
283    * 2000::/3 IANA vergebenen globalen Unicast
284    * 2001 Provider area
285    * 2001:0: Toredo
286    * 2001:0db8::/32 Documentation purposes
287    * 2002 6to4 tunnel
288    * 2003, 0240, 0260, 0261, 0262, 0280, 02a0, 02b0 und 02c0 Regional Internet Registries (RIRs)
289    * 0064:ff9b::/96 NAT64
290    *
291    * @return true when public address, otherwise false
292    */
293   public boolean isPublic()
294    {
295     return !isPrivate() && !isSpecial();
296    }
297 
298 
299   /**
300    * Returns the value of this IPV6Address as a string.
301    *
302    * @return The text value represented by this object after conversion to type string.
303    */
304   @Override
305   public String stringValue()
306    {
307     return this.address;
308    }
309 
310 
311   /**
312    * Calculate hash code.
313    *
314    * @return Hash
315    * @see java.lang.Object#hashCode()
316    */
317   @Override
318   public int hashCode()
319    {
320     return this.address.hashCode();
321    }
322 
323 
324   /**
325    * Is equal with another object.
326    *
327    * @param obj Object
328    * @return true when equal, false otherwise
329    * @see java.lang.Object#equals(java.lang.Object)
330    */
331   @Override
332   public boolean equals(final Object obj)
333    {
334     if (this == obj)
335      {
336       return true;
337      }
338     if (!(obj instanceof IPV6Address))
339      {
340       return false;
341      }
342     final IPV6Address other = (IPV6Address)obj;
343     return this.address.equals(other.address);
344    }
345 
346 
347   /**
348    * Returns the string representation of this IPV6Address.
349    *
350    * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
351    *
352    * "IPV6Address[address=ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]"
353    *
354    * @return String representation of this IPV6Address
355    * @see java.lang.Object#toString()
356    */
357   @Override
358   public String toString()
359    {
360     final var builder = new StringBuilder(21);
361     builder.append("IPV6Address[address=").append(this.address).append(']'); //$NON-NLS-1$
362     return builder.toString();
363    }
364 
365 
366   /**
367    * Compare with another object.
368    *
369    * @param obj Object to compare with
370    * @return 0: equal; 1: greater; -1: smaller
371    * @see java.lang.Comparable#compareTo(java.lang.Object)
372    */
373   @Override
374   public int compareTo(final IPV6Address obj)
375    {
376     Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
377     return this.address.compareTo(obj.address);
378    }
379 
380  }