MACAddress.java

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


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

import de.powerstat.validation.interfaces.IValueObject;


/**
 * Canonical Media-Access-Control-Adresse (MAC).
 *
 * TODO getManufacturer name
 * TODO Exists in network
 * http://standards-oui.ieee.org/oui/oui.csv
 */
public final class MACAddress implements Comparable<MACAddress>, IValueObject
 {
  /* *
   * Cache for singletons.
   */
  // private static final Map<String, MACAddress> CACHE = new WeakHashMap<>();

  /**
   * Hex 00.
   */
  private static final String H00 = "00"; //$NON-NLS-1$

  /**
   * Hex 01.
   */
  private static final String H01 = "01"; //$NON-NLS-1$

  /**
   * Hex 33.
   */
  private static final String H33 = "33"; //$NON-NLS-1$

  /**
   * Hex 5e.
   */
  private static final String H5E = "5e"; //$NON-NLS-1$

  /**
   * Hex ff.
   */
  private static final String HFF = "ff"; //$NON-NLS-1$

  /**
   * Byte separator.
   */
  private static final String SEPARATOR = ":"; //$NON-NLS-1$

  /**
   * Delimiter.
   */
  private static final String DELIMITER = "-"; //$NON-NLS-1$

  /**
   * Delimiter constant.
   */
  private static final String DELIMITER_TXT = "delimiter"; //$NON-NLS-1$

  /**
   * Illegal delimiter length constant.
   */
  private static final String ILLEGAL_DELIMITER_LENGTH = "Illegal delimiter length"; //$NON-NLS-1$

  /**
   * Illegal delimiter character constant.
   */
  private static final String ILLEGAL_DELIMITER_CHARACTER = "Illegal delimiter character"; //$NON-NLS-1$

  /**
   * IP V6 regexp.
   */
  private static final Pattern IPV6_REGEXP = Pattern.compile("^[0-9a-f]{2}([:-]?[0-9a-f]{2}){5}$"); //$NON-NLS-1$

  /**
   * IP V6 separator regexp.
   */
  private static final Pattern IPV6_SEPARATOR_REGEXP = Pattern.compile("[:-]"); //$NON-NLS-1$

  /**
   * MAC address parts.
   */
  private final String[] parts;


  /**
   * Constructor.
   *
   * @param address MAC address
   * @throws NullPointerException if address is null
   * @throws IllegalArgumentException if address is not a mac address
   */
  private MACAddress(final String address)
   {
    super();
    Objects.requireNonNull(address, "address"); //$NON-NLS-1$
    if ((address.length() != 12) && (address.length() != 17))
     {
      throw new IllegalArgumentException("To short or long for a mac address"); //$NON-NLS-1$
     }
    if (!MACAddress.IPV6_REGEXP.matcher(address.toLowerCase(Locale.getDefault())).matches())
     {
      throw new IllegalArgumentException("Not a mac address"); //$NON-NLS-1$
     }
    this.parts = MACAddress.IPV6_SEPARATOR_REGEXP.split(address.toLowerCase(Locale.getDefault()));
   }


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


  /**
   * Returns the value of this MACAddress as a string.
   *
   * @param delimiter Delimiter could be empty, : or -
   * @return The text value represented by this object after conversion to type string.
   */
  public String stringValue(final String delimiter)
   {
    Objects.requireNonNull(delimiter, MACAddress.DELIMITER_TXT);
    if (delimiter.length() > 1)
     {
      throw new IllegalArgumentException(MACAddress.ILLEGAL_DELIMITER_LENGTH);
     }
    if (!delimiter.isEmpty() && !MACAddress.SEPARATOR.equals(delimiter) && !MACAddress.DELIMITER.equals(delimiter))
     {
      throw new IllegalArgumentException(MACAddress.ILLEGAL_DELIMITER_CHARACTER);
     }
    return String.join(delimiter, this.parts);
   }


  /**
   * Returns the value of this MACADdress as a string with delimiter ':'.
   *
   * @return The text value represented by this object after conversion to type string.
   */
  @Override
  public String stringValue()
   {
    return stringValue(MACAddress.SEPARATOR);
   }


  /**
   * Is broadcast address.
   *
   * @return true if broadcast address, false otherwise
   */
  public boolean isBroadcast()
   {
    return MACAddress.HFF.equals(this.parts[0]) && MACAddress.HFF.equals(this.parts[1]) && MACAddress.HFF.equals(this.parts[2]) && MACAddress.HFF.equals(this.parts[3]) && MACAddress.HFF.equals(this.parts[4]) && MACAddress.HFF.equals(this.parts[5]);
   }


  /**
   * Is group.
   *
   * @return true: group, false: individual
   */
  public boolean isGroup()
   {
    return (Integer.parseInt(this.parts[0], 16) & 0x01) != 0;
   }


  /**
   * Is local.
   *
   * @return true: local, false: universal
   */
  public boolean isLocal()
   {
    return (Integer.parseInt(this.parts[0], 16) & 0x02) != 0;
   }


  /**
   * Is IP v4 multicast mac.
   *
   * @return true if mac is an ip v4 multicast address, false otherwise
   */
  public boolean isIPV4Multicast()
   {
    return MACAddress.H01.equals(this.parts[0]) && MACAddress.H00.equals(this.parts[1]) && MACAddress.H5E.equals(this.parts[2]) && ((Integer.parseInt(this.parts[3], 16) & 0x80) == 0);
   }


  /**
   * Is IP v6 multicast mac.
   *
   * @return true if mac is an ip v6 multicast address, false otherwise
   */
  public boolean isIPV6Multicast()
   {
    return MACAddress.H33.equals(this.parts[0]) && MACAddress.H33.equals(this.parts[1]);
   }


  /**
   * Is VRRP mac (Virtual Router Redundancy Protocol).
   *
   * @return true if mac is a vrrp address, false otherwise
   */
  public boolean isVRRP()
   {
    return MACAddress.H00.equals(this.parts[0]) && MACAddress.H00.equals(this.parts[1]) && MACAddress.H5E.equals(this.parts[2]) && MACAddress.H00.equals(this.parts[3]) && MACAddress.H01.equals(this.parts[4]);
   }


  /**
   * Get OUI (Organizationally Unique Identifier).
   *
   * @return OUI (MA-L) string 000000
   *
   * TODO OUI-28 (MA-M), OUI-36 (MA-S)
   */
  public String getOUI()
   {
    return String.format("%1$02X", Integer.parseInt(this.parts[0], 16) & 0xfc) + this.parts[1].toUpperCase(Locale.getDefault()) + this.parts[2].toUpperCase(Locale.getDefault()); //$NON-NLS-1$
   }


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


  /**
   * 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 MACAddress))
     {
      return false;
     }
    final MACAddress other = (MACAddress)obj;
    return Arrays.equals(this.parts, other.parts);
   }


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

 }