/* Copyright 2010-2014 MongoDB Inc.
*
* 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.
*/
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace MongoDB.Bson
{
///
/// Represents an ObjectId (see also BsonObjectId).
///
[Serializable]
public struct ObjectId : IComparable, IEquatable, IConvertible
{
// private static fields
private static ObjectId __emptyInstance = default(ObjectId);
private static int __staticMachine;
private static short __staticPid;
private static int __staticIncrement; // high byte will be masked out when generating new ObjectId
// private fields
// we're using 14 bytes instead of 12 to hold the ObjectId in memory but unlike a byte[] there is no additional object on the heap
// the extra two bytes are not visible to anyone outside of this class and they buy us considerable simplification
// an additional advantage of this representation is that it will serialize to JSON without any 64 bit overflow problems
private int _timestamp;
private int _machine;
private short _pid;
private int _increment;
// static constructor
static ObjectId()
{
__staticMachine = (GetMachineHash() + AppDomain.CurrentDomain.Id) & 0x00ffffff; // add AppDomain Id to ensure uniqueness across AppDomains
__staticIncrement = (new Random()).Next();
try
{
__staticPid = (short)GetCurrentProcessId(); // use low order two bytes only
}
catch (SecurityException)
{
__staticPid = 0;
}
}
// constructors
///
/// Initializes a new instance of the ObjectId class.
///
/// The bytes.
public ObjectId(byte[] bytes)
{
if (bytes == null)
{
throw new ArgumentNullException("bytes");
}
Unpack(bytes, out _timestamp, out _machine, out _pid, out _increment);
}
///
/// Initializes a new instance of the ObjectId class.
///
/// The timestamp (expressed as a DateTime).
/// The machine hash.
/// The PID.
/// The increment.
public ObjectId(DateTime timestamp, int machine, short pid, int increment)
: this(GetTimestampFromDateTime(timestamp), machine, pid, increment)
{
}
///
/// Initializes a new instance of the ObjectId class.
///
/// The timestamp.
/// The machine hash.
/// The PID.
/// The increment.
public ObjectId(int timestamp, int machine, short pid, int increment)
{
if ((machine & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("machine", "The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
}
if ((increment & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("increment", "The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
}
_timestamp = timestamp;
_machine = machine;
_pid = pid;
_increment = increment;
}
///
/// Initializes a new instance of the ObjectId class.
///
/// The value.
public ObjectId(string value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
Unpack(BsonUtils.ParseHexString(value), out _timestamp, out _machine, out _pid, out _increment);
}
// public static properties
///
/// Gets an instance of ObjectId where the value is empty.
///
public static ObjectId Empty
{
get { return __emptyInstance; }
}
// public properties
///
/// Gets the timestamp.
///
public int Timestamp
{
get { return _timestamp; }
}
///
/// Gets the machine.
///
public int Machine
{
get { return _machine; }
}
///
/// Gets the PID.
///
public short Pid
{
get { return _pid; }
}
///
/// Gets the increment.
///
public int Increment
{
get { return _increment; }
}
///
/// Gets the creation time (derived from the timestamp).
///
public DateTime CreationTime
{
get { return BsonConstants.UnixEpoch.AddSeconds(_timestamp); }
}
// public operators
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId
/// True if the first ObjectId is less than the second ObjectId.
public static bool operator <(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) < 0;
}
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId
/// True if the first ObjectId is less than or equal to the second ObjectId.
public static bool operator <=(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) <= 0;
}
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId.
/// True if the two ObjectIds are equal.
public static bool operator ==(ObjectId lhs, ObjectId rhs)
{
return lhs.Equals(rhs);
}
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId.
/// True if the two ObjectIds are not equal.
public static bool operator !=(ObjectId lhs, ObjectId rhs)
{
return !(lhs == rhs);
}
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId
/// True if the first ObjectId is greather than or equal to the second ObjectId.
public static bool operator >=(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) >= 0;
}
///
/// Compares two ObjectIds.
///
/// The first ObjectId.
/// The other ObjectId
/// True if the first ObjectId is greather than the second ObjectId.
public static bool operator >(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) > 0;
}
// public static methods
///
/// Generates a new ObjectId with a unique value.
///
/// An ObjectId.
public static ObjectId GenerateNewId()
{
return GenerateNewId(GetTimestampFromDateTime(DateTime.UtcNow));
}
///
/// Generates a new ObjectId with a unique value (with the timestamp component based on a given DateTime).
///
/// The timestamp component (expressed as a DateTime).
/// An ObjectId.
public static ObjectId GenerateNewId(DateTime timestamp)
{
return GenerateNewId(GetTimestampFromDateTime(timestamp));
}
///
/// Generates a new ObjectId with a unique value (with the given timestamp).
///
/// The timestamp component.
/// An ObjectId.
public static ObjectId GenerateNewId(int timestamp)
{
int increment = Interlocked.Increment(ref __staticIncrement) & 0x00ffffff; // only use low order 3 bytes
return new ObjectId(timestamp, __staticMachine, __staticPid, increment);
}
///
/// Packs the components of an ObjectId into a byte array.
///
/// The timestamp.
/// The machine hash.
/// The PID.
/// The increment.
/// A byte array.
public static byte[] Pack(int timestamp, int machine, short pid, int increment)
{
if ((machine & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("machine", "The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
}
if ((increment & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("increment", "The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
}
byte[] bytes = new byte[12];
bytes[0] = (byte)(timestamp >> 24);
bytes[1] = (byte)(timestamp >> 16);
bytes[2] = (byte)(timestamp >> 8);
bytes[3] = (byte)(timestamp);
bytes[4] = (byte)(machine >> 16);
bytes[5] = (byte)(machine >> 8);
bytes[6] = (byte)(machine);
bytes[7] = (byte)(pid >> 8);
bytes[8] = (byte)(pid);
bytes[9] = (byte)(increment >> 16);
bytes[10] = (byte)(increment >> 8);
bytes[11] = (byte)(increment);
return bytes;
}
///
/// Parses a string and creates a new ObjectId.
///
/// The string value.
/// A ObjectId.
public static ObjectId Parse(string s)
{
if (s == null)
{
throw new ArgumentNullException("s");
}
ObjectId objectId;
if (TryParse(s, out objectId))
{
return objectId;
}
else
{
var message = string.Format("'{0}' is not a valid 24 digit hex string.", s);
throw new FormatException(message);
}
}
///
/// Tries to parse a string and create a new ObjectId.
///
/// The string value.
/// The new ObjectId.
/// True if the string was parsed successfully.
public static bool TryParse(string s, out ObjectId objectId)
{
// don't throw ArgumentNullException if s is null
if (s != null && s.Length == 24)
{
byte[] bytes;
if (BsonUtils.TryParseHexString(s, out bytes))
{
objectId = new ObjectId(bytes);
return true;
}
}
objectId = default(ObjectId);
return false;
}
///
/// Unpacks a byte array into the components of an ObjectId.
///
/// A byte array.
/// The timestamp.
/// The machine hash.
/// The PID.
/// The increment.
public static void Unpack(byte[] bytes, out int timestamp, out int machine, out short pid, out int increment)
{
if (bytes == null)
{
throw new ArgumentNullException("bytes");
}
if (bytes.Length != 12)
{
throw new ArgumentOutOfRangeException("bytes", "Byte array must be 12 bytes long.");
}
timestamp = (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
machine = (bytes[4] << 16) + (bytes[5] << 8) + bytes[6];
pid = (short)((bytes[7] << 8) + bytes[8]);
increment = (bytes[9] << 16) + (bytes[10] << 8) + bytes[11];
}
// private static methods
///
/// Gets the current process id. This method exists because of how CAS operates on the call stack, checking
/// for permissions before executing the method. Hence, if we inlined this call, the calling method would not execute
/// before throwing an exception requiring the try/catch at an even higher level that we don't necessarily control.
///
[MethodImpl(MethodImplOptions.NoInlining)]
private static int GetCurrentProcessId()
{
return Process.GetCurrentProcess().Id;
}
private static int GetMachineHash()
{
var hostName = Environment.MachineName; // use instead of Dns.HostName so it will work offline
var md5 = MD5.Create();
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(hostName));
return (hash[0] << 16) + (hash[1] << 8) + hash[2]; // use first 3 bytes of hash
}
private static int GetTimestampFromDateTime(DateTime timestamp)
{
return (int)Math.Floor((BsonUtils.ToUniversalTime(timestamp) - BsonConstants.UnixEpoch).TotalSeconds);
}
// public methods
///
/// Compares this ObjectId to another ObjectId.
///
/// The other ObjectId.
/// A 32-bit signed integer that indicates whether this ObjectId is less than, equal to, or greather than the other.
public int CompareTo(ObjectId other)
{
int r = _timestamp.CompareTo(other._timestamp);
if (r != 0) { return r; }
r = _machine.CompareTo(other._machine);
if (r != 0) { return r; }
r = _pid.CompareTo(other._pid);
if (r != 0) { return r; }
return _increment.CompareTo(other._increment);
}
///
/// Compares this ObjectId to another ObjectId.
///
/// The other ObjectId.
/// True if the two ObjectIds are equal.
public bool Equals(ObjectId rhs)
{
return
_timestamp == rhs._timestamp &&
_machine == rhs._machine &&
_pid == rhs._pid &&
_increment == rhs._increment;
}
///
/// Compares this ObjectId to another object.
///
/// The other object.
/// True if the other object is an ObjectId and equal to this one.
public override bool Equals(object obj)
{
if (obj is ObjectId)
{
return Equals((ObjectId)obj);
}
else
{
return false;
}
}
///
/// Gets the hash code.
///
/// The hash code.
public override int GetHashCode()
{
int hash = 17;
hash = 37 * hash + _timestamp.GetHashCode();
hash = 37 * hash + _machine.GetHashCode();
hash = 37 * hash + _pid.GetHashCode();
hash = 37 * hash + _increment.GetHashCode();
return hash;
}
///
/// Converts the ObjectId to a byte array.
///
/// A byte array.
public byte[] ToByteArray()
{
return Pack(_timestamp, _machine, _pid, _increment);
}
///
/// Returns a string representation of the value.
///
/// A string representation of the value.
public override string ToString()
{
return BsonUtils.ToHexString(Pack(_timestamp, _machine, _pid, _increment));
}
// explicit IConvertible implementation
TypeCode IConvertible.GetTypeCode()
{
return TypeCode.Object;
}
bool IConvertible.ToBoolean(IFormatProvider provider)
{
throw new InvalidCastException();
}
byte IConvertible.ToByte(IFormatProvider provider)
{
throw new InvalidCastException();
}
char IConvertible.ToChar(IFormatProvider provider)
{
throw new InvalidCastException();
}
DateTime IConvertible.ToDateTime(IFormatProvider provider)
{
throw new InvalidCastException();
}
decimal IConvertible.ToDecimal(IFormatProvider provider)
{
throw new InvalidCastException();
}
double IConvertible.ToDouble(IFormatProvider provider)
{
throw new InvalidCastException();
}
short IConvertible.ToInt16(IFormatProvider provider)
{
throw new InvalidCastException();
}
int IConvertible.ToInt32(IFormatProvider provider)
{
throw new InvalidCastException();
}
long IConvertible.ToInt64(IFormatProvider provider)
{
throw new InvalidCastException();
}
sbyte IConvertible.ToSByte(IFormatProvider provider)
{
throw new InvalidCastException();
}
float IConvertible.ToSingle(IFormatProvider provider)
{
throw new InvalidCastException();
}
string IConvertible.ToString(IFormatProvider provider)
{
return ToString();
}
object IConvertible.ToType(Type conversionType, IFormatProvider provider)
{
switch (Type.GetTypeCode(conversionType))
{
case TypeCode.String:
return ((IConvertible)this).ToString(provider);
case TypeCode.Object:
if (conversionType == typeof(object) || conversionType == typeof(ObjectId))
{
return this;
}
if (conversionType == typeof(BsonObjectId))
{
return new BsonObjectId(this);
}
if (conversionType == typeof(BsonString))
{
return new BsonString(((IConvertible)this).ToString(provider));
}
break;
}
throw new InvalidCastException();
}
ushort IConvertible.ToUInt16(IFormatProvider provider)
{
throw new InvalidCastException();
}
uint IConvertible.ToUInt32(IFormatProvider provider)
{
throw new InvalidCastException();
}
ulong IConvertible.ToUInt64(IFormatProvider provider)
{
throw new InvalidCastException();
}
}
}