GUID.java
/*
* This file is part of Waarp Project (named also Waarp or GG).
*
* Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
* tags. See the COPYRIGHT.txt in the distribution for a full listing of
* individual contributors.
*
* All Waarp Project 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.
*
* Waarp 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
* Waarp . If not, see <http://www.gnu.org/licenses/>.
*/
package org.waarp.common.guid;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.waarp.common.exception.InvalidArgumentException;
import org.waarp.common.utility.BaseXx;
import org.waarp.common.utility.SingletonUtils;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public class GUID implements Comparable<GUID> {
/**
* ARK header
*/
public static final String ARK = "ark:/";
private static final String ATTEMPTED_TO_PARSE_MALFORMED_ARK_GUID =
"Attempted to parse malformed ARK GUID: ";
/**
* Native size of the GUID
*/
static final int KEYSIZE = 21;
static final int KEYB64SIZE = 28;
static final int KEYB32SIZE = 34;
static final int KEYB16SIZE = KEYSIZE * 2;
static final int HEADER_POS = 0;
static final int HEADER_SIZE = 1;
static final int TENANT_POS = 1;
static final int TENANT_SIZE = 4;
static final int PLATFORM_POS = 5;
static final int PLATFORM_SIZE = 4;
static final int PID_POS = 9;
static final int PID_SIZE = 3;
static final int TIME_POS = 12;
static final int TIME_SIZE = 6;
static final int COUNTER_POS = 18;
static final int COUNTER_SIZE = 3;
/**
* Bits size of Counter
*/
private static final int SIZE_COUNTER = COUNTER_SIZE * 8;
/**
* Min Counter value
*/
private static final int MIN_COUNTER = 0;
/**
* Max Counter value
*/
private static final int MAX_COUNTER = (1 << SIZE_COUNTER) - 1;
/**
* Version to store (to check correctness if future algorithm) between 0 and
* 255
*/
static final int VERSION = 1 & 0xFF;
static final int BYTE_SIZE = 8;
/**
* Counter part
*/
private static final AtomicInteger COUNTER = new AtomicInteger(MIN_COUNTER);
/**
* real GUID
*/
@JsonIgnore
private final byte[] bguid;
static final synchronized int getNewCounter() {
if (COUNTER.compareAndSet(MAX_COUNTER, MIN_COUNTER)) {
return MAX_COUNTER;
} else {
return COUNTER.getAndIncrement();
}
}
/**
* @return the KeySize
*/
public static int getKeySize() {
return KEYSIZE;
}
/**
* Internal constructor
*
* @param size size of the byte representation
*/
GUID(final int size) {
bguid = new byte[size];
}
/**
* Constructor that takes a byte array as this GUID's content
*
* @param bytes GUID content
*
* @throws InvalidArgumentException
*/
public GUID(final byte[] bytes) throws InvalidArgumentException {
this(KEYSIZE);
setBytes(bytes, KEYSIZE);
if (getVersion() != VERSION) {
throw new InvalidArgumentException(
"Version is incorrect: " + getVersion());
}
}
/**
* Build from String key
*
* @param idsource
*
* @throws InvalidArgumentException
*/
public GUID(final String idsource) throws InvalidArgumentException {
this(KEYSIZE);
setString(idsource);
if (getVersion() != VERSION) {
throw new InvalidArgumentException(
"Version is incorrect: " + getVersion());
}
}
/**
* Internal function
*
* @param bytes
* @param size size of the byte representation
*
* @return this
*
* @throws InvalidArgumentException
*/
@JsonIgnore
GUID setBytes(final byte[] bytes, final int size)
throws InvalidArgumentException {
if (bytes == null) {
throw new InvalidArgumentException("Empty argument");
}
if (bytes.length != size) {
throw new InvalidArgumentException(
"Attempted to parse malformed GUID: (" + bytes.length + ") " +
Arrays.toString(bytes));
}
System.arraycopy(bytes, 0, bguid, 0, size);
return this;
}
/**
* Constructor that generates a new GUID using the current process id,
* Platform Id and timestamp with no tenant
*/
public GUID() {
this(0, JvmProcessId.macAddressAsInt() & 0x7FFFFFFF);
}
/**
* Constructor that generates a new GUID using the current process id and
* timestamp
*
* @param tenantId tenant id between 0 and 2^30-1
* @param platformId platform Id between 0 and 2^31-1
*
* @throws IllegalArgumentException if any of the argument are out
* of range
*/
public GUID(final int tenantId, final int platformId) {
this(KEYSIZE);
if (tenantId < 0 || tenantId > 0x3FFFFFFF) {
throw new IllegalArgumentException(
"DomainId must be between 0 and 2^30-1: " + tenantId);
}
if (platformId < 0 || platformId > 0x7FFFFFFF) {
throw new IllegalArgumentException(
"PlatformId must be between 0 and 2^31-1: " + platformId);
}
// atomically
final long time = System.currentTimeMillis();
final int count = getNewCounter();
// 1 bytes = Version (8)
bguid[HEADER_POS] = (byte) VERSION;
// 4 bytes - 2 bits = Domain (30)
int value = tenantId;
bguid[TENANT_POS + 3] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[TENANT_POS + 2] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[TENANT_POS + 1] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[TENANT_POS] = (byte) (value & 0x3F);
// 4 bytes = -1 bit Platform (31)
value = platformId;
bguid[PLATFORM_POS + 3] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[PLATFORM_POS + 2] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[PLATFORM_POS + 1] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[PLATFORM_POS] = (byte) (value & 0x7F);
// 3 bytes = -2 bits JVMPID (22)
value = JvmProcessId.JVMPID;
bguid[PID_POS + 2] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[PID_POS + 1] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[PID_POS] = (byte) (value & 0xFF);
// 6 bytes = timestamp (so up to 8 925 years after Time 0 so year 10
// 895)
long lvalue = time;
bguid[TIME_POS + 5] = (byte) (lvalue & 0xFF);
lvalue >>>= BYTE_SIZE;
bguid[TIME_POS + 4] = (byte) (lvalue & 0xFF);
lvalue >>>= BYTE_SIZE;
bguid[TIME_POS + 3] = (byte) (lvalue & 0xFF);
lvalue >>>= BYTE_SIZE;
bguid[TIME_POS + 2] = (byte) (lvalue & 0xFF);
lvalue >>>= BYTE_SIZE;
bguid[TIME_POS + 1] = (byte) (lvalue & 0xFF);
lvalue >>>= BYTE_SIZE;
bguid[TIME_POS] = (byte) (lvalue & 0xFF);
// 3 bytes = counter against collision
value = count;
bguid[COUNTER_POS + 2] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[COUNTER_POS + 1] = (byte) (value & 0xFF);
value >>>= BYTE_SIZE;
bguid[COUNTER_POS] = (byte) (value & 0xFF);
}
/**
* @return the Base32 representation (default of toString)
*/
@JsonIgnore
public final String toBase32() {
return BaseXx.getBase32(bguid);
}
/**
* @return the Base64 representation (default of toString)
*/
@JsonIgnore
public final String toBase64() {
return BaseXx.getBase64UrlWithoutPadding(bguid);
}
/**
* @return the Hexadecimal representation
*/
@JsonIgnore
public final String toHex() {
return BaseXx.getBase16(bguid);
}
/**
* @return the Ark representation of this GUID
*/
@JsonIgnore
public final String toArk() {
return new StringBuilder(ARK).append(getTenantId()).append('/')
.append(toArkName()).toString();
}
/**
* @return the String representation of this GUID
*/
@JsonGetter("id")
public final String getId() {
return toString();
}
/**
* Internal function
*
* @param idsource
*
* @return this
*
* @throws InvalidArgumentException
*/
@JsonSetter("id")
GUID setString(final String idsource) throws InvalidArgumentException {
if (idsource == null) {
throw new InvalidArgumentException("Empty argument");
}
final String id = idsource.trim();
if (idsource.startsWith(ARK)) {
String ids = idsource;
ids = ids.substring(ARK.length());
final int separator = ids.indexOf('/');
if (separator <= 0) {
throw new InvalidArgumentException(
ATTEMPTED_TO_PARSE_MALFORMED_ARK_GUID + id);
}
int tenantId;
try {
tenantId = Integer.parseInt(ids.substring(0, separator));
} catch (final NumberFormatException e) {
throw new InvalidArgumentException(
ATTEMPTED_TO_PARSE_MALFORMED_ARK_GUID + id);
}
// BASE32
ids = ids.substring(separator + 1);
final byte[] base32 = BaseXx.getFromBase32(ids);
if (base32.length != KEYSIZE - TENANT_SIZE) {
throw new InvalidArgumentException(
ATTEMPTED_TO_PARSE_MALFORMED_ARK_GUID + id);
}
System.arraycopy(base32, 0, bguid, HEADER_POS, HEADER_SIZE);
// GUID Domain default to 0 (from 0 to 2^30-1)
bguid[TENANT_POS + 3] = (byte) (tenantId & 0xFF);
tenantId >>>= BYTE_SIZE;
bguid[TENANT_POS + 2] = (byte) (tenantId & 0xFF);
tenantId >>>= BYTE_SIZE;
bguid[TENANT_POS + 1] = (byte) (tenantId & 0xFF);
tenantId >>>= BYTE_SIZE;
bguid[TENANT_POS] = (byte) (tenantId & 0x3F);
// BASE32
System.arraycopy(base32, HEADER_SIZE, bguid, PLATFORM_POS,
PLATFORM_SIZE + PID_SIZE + TIME_SIZE + COUNTER_SIZE);
return this;
}
final int len = id.length();
try {
if (len == KEYB16SIZE) {
// HEXA BASE16
System.arraycopy(BaseXx.getFromBase16(idsource), 0, bguid, 0, KEYSIZE);
} else if (len == KEYB32SIZE) {
// BASE32
System.arraycopy(BaseXx.getFromBase32(idsource), 0, bguid, 0, KEYSIZE);
} else if (len == KEYB64SIZE) {
// BASE64
System.arraycopy(BaseXx.getFromBase64UrlWithoutPadding(idsource), 0,
bguid, 0, KEYSIZE);
} else {
throw new InvalidArgumentException(
"Attempted to parse malformed GUID: (" + len + ") " + id);
}
} catch (final IllegalArgumentException e) {
throw new InvalidArgumentException(
"Attempted to parse malformed GUID: " + id, e);
}
return this;
}
/**
* extract version field as a hex char from raw GUID bytes
*
* @return version char
*/
@JsonIgnore
public final int getVersion() {
return bguid[HEADER_POS] & 0xFF;
}
/**
* @return the Tenant Id of GUID from which it belongs to (default being 0)
*/
@JsonIgnore
public final int getTenantId() {
return (bguid[TENANT_POS] & 0x3F) << BYTE_SIZE * 3 |
(bguid[TENANT_POS + 1] & 0xFF) << BYTE_SIZE * 2 |
(bguid[TENANT_POS + 2] & 0xFF) << BYTE_SIZE |
bguid[TENANT_POS + 3] & 0xFF;
}
/**
* Extract Platform id as int. Could be using partial MAC address.
*
* @return the Platform id as int, or -1 for unrecognized format
*/
@JsonIgnore
public final int getPlatformId() {
return (bguid[PLATFORM_POS] & 0x7F) << BYTE_SIZE * 3 |
(bguid[PLATFORM_POS + 1] & 0xFF) << BYTE_SIZE * 2 |
(bguid[PLATFORM_POS + 2] & 0xFF) << BYTE_SIZE |
bguid[PLATFORM_POS + 3] & 0xFF;
}
/**
* Extract Platform id as bytes. Could be using partial MAC address.
*
* @return byte array of GUID fragment, or null for unrecognized format
*/
@JsonIgnore
public final byte[] getMacFragment() {
if (getVersion() != VERSION) {
return SingletonUtils.getSingletonByteArray();
}
final byte[] x = new byte[6];
x[0] = 0;
x[1] = 0;
x[2] = (byte) (bguid[PLATFORM_POS] & 0x7F);
x[3] = bguid[PLATFORM_POS + 1];
x[4] = bguid[PLATFORM_POS + 2];
x[5] = bguid[PLATFORM_POS + 3];
return x;
}
/**
* Extract process id and return as int
*
* @return id of process that generated the GUID, or -1 for unrecognized
* format
*/
@JsonIgnore
public final int getProcessId() {
if (getVersion() != VERSION) {
return -1;
}
return (bguid[PID_POS] & 0xFF) << BYTE_SIZE * 2 |
(bguid[PID_POS + 1] & 0xFF) << BYTE_SIZE | bguid[PID_POS + 2] & 0xFF;
}
/**
* Extract timestamp and return as long
*
* @return millisecond UTC timestamp from generation of the GUID, or -1 for
* unrecognized format
*/
@JsonIgnore
public final long getTimestamp() {
if (getVersion() != VERSION) {
return -1;
}
long time = 0;
for (int i = 0; i < TIME_SIZE; i++) {
time <<= BYTE_SIZE;
time |= bguid[TIME_POS + i] & 0xFF;
}
return time;
}
/**
* @return the associated counter against collision value
*/
@JsonIgnore
public final int getCounter() {
return (bguid[COUNTER_POS] & 0xFF) << BYTE_SIZE * 2 |
(bguid[COUNTER_POS + 1] & 0xFF) << BYTE_SIZE |
bguid[COUNTER_POS + 2] & 0xFF;
}
/**
* @return the Ark Name part of Ark representation
*/
public final String toArkName() {
final byte[] temp = new byte[KEYSIZE - TENANT_SIZE];
System.arraycopy(bguid, HEADER_POS, temp, 0, HEADER_SIZE);
System.arraycopy(bguid, PLATFORM_POS, temp, HEADER_SIZE,
PLATFORM_SIZE + PID_SIZE + TIME_SIZE + COUNTER_SIZE);
return BaseXx.getBase32(temp);
}
@Override
public String toString() {
return toBase32();
}
/**
* copy the uuid of this GUID, so that it can't be changed, and return it
*
* @return raw byte array of GUID
*/
@JsonIgnore
public final byte[] getBytes() {
return Arrays.copyOf(bguid, bguid.length);
}
@Override
@JsonIgnore
public final int hashCode() {
return Arrays.hashCode(bguid);
}
@Override
public final boolean equals(final Object o) {
if (!(o instanceof GUID)) {
return false;
}
return this == o || Arrays.equals(bguid, ((GUID) o).bguid);
}
@Override
public final int compareTo(final GUID guid) {
final int id = getTenantId();
final int id2 = guid.getTenantId();
if (id != id2) {
return id < id2? -1 : 1;
}
final long ts = getTimestamp();
final long ts2 = guid.getTimestamp();
if (ts == ts2) {
final int ct = getCounter();
final int ct2 = guid.getCounter();
if (ct == ct2) {
// then all must be equals, else whatever
return Arrays.equals(this.bguid, guid.getBytes())? 0 : -1;
}
// Cannot be equal
return ct < ct2? -1 : 1;
}
// others as ProcessId or Platform are unimportant in comparison
return ts < ts2? -1 : 1;
}
}