All Downloads are FREE. Search and download functionalities are using the official Maven repository.

smile.json.ObjectId.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010-2021 Haifeng Li. All rights reserved.
 *
 * Smile is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Smile is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Smile.  If not, see .
 */

package smile.json

import java.nio.ByteBuffer
import java.util.{Arrays, Date}
import scala.util.Try
import scala.jdk.CollectionConverters._

/**
 * BSON's 12-byte ObjectId type, constructed using:
 * a 4-byte value representing the seconds since the Unix epoch,
 * a 3-byte machine identifier,
 * a 2-byte process id, and
 * a 3-byte counter, starting with a random value.
 *
 * The implementation is adopt from ReactiveMongo.
 */
case class ObjectId(id: Array[Byte]) {
  require(id.length == ObjectId.size)

  override def equals(that: Any): Boolean = {
    that.isInstanceOf[ObjectId] && Arrays.equals(id, that.asInstanceOf[ObjectId].id)
  }

  /** In the form of a string literal "ObjectId(...)" */
  override def toString: String = {
    s"""ObjectId(${ObjectId.bytes2hex(id)})"""
  }

  /** The timestamp port of ObjectId object as a Date */
  def timestamp: Date = new Date(ByteBuffer.wrap(id.take(4)).getInt * 1000L)
}

object ObjectId {
  /** ObjectId byte array size */
  val size = 12

  private val md5Encoder = java.security.MessageDigest.getInstance("MD5")

  /** MD5 hash function */
  private def md5(bytes: Array[Byte]) = md5Encoder.digest(bytes)

  private val maxCounterValue = 16777216
  private val increment = new java.util.concurrent.atomic.AtomicInteger(scala.util.Random.nextInt(maxCounterValue))

  private def counter = (increment.getAndIncrement + maxCounterValue) % maxCounterValue

  /** Byte array to hexadecimal string. */
  def bytes2hex(bytes: Array[Byte]): String = {
    bytes.map("%02X" format _).mkString
  }

  /** Hexadecimal string to byte array. */
  def hex2bytes(s: String): Array[Byte] = {
    require(s.length % 2 == 0, "Hexadecimal string must contain an even number of characters")

    val bytes = new Array[Byte](s.length / 2)
    for (i <- 0 until s.length by 2) {
      bytes(i/2) = java.lang.Integer.parseInt(s.substring(i, i+2), 16).toByte
    }
    bytes
  }

  /**
   * The following implementation of machineId work around openjdk limitations in
   * version 6 and 7
   *
   * Openjdk fails to parse /proc/net/if_inet6 correctly to determine mac address
   * resulting in SocketException thrown.
   *
   * Please see:
   * * https://github.com/openjdk-mirror/jdk7u-jdk/blob/feeaec0647609a1e6266f902de426f1201f77c55/src/solaris/native/java/net/NetworkInterface.c#L1130
   * * http://lxr.free-electrons.com/source/net/ipv6/addrconf.c?v=3.11#L3442
   * * http://lxr.free-electrons.com/source/include/linux/netdevice.h?v=3.11#L1130
   * * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7078386
   *
   * and fix in openjdk8:
   * * http://hg.openjdk.java.net/jdk8/tl/jdk/rev/b1814b3ea6d3
   */
  private val machineId: Array[Byte] = {
    val correctVersion = System.getProperty("java.version").substring(0, 3).toFloat >= 1.8
    val noIpv6 = System.getProperty("java.net.preferIPv4Stack") == "true"
    val isLinux = System.getProperty("os.name") == "Linux"

    val mac = if (!isLinux || correctVersion || noIpv6) {
      Try {
        import java.net._
        val networkInterfacesEnum = NetworkInterface.getNetworkInterfaces
        val networkInterfaces = networkInterfacesEnum.asScala
        val ha = networkInterfaces.find(ha => Try(ha.getHardwareAddress).isSuccess && ha.getHardwareAddress != null && ha.getHardwareAddress.length == 6)
          .map(_.getHardwareAddress)
          .getOrElse(InetAddress.getLocalHost.getHostName.getBytes)
        md5(ha).take(3)
      }.toOption
    } else None

    mac.getOrElse {
      val threadId = Thread.currentThread.getId.toInt
      Array(
        (threadId & 0xFF).toByte,
        (threadId >> 8 & 0xFF).toByte,
        (threadId >> 16 & 0xFF).toByte
      )
    }
  }

  /** Generate a new BSON ObjectId. */
  def apply(): ObjectId = generate

  /**
   * Constructs a BSON ObjectId element from a hexadecimal String representation.
   * Throws an exception if the given argument is not a valid ObjectID.
   *
   * `parse(str: String): Try[BSONObjectID]` should be considered instead of this method.
   */
  def apply(id: String): ObjectId = {
    require(id.length == 24, s"wrong ObjectId: '$id'")
    /** Constructs a BSON ObjectId element from a hexadecimal String representation */
    new ObjectId(hex2bytes(id))
  }

  /** Tries to make a BSON ObjectId element from a hexadecimal String representation. */
  def parse(str: String): Try[ObjectId] = Try(apply(str))

  /**
   * Generates a new BSON ObjectId.
   *
   * +------------------------+------------------------+------------------------+------------------------+
   * + timestamp (in seconds) +   machine identifier   +    thread identifier   +        increment       +
   * +        (4 bytes)       +        (3 bytes)       +        (2 bytes)       +        (3 bytes)       +
   * +------------------------+------------------------+------------------------+------------------------+
   *
   * The returned BSONObjectID contains a timestamp set to the current time (in seconds),
   * with the `machine identifier`, `thread identifier` and `increment` properly set.
   */
  def generate: ObjectId = fromTime(System.currentTimeMillis, fillOnlyTimestamp = false)

  /**
   * Generates a new BSON ObjectID from the given timestamp in milliseconds.
   *
   * +------------------------+------------------------+------------------------+------------------------+
   * + timestamp (in seconds) +   machine identifier   +    thread identifier   +        increment       +
   * +        (4 bytes)       +        (3 bytes)       +        (2 bytes)       +        (3 bytes)       +
   * +------------------------+------------------------+------------------------+------------------------+
   *
   * The included timestamp is the number of seconds since epoch, so a BSONObjectID time part has only
   * a precision up to the second. To get a reasonably unique ID, you _must_ set `onlyTimestamp` to false.
   *
   * Crafting a BSONObjectID from a timestamp with `fillOnlyTimestamp` set to true is helpful for range queries,
   * eg if you want of find documents an _id field which timestamp part is greater than or lesser than
   * the one of another id.
   *
   * If you do not intend to use the produced BSONObjectID for range queries, then you'd rather use
   * the `generate` method instead.
   *
   * @param fillOnlyTimestamp if true, the returned BSONObjectID will only have the timestamp bytes set; the other will be set to zero.
   */
  def fromTime(timeMillis: Long, fillOnlyTimestamp: Boolean = true): ObjectId = {
    // n of seconds since epoch. Big endian
    val timestamp = (timeMillis / 1000).toInt
    val id = new Array[Byte](12)

    id(0) = (timestamp >>> 24).toByte
    id(1) = (timestamp >> 16 & 0xFF).toByte
    id(2) = (timestamp >> 8 & 0xFF).toByte
    id(3) = (timestamp & 0xFF).toByte

    if (!fillOnlyTimestamp) {
      // machine id, 3 first bytes of md5(macadress or hostname)
      id(4) = machineId(0)
      id(5) = machineId(1)
      id(6) = machineId(2)

      // 2 bytes of the pid or thread id. Thread id in our case. Low endian
      val threadId = Thread.currentThread.getId.toInt
      id(7) = (threadId & 0xFF).toByte
      id(8) = (threadId >> 8 & 0xFF).toByte

      // 3 bytes of counter sequence, which start is randomized. Big endian
      val c = counter
      id(9) = (c >> 16 & 0xFF).toByte
      id(10) = (c >> 8 & 0xFF).toByte
      id(11) = (c & 0xFF).toByte
    }

    new ObjectId(id)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy