Merge pull request #50 from wimvelzeboer/feature/uuid
Add Uuid and HttpException. Logger now includes Uuid's
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
global virtual class fflib_HttpException extends Exception
|
||||
{
|
||||
protected fflib_HttpProblem problems;
|
||||
|
||||
global fflib_HttpException(Integer statusCode)
|
||||
{
|
||||
this(statusCode, fflib_HttpStatusCode.getTitle(statusCode));
|
||||
}
|
||||
|
||||
global fflib_HttpException(Integer statusCode, String title)
|
||||
{
|
||||
this(statusCode, title, null);
|
||||
}
|
||||
|
||||
global fflib_HttpException(Integer statusCode, String title, String detail)
|
||||
{
|
||||
this.problems =
|
||||
new fflib_HttpProblem()
|
||||
.setStatus(statusCode)
|
||||
.setTitle(title)
|
||||
.setDetail(detail);
|
||||
|
||||
addMessage(
|
||||
String.valueOf(statusCode)
|
||||
+ (String.isNotBlank(title) ? + ' - ' + title : '')
|
||||
+ (String.isNotBlank(detail) ? ', ' + detail : '')
|
||||
);
|
||||
}
|
||||
|
||||
global fflib_HttpException addProblem(Integer statusCode)
|
||||
{
|
||||
return this.addProblem(statusCode, fflib_HttpStatusCode.getTitle(statusCode));
|
||||
}
|
||||
|
||||
global fflib_HttpException addProblem(Integer statusCode, String title)
|
||||
{
|
||||
return this.addProblem(statusCode, title, null);
|
||||
}
|
||||
|
||||
global fflib_HttpException addProblem(Integer statusCode, String title, String detail)
|
||||
{
|
||||
this.problems.addNextProblem()
|
||||
.setStatus(statusCode)
|
||||
.setTitle(title)
|
||||
.setDetail(detail);
|
||||
|
||||
addMessage(
|
||||
String.valueOf(statusCode)
|
||||
+ (String.isNotBlank(title) ? + ' - ' + title : '')
|
||||
+ (String.isNotBlank(detail) ? ': ' + detail : '')
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
global List<fflib_HttpProblemDetail> getProblemDetails()
|
||||
{
|
||||
return this.problems.getProblemDetails();
|
||||
}
|
||||
|
||||
global String getUuid()
|
||||
{
|
||||
return this.problems.getUuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a URI reference that identifies the specific occurrence of the problem.
|
||||
*
|
||||
* @param instance URI reference to the problem location
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpException setInstance(String instance)
|
||||
{
|
||||
this.problems.setInstance(instance);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI reference [RFC3986] that identifies the problem type.
|
||||
* This specification encourages that, when dereferenced,
|
||||
* it provide human-readable documentation for the problem type
|
||||
* (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||
* When this member is not present, its value is assumed to be "about:blank".
|
||||
*
|
||||
* @param type URI to human-readable documentation of the occurred problem
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpException setType(String type)
|
||||
{
|
||||
this.problems.setType(type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns a RFC-7807 Json object
|
||||
*/
|
||||
global String toJson()
|
||||
{
|
||||
return JSON.serialize(this.problems);
|
||||
}
|
||||
|
||||
global String toPrettyJson()
|
||||
{
|
||||
return JSON.serializePretty(this.problems);
|
||||
}
|
||||
|
||||
global Blob toJsonBlob()
|
||||
{
|
||||
return Blob.valueOf(toJson());
|
||||
}
|
||||
|
||||
global void addToRestResponse()
|
||||
{
|
||||
RestResponse response = RestContext.response;
|
||||
if (response == null)
|
||||
{
|
||||
response = new RestResponse();
|
||||
RestContext.response = response;
|
||||
}
|
||||
|
||||
response.statusCode = this.problems.getProblemDetails().get(0)?.status;
|
||||
response.responseBody = this.toJsonBlob();
|
||||
}
|
||||
|
||||
protected void addMessage(String message)
|
||||
{
|
||||
if (this.problems.getProblemDetails().size() > 1)
|
||||
{
|
||||
|
||||
setMessage(getMessage() + ', ' + message);
|
||||
}
|
||||
else
|
||||
{
|
||||
setMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>57.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -0,0 +1,104 @@
|
||||
global virtual class fflib_HttpProblem
|
||||
{
|
||||
protected List<fflib_HttpProblemDetail> errors;
|
||||
|
||||
// A uuid that can only be set once
|
||||
protected final String uuid { get; set; }
|
||||
|
||||
global fflib_HttpProblem()
|
||||
{
|
||||
this.errors = new List<fflib_HttpProblemDetail> {new fflib_HttpProblemDetail()};
|
||||
this.uuid = new fflib_Uuid().getValue();
|
||||
}
|
||||
|
||||
|
||||
global fflib_HttpProblem addNextProblem()
|
||||
{
|
||||
this.errors.add(new fflib_HttpProblemDetail());
|
||||
return this;
|
||||
}
|
||||
|
||||
global List<fflib_HttpProblemDetail> getProblemDetails()
|
||||
{
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
global String getUuid()
|
||||
{
|
||||
return this.uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a human-readable explanation specific to this occurrence of the problem.
|
||||
*
|
||||
* @param message Detailed problem description
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpProblem setDetail(String message)
|
||||
{
|
||||
getLatestProblem().detail = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a URI reference that identifies the specific occurrence of the problem.
|
||||
*
|
||||
* @param instance URI reference to the problem location
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpProblem setInstance(String instance)
|
||||
{
|
||||
getLatestProblem().instance = instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a short, human-readable summary of the problem.
|
||||
*
|
||||
* @param title Short summary of the problem
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpProblem setTitle(String title)
|
||||
{
|
||||
getLatestProblem().title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the URI reference [RFC3986] that identifies the problem type.
|
||||
* This specification encourages that, when dereferenced,
|
||||
* it provide human-readable documentation for the problem type
|
||||
* (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||
* When this member is not present, its value is assumed to be "about:blank".
|
||||
*
|
||||
* @param type URI to human-readable documentation of the occurred problem
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpProblem setType(String type)
|
||||
{
|
||||
getLatestProblem().type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code of the problem.
|
||||
*
|
||||
* @param httpResponseCode A HTTP status code (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
|
||||
*
|
||||
* @return Instance of itself to allow for chaining
|
||||
*/
|
||||
global fflib_HttpProblem setStatus(Integer httpResponseCode)
|
||||
{
|
||||
getLatestProblem().status = httpResponseCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected fflib_HttpProblemDetail getLatestProblem()
|
||||
{
|
||||
return this.errors.get(this.errors.size() - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>57.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* A RRC 7807 structured Problem Detail Object
|
||||
*/
|
||||
global virtual class fflib_HttpProblemDetail
|
||||
{
|
||||
/**
|
||||
* A human-readable explanation specific to this occurrence of the problem.
|
||||
*/
|
||||
global String detail { get; set; }
|
||||
|
||||
/**
|
||||
* A URI reference that identifies the specific occurrence of the problem.
|
||||
* It may or may not yield further information if de-referenced.
|
||||
*/
|
||||
global String instance { get; set; }
|
||||
|
||||
/**
|
||||
* A short, human-readable summary of the problem type.
|
||||
* It SHOULD NOT change from occurrence to occurrence of the
|
||||
* problem, except for purposes of localization (e.g., using
|
||||
* proactive content negotiation; see [RFC7231], Section 3.4).
|
||||
*/
|
||||
global String title { get; set; }
|
||||
|
||||
/**
|
||||
* A URI reference that identifies the problem type.
|
||||
* This specification encourages that, when problem type (e.g., using HTML [W3C.REC-html5-20141028]).
|
||||
*/
|
||||
global String type { get; set; }
|
||||
|
||||
/**
|
||||
* The HTTP status code
|
||||
*/
|
||||
global Integer status { get; set; }
|
||||
|
||||
global fflib_HttpProblemDetail() {}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>57.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -0,0 +1,128 @@
|
||||
public class fflib_HttpStatusCode
|
||||
{
|
||||
// --- 1xx Informational ---
|
||||
public static final Integer INFO_CONTINUE = 100;
|
||||
public static final Integer INFO_SWITCHING_PROTOCOLS = 101;
|
||||
public static final Integer INFO_PROCESSING = 102;
|
||||
|
||||
// --- 2xx Success ---
|
||||
public static final Integer SUCCESS_OK = 200;
|
||||
public static final Integer SUCCESS_CREATED = 201;
|
||||
public static final Integer SUCCESS_ACCEPTED = 202;
|
||||
public static final Integer SUCCESS_NON_AUTHORITATIVE_INFORMATION = 203;
|
||||
public static final Integer SUCCESS_NO_CONTENT = 204;
|
||||
public static final Integer SUCCESS_RESET_CONTENT = 205;
|
||||
public static final Integer SUCCESS_PARTIAL_CONTENT = 206;
|
||||
public static final Integer SUCCESS_MULTI_STATUS = 207;
|
||||
|
||||
// --- 3xx Redirection ---
|
||||
public static final Integer REDIRECT_MULTIPLE_CHOICES = 300;
|
||||
public static final Integer REDIRECT_MOVED_PERMANENTLY = 301;
|
||||
public static final Integer REDIRECT_MOVED_TEMPORARILY = 302;
|
||||
public static final Integer REDIRECT_SEE_OTHER = 303;
|
||||
public static final Integer REDIRECT_NOT_MODIFIED = 304;
|
||||
public static final Integer REDIRECT_USE_PROXY = 305;
|
||||
public static final Integer REDIRECT_TEMPORARY_REDIRECT = 307;
|
||||
|
||||
// --- 4xx Client Error ---
|
||||
public static final Integer CLIENT_ERROR_BAD_REQUEST = 400;
|
||||
public static final Integer CLIENT_ERROR_UNAUTHORIZED = 401;
|
||||
public static final Integer CLIENT_ERROR_PAYMENT_REQUIRED = 402;
|
||||
public static final Integer CLIENT_ERROR_FORBIDDEN = 403;
|
||||
public static final Integer CLIENT_ERROR_NOT_FOUND = 404;
|
||||
public static final Integer CLIENT_ERROR_METHOD_NOT_ALLOWED = 405;
|
||||
public static final Integer CLIENT_ERROR_NOT_ACCEPTABLE = 406;
|
||||
public static final Integer CLIENT_ERROR_PROXY_AUTHENTICATION_REQUIRED = 407;
|
||||
public static final Integer CLIENT_ERROR_REQUEST_TIMEOUT = 408;
|
||||
public static final Integer CLIENT_ERROR_CONFLICT = 409;
|
||||
public static final Integer CLIENT_ERROR_GONE = 410;
|
||||
public static final Integer CLIENT_ERROR_LENGTH_REQUIRED = 411;
|
||||
public static final Integer CLIENT_ERROR_PRECONDITION_FAILED = 412;
|
||||
public static final Integer CLIENT_ERROR_REQUEST_TOO_LONG = 413;
|
||||
public static final Integer CLIENT_ERROR_REQUEST_URI_TOO_LONG = 414;
|
||||
public static final Integer CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE = 415;
|
||||
public static final Integer CLIENT_ERROR_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
|
||||
public static final Integer CLIENT_ERROR_EXPECTATION_FAILED = 417;
|
||||
public static final Integer CLIENT_ERROR_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
|
||||
public static final Integer CLIENT_ERROR_METHOD_FAILURE = 420;
|
||||
public static final Integer CLIENT_ERROR_UNPROCESSABLE_ENTITY = 422;
|
||||
public static final Integer CLIENT_ERROR_LOCKED = 423;
|
||||
public static final Integer CLIENT_ERROR_FAILED_DEPENDENCY = 424;
|
||||
|
||||
// --- 5xx Server Error ---
|
||||
public static final Integer SERVER_ERROR_INTERNAL_SERVER_ERROR = 500;
|
||||
public static final Integer SERVER_ERROR_NOT_IMPLEMENTED = 501;
|
||||
public static final Integer SERVER_ERROR_BAD_GATEWAY = 502;
|
||||
public static final Integer SERVER_ERROR_SERVICE_UNAVAILABLE = 503;
|
||||
public static final Integer SERVER_ERROR_GATEWAY_TIMEOUT = 504;
|
||||
public static final Integer SERVER_ERROR_HTTP_VERSION_NOT_SUPPORTED = 505;
|
||||
public static final Integer SERVER_ERROR_INSUFFICIENT_STORAGE = 507;
|
||||
|
||||
public static final Map<Integer, String> MESSAGE_BY_STATUS_CODE = new Map<Integer, String>
|
||||
{
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing',
|
||||
|
||||
200 => 'Ok',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non authoritative information',
|
||||
204 => 'No content',
|
||||
205 => 'Reset content',
|
||||
206 => 'Partial content',
|
||||
207 => 'Multi status',
|
||||
|
||||
300 => 'Multiple choices',
|
||||
301 => 'Moved permanently',
|
||||
302 => 'Moved temporarily',
|
||||
303 => 'See other',
|
||||
304 => 'Not modified',
|
||||
305 => 'Use proxy',
|
||||
307 => 'Temporary redirect',
|
||||
|
||||
400 => 'Bad request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not found',
|
||||
405 => 'Method not allowed',
|
||||
406 => 'Not acceptable',
|
||||
407 => 'Proxy authentication required',
|
||||
408 => 'Request timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length required',
|
||||
412 => 'Precondition failed',
|
||||
413 => 'Request too long',
|
||||
414 => 'Request uri too long',
|
||||
415 => 'Unsupported media type',
|
||||
416 => 'Requested range not satisfiable',
|
||||
417 => 'Expectation failed',
|
||||
419 => 'Insufficient space on resource',
|
||||
420 => 'Method failure',
|
||||
422 => 'Unprocessable entity',
|
||||
423 => 'Locked',
|
||||
424 => 'Failed dependency',
|
||||
|
||||
500 => 'Internal server error',
|
||||
501 => 'Not implemented',
|
||||
502 => 'Bad gateway',
|
||||
503 => 'Service unavailable',
|
||||
504 => 'Gateway timeout',
|
||||
505 => 'Http version not supported',
|
||||
507 => 'Insufficient storage'
|
||||
};
|
||||
|
||||
public static String getTitle(Integer statusCode)
|
||||
{
|
||||
if (MESSAGE_BY_STATUS_CODE.containsKey(statusCode))
|
||||
{
|
||||
return MESSAGE_BY_STATUS_CODE.get(statusCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>57.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -0,0 +1,166 @@
|
||||
/******************************************************************************************
|
||||
* 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. *
|
||||
******************************************************************************************/
|
||||
/**
|
||||
* Changes made:
|
||||
* - Renamed class into fflib_Uuid
|
||||
* - Renamed some parameters to resolve dictionary warnings
|
||||
* - Added the fflib Apex styling
|
||||
*/
|
||||
public without sharing class fflib_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}';
|
||||
|
||||
private static final String ERROR_MESSAGE_INVALID_UUID = '{0} is not a valid UUID value';
|
||||
|
||||
public static String formatValue(String unformattedValue)
|
||||
{
|
||||
// Remove any non-alphanumeric characters
|
||||
String nonformattedValue = unformattedValue.replaceAll('[^a-zA-Z0-9]', '');
|
||||
|
||||
// If the nonformatted 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 (nonformattedValue.length() != (UUID_V4_LENGTH - 4))
|
||||
{
|
||||
throw new UuidException(String.format(ERROR_MESSAGE_INVALID_UUID, new List<String> {unformattedValue}));
|
||||
}
|
||||
|
||||
// UUID Pattern: 8-4-4-4-12
|
||||
String formattedValue =
|
||||
nonformattedValue.substring(0, 8) +
|
||||
'-' +
|
||||
nonformattedValue.substring(8, 12) +
|
||||
'-' +
|
||||
nonformattedValue.substring(12, 16) +
|
||||
'-' +
|
||||
nonformattedValue.substring(16, 20) +
|
||||
'-' +
|
||||
nonformattedValue.substring(20);
|
||||
|
||||
formattedValue = formattedValue.toLowerCase();
|
||||
|
||||
if (!fflib_Uuid.isValid(formattedValue))
|
||||
{
|
||||
throw new UuidException(String.format(ERROR_MESSAGE_INVALID_UUID, new List<String> {nonformattedValue}));
|
||||
}
|
||||
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
public static Boolean isValid(String uuidValue)
|
||||
{
|
||||
if (String.isBlank(uuidValue) || 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 fflib_Uuid valueOf(String uuidValue)
|
||||
{
|
||||
return new fflib_Uuid(uuidValue);
|
||||
}
|
||||
|
||||
private final String value;
|
||||
|
||||
public fflib_Uuid() {
|
||||
this.value = this.generateValue();
|
||||
}
|
||||
|
||||
private fflib_Uuid(String uuidValue) {
|
||||
this.value = fflib_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 fflib_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 {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>58.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -29,6 +29,7 @@
|
||||
*/
|
||||
public with sharing class fflib_LogEntry
|
||||
{
|
||||
public String uuid { get; set; }
|
||||
public String className { get; set; }
|
||||
public String executionContext { get; set; }
|
||||
public Decimal lineNumber { get; set; }
|
||||
@@ -61,6 +62,7 @@ public with sharing class fflib_LogEntry
|
||||
this.methodName = logEvent.MethodName__c;
|
||||
this.userId = logEvent.CreatedById;
|
||||
this.createdDatetime = logEvent.CreatedDate;
|
||||
this.uuid = String.isBlank(logEvent.Uuid__c) ? logEvent.EventUuid : logEvent.Uuid__c;
|
||||
}
|
||||
|
||||
public fflib_LogEvent__e toPlatformEvent()
|
||||
@@ -71,7 +73,8 @@ public with sharing class fflib_LogEntry
|
||||
LineNumber__c = this.lineNumber,
|
||||
LogLevel__c = this.logLevel.name(),
|
||||
Message__c = this.message,
|
||||
MethodName__c = this.methodName
|
||||
MethodName__c = this.methodName,
|
||||
Uuid__c = this.uuid
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,12 @@ public with sharing class fflib_Logger implements fflib_ILogger
|
||||
|
||||
public void add(LoggingLevel loggingLevel, Exception e)
|
||||
{
|
||||
add(new fflib_LogEntry(loggingLevel, messageFormatter.format(e)));
|
||||
fflib_LogEntry logEntry = new fflib_LogEntry(loggingLevel, messageFormatter.format(e));
|
||||
if (e instanceof fflib_HttpException)
|
||||
{
|
||||
logEntry.uuid = ((fflib_HttpException) e).getUuid();
|
||||
}
|
||||
add(logEntry);
|
||||
}
|
||||
|
||||
public void add(LoggingLevel loggingLevel, String message)
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Uuid__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<isFilteringDisabled>false</isFilteringDisabled>
|
||||
<isNameField>false</isNameField>
|
||||
<isSortingDisabled>false</isSortingDisabled>
|
||||
<label>UUID</label>
|
||||
<description>A universally unique identifier (UUID) for the error. Its useful to expose this UUID in the UI/API when errors occur so that the log entry for that issue can easily be found</description>
|
||||
<inlineHelpText>A universally unique identifier (UUID) for the error</inlineHelpText>
|
||||
<length>36</length>
|
||||
<required>false</required>
|
||||
<type>Text</type>
|
||||
<unique>false</unique>
|
||||
</CustomField>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<fullName>Uuid__c</fullName>
|
||||
<externalId>false</externalId>
|
||||
<isFilteringDisabled>false</isFilteringDisabled>
|
||||
<isNameField>false</isNameField>
|
||||
<isSortingDisabled>false</isSortingDisabled>
|
||||
<label>UUID</label>
|
||||
<description>A universally unique identifier (UUID) for the error. Its useful to expose this UUID in the UI/API when errors occur so that the log entry for that issue can easily be found</description>
|
||||
<inlineHelpText>A universally unique identifier (UUID) for the error</inlineHelpText>
|
||||
<length>36</length>
|
||||
<required>false</required>
|
||||
<type>Text</type>
|
||||
<unique>false</unique>
|
||||
</CustomField>
|
||||
@@ -0,0 +1,180 @@
|
||||
@IsTest
|
||||
private class fflib_HttpExceptionTest
|
||||
{
|
||||
@IsTest
|
||||
static void itShouldThrowExceptionWithOnlyHttpSResponseStatusCode()
|
||||
{
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
|
||||
final fflib_HttpProblemDetail problem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
|
||||
System.Assert.areEqual(
|
||||
fflib_HttpStatusCode.getTitle(400),
|
||||
problem.title,
|
||||
'Incorrect returned http response status title');
|
||||
System.Assert.isNull(problem.detail, 'Expected problem detail to be blank');
|
||||
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
|
||||
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + fflib_HttpStatusCode.getTitle(400),
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
System.Assert.isTrue(fflib_Uuid.isValid(e.getUuid()), 'Invalid Uuid');
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void itShouldThrowExceptionWithStatusCodeAndCustomTitle()
|
||||
{
|
||||
final String myTitle = 'My Error message Title';
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
|
||||
final fflib_HttpProblemDetail problem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
|
||||
System.Assert.areEqual(
|
||||
myTitle,
|
||||
problem.title,
|
||||
'Incorrect returned http response status title');
|
||||
System.Assert.isNull(problem.detail, 'Expected problem detail to be blank');
|
||||
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
|
||||
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + myTitle,
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void itShouldThrowExceptionWithStatusCodeAndCustomTitleAndDetail()
|
||||
{
|
||||
final String myTitle = 'My Error message Title';
|
||||
final String myDetail = 'Very lengthy description of the error';
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle, myDetail);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
|
||||
final fflib_HttpProblemDetail problem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
|
||||
System.Assert.areEqual(
|
||||
myTitle,
|
||||
problem.title,
|
||||
'Incorrect returned http response status title');
|
||||
System.Assert.areEqual(
|
||||
myDetail,
|
||||
problem.detail,
|
||||
'Incorrect returned problem detail');
|
||||
System.Assert.isNull(problem.instance, 'Expected problem instance to be blank');
|
||||
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + myTitle + ', ' + myDetail,
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void itShouldThrowExceptionWithInstance()
|
||||
{
|
||||
final String instanceUri = '/somewhere/over/the/rainbow';
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST)
|
||||
.setInstance(instanceUri);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
|
||||
final fflib_HttpProblemDetail problem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
|
||||
System.Assert.areEqual(
|
||||
fflib_HttpStatusCode.getTitle(400),
|
||||
problem.title,
|
||||
'Incorrect returned http response status title');
|
||||
System.Assert.isNull(problem.detail, 'Expected problem instance to be blank');
|
||||
System.Assert.areEqual(instanceUri, problem.instance, 'Incorrect returned problem instance');
|
||||
System.Assert.isNull(problem.type, 'Expected problem type to be blank');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + fflib_HttpStatusCode.getTitle(400),
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void itShouldThrowException()
|
||||
{
|
||||
final String myTitle = 'My Error message Title';
|
||||
final String myDetail = 'Very lengthy description of the error';
|
||||
final String instanceUri = '/somewhere/over/the/rainbow';
|
||||
final String typeUri = '/somewhere/very/knowledgeable';
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST, myTitle, myDetail)
|
||||
.setInstance(instanceUri)
|
||||
.setType(typeUri);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
final List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(1, problemDetails.size(), 'Expected only one problem');
|
||||
final fflib_HttpProblemDetail problem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, problem.status, 'Incorrect returned http response status code');
|
||||
System.Assert.areEqual(myTitle, problem.title, 'Incorrect returned http response status title');
|
||||
System.Assert.areEqual(myDetail, problem.detail, 'Incorrect returned problem detail');
|
||||
System.Assert.areEqual(instanceUri, problem.instance, 'Incorrect returned problem instance');
|
||||
System.Assert.areEqual(typeUri, problem.type, 'Incorrect returned problem type');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + myTitle + ', ' + myDetail,
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void itShouldThrowExceptionWithMultipleProblems()
|
||||
{
|
||||
try
|
||||
{
|
||||
throw new fflib_HttpException(fflib_HttpStatusCode.CLIENT_ERROR_BAD_REQUEST)
|
||||
.addProblem(fflib_HttpStatusCode.CLIENT_ERROR_METHOD_NOT_ALLOWED);
|
||||
}
|
||||
catch (fflib_HttpException e)
|
||||
{
|
||||
List<fflib_HttpProblemDetail> problemDetails = e.getProblemDetails();
|
||||
System.Assert.areEqual(2, problemDetails.size(), 'Expected only one firstProblem');
|
||||
final fflib_HttpProblemDetail firstProblem = problemDetails.get(0);
|
||||
System.Assert.areEqual(400, firstProblem.status, 'Incorrect returned http response status code for first problem');
|
||||
final fflib_HttpProblemDetail secondProblem = problemDetails.get(1);
|
||||
System.Assert.areEqual(405, secondProblem.status, 'Incorrect returned http response status code for second problem');
|
||||
System.Assert.areEqual(
|
||||
'400 - ' + fflib_HttpStatusCode.getTitle(400) +
|
||||
', 405 - ' + fflib_HttpStatusCode.getTitle(405),
|
||||
e.getMessage(),
|
||||
'Incorrect returned exception message'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>57.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
@@ -41,7 +41,7 @@ private class fflib_LoggerTest
|
||||
{
|
||||
'LogTestMockException:[]:',
|
||||
ERROR_MESSAGE,
|
||||
'()'
|
||||
'Class.fflib_LoggerTest.itShouldAppendDebugException:'
|
||||
}
|
||||
);
|
||||
private static final String ERROR_MESSAGE_FORMATTED_WITH_ARGS =
|
||||
@@ -83,9 +83,6 @@ private class fflib_LoggerTest
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* todo : For some reason this method fails when run by Jenkins deployment user during a build, need to investigate why and re-enable it
|
||||
|
||||
@IsTest
|
||||
static void itShouldAppendDebugException()
|
||||
{
|
||||
@@ -97,13 +94,30 @@ private class fflib_LoggerTest
|
||||
log.debug(e);
|
||||
|
||||
// THEN - It should be appended to the logAppender
|
||||
System.Assert.isTrue(
|
||||
APPENDER_MOCK
|
||||
.getAppendedMessages(System.LoggingLevel.DEBUG)
|
||||
.contains(ERROR_MESSAGE_FORMATTED_WITH_EXCEPTION)
|
||||
);
|
||||
final List<String> result = APPENDER_MOCK
|
||||
.getAppendedMessages(System.LoggingLevel.DEBUG);
|
||||
System.debug(result);
|
||||
System.Assert.areEqual(1, result.size(), 'Expected only one error');
|
||||
System.Assert.isTrue(result.get(0).startsWith(ERROR_MESSAGE_FORMATTED_WITH_EXCEPTION), 'Incorrect returned error message');
|
||||
}
|
||||
*/
|
||||
|
||||
@IsTest
|
||||
static void itShouldAppendDebugExceptionWithUuid()
|
||||
{
|
||||
// GIVEN - An error message and a configured logger
|
||||
fflib_ILogger log = generatedLoggerWithMockAppender();
|
||||
final fflib_HttpException e = new fflib_HttpException(400);
|
||||
final String uuid = e.getUuid();
|
||||
|
||||
// WHEN - When the error is logged
|
||||
log.debug(e);
|
||||
|
||||
// THEN - It should be appended to the logAppender
|
||||
final List<fflib_LogEntry> result = APPENDER_MOCK.getEntries();
|
||||
System.Assert.areEqual(1, result.size(), 'Expected only one log entry');
|
||||
System.Assert.areEqual(uuid, result.get(0).uuid, 'Incorrect UUID on the LogEntry');
|
||||
}
|
||||
|
||||
|
||||
@IsTest
|
||||
static void itShouldAppendDebugMessageWithArguments()
|
||||
@@ -255,6 +269,12 @@ private class fflib_LoggerTest
|
||||
{
|
||||
this.appendedEntries.add(logEntry);
|
||||
}
|
||||
|
||||
public List<fflib_LogEntry> getEntries()
|
||||
{
|
||||
return this.appendedEntries;
|
||||
}
|
||||
|
||||
public List<String> getAppendedMessages(System.LoggingLevel loggingLevel)
|
||||
{
|
||||
List<String> result = new List<String>();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
@IsTest
|
||||
private class fflib_UuidTest
|
||||
{
|
||||
|
||||
@IsTest
|
||||
static void it_should_create_several_valid_uuids()
|
||||
{
|
||||
String generatedUuid = new fflib_Uuid().getValue();
|
||||
System.assertEquals(36, generatedUuid.length());
|
||||
|
||||
Pattern pattern = Pattern.compile(
|
||||
'[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
||||
);
|
||||
|
||||
for (Integer i = 0; i < 10; i++)
|
||||
{
|
||||
fflib_Uuid uuid = new fflib_Uuid();
|
||||
Matcher matcher = pattern.matcher(uuid.getValue());
|
||||
System.assert(matcher.matches(), 'Generated UUID=' + uuid.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_reuse_a_uuid_on_subsequent_calls()
|
||||
{
|
||||
fflib_Uuid uuid = new fflib_Uuid();
|
||||
String originalValue = uuid.getValue();
|
||||
|
||||
for (Integer i = 0; i < 5; i++)
|
||||
{
|
||||
System.assertEquals(originalValue, uuid.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_verify_that_a_uuid_is_a_uuid()
|
||||
{
|
||||
String generatedUuid = new fflib_Uuid().getValue();
|
||||
System.assert(fflib_Uuid.isValid(generatedUuid));
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_not_consider_a_blank_string_a_uuid()
|
||||
{
|
||||
System.assertEquals(false, fflib_Uuid.isValid(''));
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_not_consider_null_a_uuid()
|
||||
{
|
||||
System.assertEquals(false, fflib_Uuid.isValid(null));
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_validate_a_uuid_in_upper_case()
|
||||
{
|
||||
String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toUpperCase();
|
||||
System.assertEquals(true, fflib_Uuid.isValid(exampleUuid));
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_validate_a_uuid_in_lower_case()
|
||||
{
|
||||
String exampleUuid = 'f3665813-1a60-4924-ad9b-23a9cef17d80'.toLowerCase();
|
||||
System.assertEquals(true, fflib_Uuid.isValid(exampleUuid));
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_convert_a_valid_string_to_a_uuid()
|
||||
{
|
||||
String uuidValue = new fflib_Uuid().getValue();
|
||||
|
||||
Test.startTest();
|
||||
fflib_Uuid convertedUuid = fflib_Uuid.valueOf(uuidValue);
|
||||
Test.stopTest();
|
||||
|
||||
System.assertEquals(uuidValue, convertedUuid.getValue());
|
||||
}
|
||||
|
||||
@IsTest
|
||||
static void it_should_not_convert_an_invalid_string_to_a_uuid()
|
||||
{
|
||||
String invalidUuidValue = 'this-is-not-a-valid-uuid';
|
||||
|
||||
Test.startTest();
|
||||
try
|
||||
{
|
||||
fflib_Uuid.valueOf(invalidUuidValue);
|
||||
System.assert(false, 'Error expected here');
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
String expectedError = invalidUuidValue + ' is not a valid UUID value';
|
||||
System.Assert.areEqual(expectedError, ex.getMessage());
|
||||
}
|
||||
Test.stopTest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||
<apiVersion>58.0</apiVersion>
|
||||
<status>Active</status>
|
||||
</ApexClass>
|
||||
Reference in New Issue
Block a user