IPV4Address.java

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


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

import de.powerstat.validation.interfaces.IValueObject;


/**
 * IP V4 address.
 *
 * DSGVO relevant.
 *
 * TODO convert to IP V6 format
 * TODO https://datahub.io/core/geoip2-ipv4/r/geoip2-ipv4.csv
 * TODO ping ok?
 */
public final class IPV4Address implements Comparable<IPV4Address>, IValueObject
 {
  /* *
   * Cache for singletons.
   */
  // private static final Map<String, IPV4Address> CACHE = new WeakHashMap<>();

  /**
   * Class c 192.
   */
  private static final String CLASS_C_192 = "192"; //$NON-NLS-1$

  /**
   * 100.
   */
  private static final String C100 = "100"; //$NON-NLS-1$

  /**
   * 198.
   */
  private static final String C198 = "198"; //$NON-NLS-1$

  /**
   * 0.
   */
  private static final String ZERO = "0"; //$NON-NLS-1$

  /**
   * IP V4 regexp.
   */
  private static final Pattern IPV4_REGEXP = Pattern.compile("^((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)$"); //$NON-NLS-1$

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

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


  /**
   * Constructor.
   *
   * @param address IP V4 address
   * @throws NullPointerException if address is null
   * @throws IllegalArgumentException if address is not an ip v4 address
   */
  private IPV4Address(final String address)
   {
    super();
    Objects.requireNonNull(address, "address"); //$NON-NLS-1$
    if ((address.length() < 7) || (address.length() > 15))
     {
      throw new IllegalArgumentException("To short or long for an IP V4 address"); //$NON-NLS-1$
     }
    if (!IPV4Address.IPV4_REGEXP.matcher(address).matches())
     {
      throw new IllegalArgumentException("Not an IP V4 address"); //$NON-NLS-1$
     }
    this.address = address;
    this.parts = address.split("\\."); //$NON-NLS-1$
   }


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


  /**
   * Is an IP V4 private address.
   *
   * 10.0.0.0–10.255.255.255     private,      1  8-Bit-Net  10.0.0.0/8     RFC 1918
   * 172.16.0.0–172.31.255.255   private,     16 16-Bit-Nets 172.16.0.0/12  RFC 1918
   * 192.168.0.0–192.168.255.255 private,    256 24-Bit-Nets 192.168.0.0/16 RFC 1918
   * 169.254.0.0–169.254.255.255 link local,   1 16-Bit-Net  169.254.0.0/16 RFC 3927
   *
   * @return true when private address, otherwise false
   */
  public boolean isPrivate()
   {
    if ("10".equals(this.parts[0]) || (IPV4Address.CLASS_C_192.equals(this.parts[0]) && "168".equals(this.parts[1])) || ("169".equals(this.parts[0]) && "254".equals(this.parts[1]))) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
     {
      return true;
     }
    if ("172".equals(this.parts[0])) //$NON-NLS-1$
     {
      final int block2 = Integer.parseInt(this.parts[1]);
      if ((block2 >= 16) && (block2 <= 31))
       {
        return true;
       }
     }
    return false;
   }


  /**
   * Is an IP V4 special address.
   *
   * 0.0.0.0/8           Das vorliegende Netzwerk       RFC 1122
   * 127.0.0.0/8         Loopback                       RFC 1122
   * 100.64.0.0/10       Shared Transition Space        RFC 6598
   * 192.0.0.0/24        IETF Protocol Assignments      RFC 6890
   * 192.0.2.0/24        Test-Netzwerke                 RFC 6890
   * 192.88.99.0/24      IPv6 zu IPv4 Relay (Veraltet)  RFC 7526
   * 198.18.0.0/15       Netzwerk-Benchmark-Tests       RFC 2544
   * 198.51.100.0/24     Test-Netzwerke                 RFC 6890
   * 203.0.113.0/24      Test-Netzwerke                 RFC 6890
   * 224.0.0.0/4         Multicasts                     RFC 5771
   * 240.0.0.0/4         Reserviert                     RFC 1700
   * 255.255.255.255/32  Limited Broadcast              RFC 919, RFC 922
   *
   * @return true when special address, otherwise false
   */
  public boolean isSpecial()
   {
    if (IPV4Address.ZERO.equals(this.parts[0]) ||
        "127".equals(this.parts[0]) || //$NON-NLS-1$
        (IPV4Address.CLASS_C_192.equals(this.parts[0]) && IPV4Address.ZERO.equals(this.parts[1]) && IPV4Address.ZERO.equals(this.parts[2])) ||
        (IPV4Address.CLASS_C_192.equals(this.parts[0]) && IPV4Address.ZERO.equals(this.parts[1]) && "2".equals(this.parts[2])) || //$NON-NLS-1$
        (IPV4Address.CLASS_C_192.equals(this.parts[0]) && "88".equals(this.parts[1]) && "99".equals(this.parts[2])) || //$NON-NLS-1$ //$NON-NLS-2$
        (IPV4Address.C198.equals(this.parts[0]) && "51".equals(this.parts[1]) && IPV4Address.C100.equals(this.parts[2])) || //$NON-NLS-1$
        ("203".equals(this.parts[0]) && IPV4Address.ZERO.equals(this.parts[1]) && "113".equals(this.parts[2])) //$NON-NLS-1$ //$NON-NLS-2$
       )
     {
      return true;
     }
    if (IPV4Address.C100.equals(this.parts[0]))
     {
      final int block2 = Integer.parseInt(this.parts[1]);
      if ((block2 >= 64) && (block2 <= 127))
       {
        return true;
       }
     }
    if (IPV4Address.C198.equals(this.parts[0]))
     {
      final int block2 = Integer.parseInt(this.parts[1]);
      if ((block2 >= 18) && (block2 <= 19))
       {
        return true;
       }
     }
    final int block1 = Integer.parseInt(this.parts[0]);
    return (block1 >= 224);
   }


  /**
   * Is an IP V4 public address.
   *
   * @return true when public address, otherwise false
   */
  public boolean isPublic()
   {
    return !isPrivate() && !isSpecial();
   }


  /**
   * Returns the value of this IPV4Address 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 IPV4Address))
     {
      return false;
     }
    final IPV4Address other = (IPV4Address)obj;
    return this.address.equals(other.address);
   }


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

 }