-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
10 changed files
with
303 additions
and
87 deletions.
There are no files selected for viewing
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
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 | ||
|
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
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 |
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
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); | ||
``` |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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
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 {} | ||
|
||
} |
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
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> |
Oops, something went wrong.