GradoopId.java

/*
 * Copyright © 2014 - 2021 Leipzig University (Database Research Group)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gradoop.common.model.impl.id;

import org.apache.flink.core.memory.DataInputView;
import org.apache.flink.core.memory.DataOutputView;
import org.apache.flink.core.memory.MemorySegment;
import org.apache.flink.types.CopyableValue;
import org.apache.flink.types.NormalizableKey;
import org.gradoop.common.model.api.entities.Identifiable;

import java.io.IOException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Primary key for an EPGM element.
 * <p>
 * This implementation reuses much of the code of BSON's ObjectId
 * (org.bson.types.ObjectId) to guarantee uniqueness. Much of the code is copied directly or
 * has only small changes.
 *
 * @see Identifiable
 * <p>
 * references to: org.bson.types.ObjectId
 */
public class GradoopId implements NormalizableKey<GradoopId>, CopyableValue<GradoopId> {

  /**
   * Number of bytes to represent an id internally.
   */
  public static final int ID_SIZE = 12;

  /**
   * Represents a null id.
   */
  public static final GradoopId NULL_VALUE =
    new GradoopId(0, 0, (short) 0, 0);

  /**
   * Integer containing a unique identifier of the machine
   */
  private static final int MACHINE_IDENTIFIER;

  /**
   * Short containing a unique identifier of the process
   */
  private static final short PROCESS_IDENTIFIER;

  /**
   * Integer containing a counter that is increased whenever a new id is created
   */
  private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt());

  /**
   * Bit mask used to extract the lowest three bytes of four
   */
  private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff;

  /**
   * Bit mask used to extract the highest byte of four
   */
  private static final int HIGH_ORDER_ONE_BYTE = 0xff000000;

  /**
   * Required for {@link GradoopId#toString()}
   */
  private static final char[] HEX_CHARS = new char[] {
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

  /**
   * Internal byte representation
   */
  private byte[] bytes;

  static {
    MACHINE_IDENTIFIER = createMachineIdentifier();
    PROCESS_IDENTIFIER = createProcessIdentifier();
  }

  /**
   * Required default constructor for instantiation by serialization logic.
   */
  public GradoopId() {
    bytes = new byte[ID_SIZE];
  }

  /**
   * Creates a GradoopId from a given byte representation
   *
   * @param bytes the GradoopId represented by the byte array
   */
  private GradoopId(byte[] bytes) {
    this.bytes = bytes;
  }

  /**
   * Creates a GradoopId using the given time, machine identifier, process identifier, and counter.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @param timestamp         the time in seconds
   * @param machineIdentifier the machine identifier
   * @param processIdentifier the process identifier
   * @param counter           the counter
   * @throws IllegalArgumentException if the high order byte of machineIdentifier
   *                                  or counter is not zero
   */
  public GradoopId(final int timestamp, final int machineIdentifier,
    final short processIdentifier, final int counter) {
    this(timestamp, machineIdentifier, processIdentifier, counter, true);
  }


  /**
   * Creates a GradoopId using the given time, machine identifier, process identifier, and counter.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @param timestamp         the time in seconds
   * @param machineIdentifier the machine identifier
   * @param processIdentifier the process identifier
   * @param counter           the counter
   * @param checkCounter      if the constructor should test if the counter is between 0 and
   *                          16777215
   */
  private GradoopId(final int timestamp, final int machineIdentifier, final short processIdentifier,
    final int counter, final boolean checkCounter) {
    if ((machineIdentifier & HIGH_ORDER_ONE_BYTE) != 0) {
      throw new IllegalArgumentException("The machine identifier must be between 0" +
        " and 16777215 (it must fit in three bytes).");
    }
    if (checkCounter && ((counter & HIGH_ORDER_ONE_BYTE) != 0)) {
      throw new IllegalArgumentException("The counter must be between 0" +
        " and 16777215 (it must fit in three bytes).");
    }

    ByteBuffer buffer = ByteBuffer.allocate(12);

    buffer.put((byte) (timestamp >> 24));
    buffer.put((byte) (timestamp >> 16));
    buffer.put((byte) (timestamp >> 8));
    buffer.put((byte) timestamp);

    buffer.put((byte) (machineIdentifier >> 16));
    buffer.put((byte) (machineIdentifier >> 8));
    buffer.put((byte) machineIdentifier);

    buffer.put((byte) (processIdentifier >> 8));
    buffer.put((byte) processIdentifier);

    buffer.put((byte) (counter >> 16));
    buffer.put((byte) (counter >> 8));
    buffer.put((byte) counter);

    this.bytes = buffer.array();
  }

  /**
   * Creates the machine identifier from the network interface.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @return a short representing the process
   */
  private static int createMachineIdentifier() {
    int machinePiece;
    try {
      StringBuilder sb = new StringBuilder();
      Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
      while (e.hasMoreElements()) {
        NetworkInterface ni = e.nextElement();
        sb.append(ni.toString());
        byte[] mac = ni.getHardwareAddress();
        if (mac != null) {
          ByteBuffer bb = ByteBuffer.wrap(mac);
          try {
            sb.append(bb.getChar());
            sb.append(bb.getChar());
            sb.append(bb.getChar());
          } catch (BufferUnderflowException shortHardwareAddressException) {
            // mac with less than 6 bytes. continue
          }
        }
      }
      machinePiece = sb.toString().hashCode();
    } catch (SocketException t) {
      machinePiece = new SecureRandom().nextInt();
    }
    machinePiece = machinePiece & LOW_ORDER_THREE_BYTES;
    return machinePiece;
  }

  /**
   * Creates the process identifier.  This does not have to be unique per class loader because
   * NEXT_COUNTER will provide the uniqueness.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @return a short representing the process
   */
  private static short createProcessIdentifier() {
    short processId;
    String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName();
    if (processName.contains("@")) {
      processId = (short) Integer.parseInt(processName.substring(0, processName.indexOf('@')));
    } else {
      processId = (short) java.lang.management.ManagementFactory
        .getRuntimeMXBean().getName().hashCode();
    }

    return processId;
  }

  /**
   * Returns a new GradoopId
   *
   * @return new GradoopId
   */
  public static GradoopId get() {
    return new GradoopId(dateToTimestampSeconds(new Date()), MACHINE_IDENTIFIER,
      PROCESS_IDENTIFIER, NEXT_COUNTER.getAndIncrement(), false);
  }

  /**
   * Converts a date into the seconds since unix epoch.
   *
   * @param time a time
   * @return int representing the seconds between unix epoch and the given time
   */
  private static int dateToTimestampSeconds(final Date time) {
    return (int) (time.getTime() / 1000);
  }

  /**
   * Returns the Gradoop ID represented by a specified hexadecimal string.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @param string hexadecimal GradoopId representation
   * @return GradoopId
   */
  public static GradoopId fromString(String string) {
    if (!GradoopId.isValid(string)) {
      throw new IllegalArgumentException(
        "invalid hexadecimal representation of a GradoopId: [" + string + "]");
    }

    byte[] b = new byte[12];
    for (int i = 0; i < b.length; i++) {
      b[i] = (byte) Integer.parseInt(string.substring(i * 2, i * 2 + 2), 16);
    }
    return new GradoopId(b);
  }

  /**
   * Checks if a string can be transformed into a GradoopId.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @param hexString a potential GradoopId as a String.
   * @return whether the string could be an object id
   * @throws IllegalArgumentException if hexString is null
   */
  public static boolean isValid(final String hexString) {
    if (hexString == null) {
      throw new IllegalArgumentException();
    }

    int len = hexString.length();
    if (len != 24) {
      return false;
    }

    for (int i = 0; i < len; i++) {
      char c = hexString.charAt(i);
      if (c >= '0' && c <= '9') {
        continue;
      }
      if (c >= 'a' && c <= 'f') {
        continue;
      }
      if (c >= 'A' && c <= 'F') {
        continue;
      }

      return false;
    }

    return true;
  }

  /**
   * Returns the Gradoop ID represented by a byte array
   *
   * @param bytes byte representation
   * @return Gradoop ID
   */
  public static GradoopId fromByteArray(byte[] bytes) {
    return new GradoopId(bytes);
  }

  /**
   * Returns byte representation of a GradoopId
   *
   * @return Byte representation
   */
  public byte[] toByteArray() {
    return bytes;
  }

  /**
   * Checks if the specified object is equal to the current id.
   *
   * @param o the object to be compared
   * @return true, iff the specified id is equal to this id
   */
  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    byte[] firstBytes = this.bytes;
    byte[] secondBytes = ((GradoopId) o).bytes;
    for (int i = 0; i < GradoopId.ID_SIZE; i++) {
      if (firstBytes[i] != secondBytes[i]) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns the hash code of this GradoopId.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @return hash code
   */
  @Override
  public int hashCode() {
    int result = getTimeStamp();
    result = 31 * result + getMachineIdentifier();
    result = 31 * result + (int) getProcessIdentifier();
    result = 31 * result + getCounter();
    return result;
  }

  /**
   * Performs a byte-wise comparison of this and the specified GradoopId.
   *
   * @param other the object to be compared.
   * @return a negative integer, zero, or a positive integer as this object
   * is less than, equal to, or greater than the specified object.
   */
  @Override
  public int compareTo(GradoopId other) {

    for (int i = 0; i < GradoopId.ID_SIZE; i++) {
      if (this.bytes[i] != other.bytes[i]) {
        return ((this.bytes[i] & 0xff) < (other.bytes[i] & 0xff)) ? -1 : 1;
      }
    }
    return 0;
  }

  /**
   * Returns hex string representation of a GradoopId.
   * <p>
   * Note: Implementation taken from org.bson.types.ObjectId
   *
   * @return GradoopId string representation.
   */
  @Override
  public String toString() {
    char[] chars = new char[24];
    int i = 0;
    for (byte b : bytes) {
      chars[i++] = HEX_CHARS[b >> 4 & 0xF];
      chars[i++] = HEX_CHARS[b & 0xF];
    }
    return String.valueOf(chars);
  }

  //------------------------------------------------------------------------------------------------
  // methods inherited from NormalizableKey
  //------------------------------------------------------------------------------------------------

  @Override
  public int getMaxNormalizedKeyLen() {
    return ID_SIZE;
  }

  @Override
  public void copyNormalizedKey(MemorySegment target, int offset, int len) {
    target.put(offset, bytes, 0, len);
  }

  @Override
  public void write(DataOutputView out) throws IOException {
    out.write(bytes);
  }

  @Override
  public void read(DataInputView in) throws IOException {
    in.readFully(bytes);
  }

  //------------------------------------------------------------------------------------------------
  // methods inherited from CopyableValue
  //------------------------------------------------------------------------------------------------

  @Override
  public int getBinaryLength() {
    return ID_SIZE;
  }

  @Override
  public void copyTo(GradoopId target) {
    System.arraycopy(bytes, 0, target.bytes, 0, ID_SIZE);
  }

  @Override
  public GradoopId copy() {
    return new GradoopId(Arrays.copyOf(bytes, ID_SIZE));
  }

  @Override
  public void copy(DataInputView source, DataOutputView target) throws IOException {
    target.write(source, ID_SIZE);
  }

  //------------------------------------------------------------------------------------------------
  // private little helpers
  //------------------------------------------------------------------------------------------------

  /**
   * Returns the timestamp component of the id.
   *
   * @return the timestamp
   */
  private int getTimeStamp() {
    return makeInt(bytes[0], bytes[1], bytes[2], bytes[3]);
  }

  /**
   * Returns the machine identifier component of the id.
   *
   * @return the machine identifier
   */
  private int getMachineIdentifier() {
    return makeInt((byte) 0, bytes[4], bytes[5], bytes[6]);
  }

  /**
   * Returns the process identifier component of the id.
   *
   * @return the process identifier
   */
  private short getProcessIdentifier() {
    return (short) makeInt((byte) 0, (byte) 0, bytes[7], bytes[8]);
  }

  /**
   * Returns the counter component of the id.
   *
   * @return the counter
   */
  private int getCounter() {
    return makeInt((byte) 0, bytes[9], bytes[10], bytes[11]);
  }


  //------------------------------------------------------------------------------------------------
  // static helper functions
  //------------------------------------------------------------------------------------------------

  /**
   * Compares the given GradoopIds and returns the smaller one. It both are equal, the first
   * argument is returned.
   *
   * @param first  first GradoopId
   * @param second second GradoopId
   * @return smaller GradoopId or first if equal
   */
  public static GradoopId min(GradoopId first, GradoopId second) {
    int comparison = first.compareTo(second);
    return comparison == 0 ? first : (comparison < 0 ? first : second);
  }

  /**
   * Returns a primitive int represented by the given 4 bytes.
   *
   * @param b3 byte 3
   * @param b2 byte 2
   * @param b1 byte 1
   * @param b0 byte 0
   * @return int value
   */
  private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) {
    return (b3 << 24) | ((b2 & 0xff) << 16) | ((b1 & 0xff) << 8) | ((b0 & 0xff));
  }
}