IPV6Address.java

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


import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;

import de.powerstat.validation.interfaces.IValueObject;


/**
 * IP V6 address.
 *
 * DSGVO relevant.
 *
 * TODO ping ok?
 */
public final class IPV6Address implements Comparable<IPV6Address>, IValueObject
 {
  /* *
   * Logger.
   */
  // private static final Logger LOGGER = LogManager.getLogger(IPV6Address.class);

  /* *
   * Cache for singletons.
   */
  // private static final Map<String, IPV6Address> CACHE = new WeakHashMap<>();

  /**
   * IP V6 regexp.
   */
  private static final Pattern IPV6_REGEXP = Pattern.compile("^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"); //$NON-NLS-1$

  /**
   * IPV6 zero block.
   */
  private static final String BLOCK_ZERO = "0000"; //$NON-NLS-1$

  /**
   * Hex output format.
   */
  private static final String HEX_OUTPUT = "%02x"; //$NON-NLS-1$

  /**
   * IPV6 block expansion.
   */
  private static final String IPV6_EXP = "::"; //$NON-NLS-1$

  /**
   * IPV6 block separator.
   */
  private static final String IV6_SEP = ":"; //$NON-NLS-1$

  /**
   * IP V6 address.
   */
  private final String address;

  /**
   * IP V6 address parts.
   */
  private final String[] blocks;


  /**
   * Constructor.
   *
   * @param address IP V6 address
   * @throws NullPointerException if address is null
   * @throws IllegalArgumentException if address is not an ip v6 address
   */
  private IPV6Address(final String address)
   {
    super();
    Objects.requireNonNull(address, "address"); //$NON-NLS-1$
    if ((address.length() < 2) || (address.length() > 45)) // 39, ipv4 embedding
     {
      throw new IllegalArgumentException("To short or long for an IP V6 address"); //$NON-NLS-1$
     }
    String expandedAddress = address.toLowerCase(Locale.getDefault());
    expandedAddress = expandIPV4Address(expandedAddress);
    expandedAddress = expandExpansionBlock(expandedAddress);
    if (!IPV6Address.IPV6_REGEXP.matcher(expandedAddress).matches())
     {
      throw new IllegalArgumentException("Not an IP V6 address"); //$NON-NLS-1$
     }
    expandedAddress = normalizeIPV6Address(expandedAddress);
    this.address = expandedAddress;
    this.blocks = expandedAddress.split(IPV6Address.IV6_SEP);
   }


  /**
   * Expand a possibly embedded IP V4 address.
   *
   * @param address IP V6 address
   * @return IP V6 address
   * @throws NullPointerException if address is null
   * @throws IllegalArgumentException if address is not an ip v4 address
   */
  private static String expandIPV4Address(final String address)
   {
    final int ipv4pos = address.indexOf('.');
    if (ipv4pos == -1)
     {
      return address;
     }
    final int blockStart = address.lastIndexOf(':', ipv4pos);
    final var ipv4 = address.substring(blockStart + 1);
    /* final IPV4Address ipv4address = */ IPV4Address.of(ipv4); // TODO use IPV4Address to ip v6 conversion method
    final var newAddress = address.substring(0, blockStart + 1);
    final String[] parts = ipv4.split("\\."); //$NON-NLS-1$
    final int block1 = Integer.parseInt(parts[0]);
    final int block2 = Integer.parseInt(parts[1]);
    final int block3 = Integer.parseInt(parts[2]);
    final int block4 = Integer.parseInt(parts[3]);
    return newAddress + Integer.toHexString(block1) + String.format(IPV6Address.HEX_OUTPUT, block2) + ':' + Integer.toHexString(block3) + String.format(IPV6Address.HEX_OUTPUT, block4);
   }


  /**
   * Count colons.
   *
   * @param str String to count coolons in
   * @return Numbe rof colons found
   */
  private static int countColons(final String str)
   {
    int colons = 0;
    int expPos = -1;
    do
     {
      expPos = str.indexOf(':', expPos + 1);
      if (expPos > -1)
       {
        ++colons;
       }
     }
    while (expPos > -1);
    return colons;
   }


  /**
   * Expand possible expansion block.
   *
   * @param address IP V6 address
   * @return IP V6 address
   */
  private static String expandExpansionBlock(final String address)
   {
    final int expPos = address.indexOf(IPV6Address.IPV6_EXP);
    if ((expPos == -1))
     {
      return address;
     }
    if (address.indexOf(IPV6Address.IPV6_EXP, expPos + 1) != -1)
     {
      throw new IllegalArgumentException("Not an IP V6 address (more than one expansion block)"); //$NON-NLS-1$
     }
    final var start = address.substring(0, expPos);
    final var end = address.substring(expPos + 2);
    int blocks = 8;
    if (start.length() > 0)
     {
      blocks -= countColons(start) + 1;
     }
    if (end.length() > 0)
     {
      blocks -= countColons(end) + 1;
     }
    final var replace = new StringBuilder();
    if (start.length() > 0)
     {
      replace.append(':');
     }
    while (blocks > 0)
     {
      replace.append(IPV6Address.BLOCK_ZERO);
      --blocks;
      if (blocks > 0)
       {
        replace.append(':');
       }
     }
    if (end.length() > 0)
     {
      replace.append(':');
     }
    replace.append(end);
    replace.insert(0, start);
    return replace.toString();
   }


  /**
   * Normalize IP V6 address.
   *
   * @param address IP V6 address
   * @return Normalized IP V6 address
   */
  private static String normalizeIPV6Address(final String address)
   {
    final String[] parts = address.split(IPV6Address.IV6_SEP);
    final var normalizedAddress = new StringBuilder();
    for (final String part : parts)
     {
      normalizedAddress.append(IPV6Address.BLOCK_ZERO.substring(part.length())).append(part).append(':');
     }
    normalizedAddress.setLength(normalizedAddress.length() - 1);
    return normalizedAddress.toString();
   }


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


  /**
   * Is an IP V6 private address.
   *
   * fc Unique Local Unicast
   * fd Unique Local Unicast
   * fe:80:00:00:00:00:00:00 Link-Local
   *
   * @return true if private, false otherwise
   */
  @SuppressWarnings("java:S1313")
  public boolean isPrivate()
   {
    return ("00fe:0080:0000:0000:0000:0000:0000:0000".equals(this.address) || // Link-Local //$NON-NLS-1$
            "00fc".equals(this.blocks[0]) || "00fd".equals(this.blocks[0]) // Unique Local Unicast //$NON-NLS-1$ //$NON-NLS-2$
           );
   }


  /**
   * Is an IP V6 special address.
   *
   * 0:0:0:0:0:0:0:0 default route
   * 0:0:0:0:0:0:0:1 loopback
   * ff Multicast
   *
   * @return true if special, false otherwise
   */
  public boolean isSpecial()
   {
    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$
            "00ff".equals(this.blocks[0]) // Multicast //$NON-NLS-1$
           );
   }


  /**
   * Is an IP V6 public address.
   *
   * 0:0:0:0:0:ffff::/96 IPv4 mapped (abgebildete) IPv6 Adressen
   * 2000::/3 IANA vergebenen globalen Unicast
   * 2001 Provider area
   * 2001:0: Toredo
   * 2001:0db8::/32 Documentation purposes
   * 2002 6to4 tunnel
   * 2003, 0240, 0260, 0261, 0262, 0280, 02a0, 02b0 und 02c0 Regional Internet Registries (RIRs)
   * 0064:ff9b::/96 NAT64
   *
   * @return true when public address, otherwise false
   */
  public boolean isPublic()
   {
    return !isPrivate() && !isSpecial();
   }


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


  /**
   * Calculate hash code.
   *
   * @return Hash
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
   {
    return this.address.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 IPV6Address))
     {
      return false;
     }
    final IPV6Address other = (IPV6Address)obj;
    return this.address.equals(other.address);
   }


  /**
   * Returns the string representation of this IPV6Address.
   *
   * The exact details of this representation are unspecified and subject to change, but the following may be regarded as typical:
   *
   * "IPV6Address[address=ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]"
   *
   * @return String representation of this IPV6Address
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
   {
    final var builder = new StringBuilder(21);
    builder.append("IPV6Address[address=").append(this.address).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 IPV6Address obj)
   {
    Objects.requireNonNull(obj, "obj"); //$NON-NLS-1$
    return this.address.compareTo(obj.address);
   }

 }