Skip to content

Commit

Permalink
Rewrite for UUID v4
Browse files Browse the repository at this point in the history
1. Completely rewrote UUID generation to comply with UUID v4 standard
2. Renamed project & class from Guid to Uuid (since UUID is the true standard)
3. Added package.xml and Travis CI builds
4. Rewrote README.md to include use cases & code samples
  • Loading branch information
jongpie committed Jul 26, 2018
1 parent c4768ee commit 894d06c
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 87 deletions.
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#Folders to exclude
.settings/
config/
debug/
deploy/

#Files to exclude
*.log
*.sublime-project
*.sublime-workspace
*.sublime-settings

7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: node_js
node_js:
- "7"
install:
- npm install -g jsforce-metadata-tools
script:
- jsforce-deploy --checkOnly -u $DEPLOYMENT_USERNAME -p $DEPLOYMENT_PASSWORD$DEPLOYMENT_TOKEN -D $TRAVIS_BUILD_DIR/src -l $DEPLOYMENT_LOGIN_URL --rollbackOnError true --testLevel $DEPLOYMENT_TEST_LEVEL --pollTimeout $POLL_TIMEOUT --pollInterval $POLL_INTERVAL--verbose
81 changes: 70 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,75 @@
# Apex GUID
<a href="https://githubsfdeploy.herokuapp.com?owner=jongpie&repo=apexguid">
<img alt="Deploy to Salesforce"
src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png">
# Apex UUID
[![Build Status](https://travis-ci.org/jongpie/ApexUuid.svg?branch=master)](https://travis-ci.org/jongpie/ApexUuid)

<a href="https://githubsfdeploy.herokuapp.com" target="_blank">
<img alt="Deploy to Salesforce" src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/deploy.png">
</a>
## Issue
Often times, we need be able to refer to related records before they have been inserted. Salesforce does not provide a way to generate a unique ID in Apex, such as as GUID/UUID. This repo aims to solve that.

## Goal
* Create a solution that allows developers to generate a unique ID in code that can be used as an external ID text field
## Usage
Provides a way to generate a [UUID (Universally Unique Identifier)](https://en.wikipedia.org/wiki/Universally_unique_identifier) in Salesforce's Apex language. This uses Verion 4 of the UUID standard - more details available [here](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random))

### Generating & using a UUID
To generate a UUID, simply instantiate a new instance. The string value can be retrieved with `getValue()`
```
Uuid myUuid = new Uuid();
String myUuidValue = myUuid.getValue();
```

You can use the UUID value as a unique ID, generated by Apex. This lets you do some powerful things in Apex when your objects have external ID fields to store the UUID value.

It's best if you create a custom field on your object to store the UUID value with these properties: Text(36) (External ID) (Unique Case Insensitive).

In the code samples below, the field name Uuid__c is used as an example field name.

> ##### Example: Using a code-generated external ID (such as a UUID), we can create multiple accounts and related contacts with only 1 DML statement
```
List<Sobject> recordsToCreate = new List<Sobject>();
// Create 10 sample accounts
for(Integer accountCount = 0; accountCount < 10; accountCount++) {
// Create a new account & set a custom external ID text field called Uuid__c
Account newAccount = new Account(
Name = 'Account ' + accountCount,
Uuid__c = new Uuid().getValue()
);
recordsToCreate.add(newAccount);
// For each sample account, create 10 sample contacts
for(Integer contactCount = 0; contactCount < 10; contactCount++) {
// Instead of setting contact.AccountId with a Salesforce ID...
// we can use an Account object with a Uuid__c value to set the Contact-Account relationship
Contact newContact = new Contact(
Account = new Account(Uuid__c = newAccount.Uuid__c),
LastName = 'Contact ' + contactCount
);
recordsToCreate.add(newContact);
}
}
// Sort so that the accounts are created before contacts (accounts are the parent object)
recordsToCreate.sort();
insert recordsToCreate;
// Verify that we only used 1 DML statement
System.assertEquals(1, Limits.getDmlStatements());
```

### Using a UUID's string value
If you already have a UUID as a string (previously generated in Apex, generated by an external system, etc), there are 3 static methods to help work with the string value.
1. **Do I have a valid UUID value?**

## Implementation
--Coming soon--
This checks if the string value matches the regex for a UUID v4, including hyphens (but it is case-insensitive)
```
Boolean isValid = Uuid.isValid(myUuidValue);
```
2. **I have a UUID value but need to format it**

### Example Implementation: --Coming soon--
This returns a formatted string that follows the UUID pattern 8-4-4-4-12 in lowercase. If an invalid string is provided, a UuidException is thrown.
```
String formattedUuidValue = Uuid.formatValue(myUnformattedUuidValue);
```
3. **I have a UUID value, how can I use it to construct a UUID?**

This will automatically format the value for you, but the intial value must be a valid (unformatted) UUID string
```
Uuid myUuid = Uuid.valueOf(myUuidValue);
```
27 changes: 0 additions & 27 deletions src/classes/Guid.cls

This file was deleted.

47 changes: 0 additions & 47 deletions src/classes/Guid_Tests.cls

This file was deleted.

118 changes: 118 additions & 0 deletions src/classes/Uuid.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/******************************************************************************************
* This file is part of the Apex UUID project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/ApexUuid for full license details. *
******************************************************************************************/
public without sharing class Uuid {

private static final Integer HEX_BASE = HEX_CHARACTERS.length();
private static final String HEX_CHARACTERS = '0123456789abcdef';
private static final String HEX_PREFIX = '0x';
private static final List<String> HEX_CHARACTER_LIST = HEX_CHARACTERS.split('');
private static final Integer UUID_V4_LENGTH = 36;
private 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 String formatValue(String unformattedValue) {
final String invalidValueError = unformattedValue + ' is not a valid UUID value';

// Remove any non-alphanumeric characters
unformattedValue = unformattedValue.replaceAll('[^a-zA-Z0-9]', '');

// If the unformatted value isn't even the right length to be valid, then throw an exception
// Subtract 4 because the UUID_V4_LENGTH includes 4 '-' characters in the UUID pattern
if(unformattedValue.length() != (UUID_V4_LENGTH - 4)) throw new UuidException(invalidValueError);

// UUID Pattern: 8-4-4-4-12
String formattedValue = unformattedValue.substring(0, 8)
+ '-' + unformattedValue.substring(8, 12)
+ '-' + unformattedValue.substring(12, 16)
+ '-' + unformattedValue.substring(16, 20)
+ '-' + unformattedValue.substring(20);

formattedValue = formattedValue.toLowerCase();

if(!Uuid.isValid(formattedValue)) throw new UuidException(invalidValueError);

return formattedValue;
}

public static Boolean isValid(String uuidValue) {
if(String.isBlank(uuidValue)) return false;
if(uuidValue.length() != UUID_V4_LENGTH) return false;

Pattern uuidPattern = Pattern.compile(UUID_V4_REGEX.toLowerCase());
Matcher uuidMatcher = uuidPattern.matcher(uuidValue.toLowerCase());

return uuidMatcher.matches();
}

public static Uuid valueOf(String uuidValue) {
return new Uuid(uuidValue);
}

private final String value;

public Uuid() {
this.value = this.generateValue();
}

private Uuid(String uuidValue) {
this.value = Uuid.formatValue(uuidValue);
}

public String getValue() {
return this.value;
}

private String generateValue() {
String hexValue = EncodingUtil.convertToHex(Crypto.generateAesKey(128));

// Version Calculation: (i & 0x0f) | 0x40
// Version Format: Always begins with 4
String versionShiftedHexBits = this.getShiftedHexBits(hexValue.substring(14, 16), this.convertHexToInteger('0x0f'), this.convertHexToInteger('0x40'));

// Variant Calculation: (i & 0x3f) | 0x80
// Variant Format: Always begins with 8, 9, A or B
String variantShiftedHexBits = this.getShiftedHexBits(hexValue.substring(18, 20), this.convertHexToInteger('0x3f'), this.convertHexToInteger('0x80'));

String uuidValue = 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.formatValue(uuidValue);
}

private String getShiftedHexBits(String hexSubstring, Integer lowerThreshold, Integer upperThreshold) {
Integer shiftedIntegerBits = (this.convertHexToInteger(hexSubstring) & lowerThreshold) | upperThreshold;
return this.convertIntegerToHex(shiftedIntegerBits);
}

private Integer convertHexToInteger(String hexValue) {
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 = HEX_BASE * integerValue + hexCharacterIndex;
}
return integerValue;
}

private String convertIntegerToHex(Integer integerValue) {
String hexValue = '';
while(integerValue > 0) {
Integer hexCharacterIndex = Math.mod(integerValue, HEX_BASE);

hexValue = HEX_CHARACTER_LIST[hexCharacterIndex] + hexValue;
integerValue = integerValue / HEX_BASE;
}
return hexValue;
}

private class UuidException extends Exception {}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>38.0</apiVersion>
<apiVersion>43.0</apiVersion>
<status>Active</status>
</ApexClass>

0 comments on commit 894d06c

Please sign in to comment.