Created
July 5, 2019 19:04
-
-
Save SpenceDiNicolantonio/92ae16dff66aef0d15a70f9cc9322e95 to your computer and use it in GitHub Desktop.
[UUID Generator in Apex] #salesforce #apex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A UUID generator that can construct unique IDs in a variety of formats. | |
* | |
* A UUID is generated immediately upon instantiation of this class. The UUID can be retrieved in its normal form | |
* (e.g. f111b8c5-ca2f-4a1a-8d0d-a8dd5f37c05f) or as a shortened web-safe form (e.g. jp64hwPZ-Lh7vY8INQA7ImQPbQE), which | |
* is constructed by converting the UUID to Base64 and replacing '/' and '+' with '-' and '_', respectively. | |
*/ | |
public class Uuid { | |
private static final String HEX_PREFIX = '0x'; | |
private static final String HEX_ALPHABET = '0123456789abcdef'; | |
private static final String[] HEX_CHARACTERS = HEX_ALPHABET.split(''); | |
public static final Integer UUID_V4_LENGTH = 36; | |
public static final String UUID_V4_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; | |
public static final String UUID_SHORT_REGEX = '[0-9a-zA-Z-_]{27}'; | |
// UUID value in its normal form | |
public final String value { get; private set; } | |
// UUID value in its web-safe short-form | |
public final String shortValue { get; private set; } | |
/* | |
* Constructs a new UUID. | |
*/ | |
public Uuid() { | |
this.value = generate(); | |
this.shortValue = getShortValue(this); | |
} | |
/* | |
* Overrides the toString() method to return the formatted UUID value. | |
* @return Formatted UUID value | |
*/ | |
public override String toString() { | |
return value; | |
} | |
//================================================================================================================== | |
// Private | |
//================================================================================================================== | |
/* | |
* Generates a UUID string according to the UUID v4 spec. | |
* @return A newly generated UUID | |
*/ | |
private String generate() { | |
String hexValue = EncodingUtil.convertToHex(Crypto.generateAesKey(128)); | |
// Version Calculation: (i & 0x0f) | 0x40 | |
// Version Format: Always begins with 4 | |
String versionShiftedHexBits = getShiftedHexBits( | |
hexValue.substring(14, 16), | |
convertHexToInteger('0x0f'), | |
convertHexToInteger('0x40') | |
); | |
// Variant Calculation: (i & 0x3f) | 0x80 | |
// Variant Format: Always begins with 8, 9, A or B | |
String variantShiftedHexBits = getShiftedHexBits( | |
hexValue.substring(18, 20), | |
convertHexToInteger('0x3f'), | |
convertHexToInteger('0x80') | |
); | |
String uuid = hexValue.substring(0, 8) // time-low | |
+ '-' + hexValue.substring(8, 12) // time-mid | |
+ '-' + versionShiftedHexBits + hexValue.substring(14, 16) // time-high-and-version | |
+ '-' + variantShiftedHexBits + hexValue.substring(18, 20) // clock-seq-and-reserved + clock-seq-low | |
+ '-' + hexValue.substring(20); // node | |
return uuid; | |
} | |
private String getShiftedHexBits(String hexSubstring, Integer lowerThreshold, Integer upperThreshold) { | |
Integer shiftedIntegerBits = (convertHexToInteger(hexSubstring) & lowerThreshold) | upperThreshold; | |
return convertIntegerToHex(shiftedIntegerBits); | |
} | |
/* | |
* Converts a given hexadecimal string to an integer value. | |
* @param hexValue {String} Value to be converted | |
* @return Integer equivalent to the given string | |
*/ | |
private Integer convertHexToInteger(String hexValue) { | |
Integer hexBase = HEX_ALPHABET.length(); | |
hexValue = hexValue.toLowerCase(); | |
if (hexValue.startsWith(HEX_PREFIX)) { | |
hexValue = hexValue.substringAfter(HEX_PREFIX); | |
} | |
Integer integerValue = 0; | |
for (String hexCharacter : hexValue.split('')) { | |
Integer hexCharacterIndex = HEX_CHARACTERS.indexOf(hexCharacter); | |
integerValue = hexBase * integerValue + hexCharacterIndex; | |
} | |
return integerValue; | |
} | |
/* | |
* Converts a given integer to a hexadecimal string. | |
* @param integerValue {Integer} Value to be converted | |
* @return Hexadecimal equivalent to the given integer | |
*/ | |
private String convertIntegerToHex(Integer integerValue) { | |
Integer hexBase = HEX_ALPHABET.length(); | |
String hexValue = ''; | |
while (integerValue > 0) { | |
Integer hexCharacterIndex = Math.mod(integerValue, hexBase); | |
hexValue = HEX_CHARACTERS[hexCharacterIndex] + hexValue; | |
integerValue = integerValue / hexBase; | |
} | |
return hexValue; | |
} | |
//================================================================================================================== | |
// Static | |
//================================================================================================================== | |
/* | |
* Determines whether a given string is a valid UUID. | |
* @param uuid {String} A UUID string | |
* @return True if the given string represents a valid UUID; false otherwise | |
*/ | |
public static Boolean validate(String uuid) { | |
// Should not be an empty string | |
if (String.isBlank(uuid)) { | |
return false; | |
} | |
// Should be of appropriate length | |
if (uuid.length() != UUID_V4_LENGTH) { | |
return false; | |
} | |
// Should match UUID regex | |
Pattern uuidPattern = Pattern.compile(UUID_V4_REGEX.toLowerCase()); | |
Matcher uuidMatcher = uuidPattern.matcher(uuid.toLowerCase()); | |
return !!uuidMatcher.matches(); | |
} | |
/* | |
* Generates a hashed and base64-encoded representation of the UUID, with '/' and '+' characters will be replaced by '-' | |
* and '_', respectively, to avoid compatibility issues. | |
* @param A UUID to convert | |
* @return UUID value encoded as a base64 string | |
*/ | |
public static String getShortValue(Uuid uuid) { | |
// Remove hyphens | |
String value = uuid.value.replace('-', ''); | |
// Hash the UUID and convert to a base-64 string and replace '/' and '+' | |
Blob digest = Crypto.generateDigest('SHA1', Blob.valueOf(value)); | |
String encoded = EncodingUtil.base64Encode(digest); | |
// Drop trailing '=' and replace '/' and '+' | |
return encoded.substring(0, encoded.length() - 1) | |
.replace('/', '-') | |
.replace('+', '_'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@IsTest | |
public class UuidTest { | |
private static List<Uuid> getUuids() { | |
List<Uuid> uuids = new List<Uuid>(); | |
for (Integer i = 0; i < 100; i++) { | |
uuids.add(new Uuid()); | |
} | |
return uuids; | |
} | |
@isTest | |
private static void shouldGenerateValidUuids() { | |
Test.startTest(); | |
List<Uuid> uuids = getUuids(); | |
Pattern uuidPattern = Pattern.compile(Uuid.UUID_V4_REGEX); | |
Test.stopTest(); | |
for (Uuid id : uuids) { | |
System.assert(uuidPattern.matcher(id.value).matches(), 'Invalid UUID: ' + id.value); | |
} | |
} | |
@IsTest | |
private static void shouldGenerateShortUuids() { | |
Test.startTest(); | |
List<Uuid> uuids = getUuids(); | |
Pattern uuidPattern = Pattern.compile(Uuid.UUID_SHORT_REGEX); | |
Test.stopTest(); | |
for (Uuid id : uuids) { | |
System.assert(uuidPattern.matcher(id.shortValue).matches(), 'Invalid short UUID: ' + id.shortValue); | |
} | |
} | |
@IsTest | |
private static void shouldValidateUuids() { | |
System.assert(Uuid.validate('73b82667-99bf-4564-82bb-9f95a08ca306')); // Valid | |
System.assert(!Uuid.validate('73b8266799bf456482bb9f95a08ca306')); // Missing hyphens | |
System.assert(!Uuid.validate('73b82667-99bf-4564-82bb-9f95a08ca30')); // Not enough characters | |
System.assert(!Uuid.validate('')); // Empty String | |
} | |
@IsTest | |
private static void stringRepresentationShouldMatchFormattedValue() { | |
Test.startTest(); | |
List<Uuid> uuids = getUuids(); | |
Test.stopTest(); | |
for (Uuid id : uuids) { | |
System.assertEquals(id.value, id.toString()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment