From 752573c3cd7eb860b7e47824cdb3bde10f359152 Mon Sep 17 00:00:00 2001 From: WimVelzeboer Date: Tue, 1 Aug 2023 11:25:55 +0100 Subject: [PATCH] Add Uuid and HttpException. Logger now includes Uuid's Signed-off-by: WimVelzeboer --- .../classes/http/fflib_HttpException.cls | 138 ++++++++++++++ .../http/fflib_HttpException.cls-meta.xml | 5 + .../classes/http/fflib_HttpProblem.cls | 104 ++++++++++ .../http/fflib_HttpProblem.cls-meta.xml | 5 + .../classes/http/fflib_HttpProblemDetail.cls | 37 ++++ .../http/fflib_HttpProblemDetail.cls-meta.xml | 5 + .../classes/http/fflib_HttpStatusCode.cls | 128 +++++++++++++ .../http/fflib_HttpStatusCode.cls-meta.xml | 5 + .../main/default/classes/uuid/fflib_Uuid.cls | 166 ++++++++++++++++ .../classes/uuid/fflib_Uuid.cls-meta.xml | 5 + .../main/logger/classes/fflib_LogEntry.cls | 5 +- .../main/logger/classes/fflib_Logger.cls | 7 +- .../fields/Uuid__c.field-meta.xml | 15 ++ .../fields/Uuid__c.field-meta.xml | 15 ++ .../classes/http/fflib_HttpExceptionTest.cls | 180 ++++++++++++++++++ .../http/fflib_HttpExceptionTest.cls-meta.xml | 5 + .../tests/classes/logger/fflib_LoggerTest.cls | 40 +++- .../tests/classes/uuid/fflib_UuidTest.cls | 98 ++++++++++ .../classes/uuid/fflib_UuidTest.cls-meta.xml | 5 + 19 files changed, 956 insertions(+), 12 deletions(-) create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls create mode 100644 sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls create mode 100644 sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/main/logger/objects/fflib_LogEntry__c/fields/Uuid__c.field-meta.xml create mode 100644 sfdx-source/apex-extensions/main/logger/objects/fflib_LogEvent__e/fields/Uuid__c.field-meta.xml create mode 100644 sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls create mode 100644 sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls-meta.xml create mode 100644 sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls create mode 100644 sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls-meta.xml diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls new file mode 100644 index 0000000..ebea6c5 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls @@ -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 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); + } + } +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls-meta.xml new file mode 100644 index 0000000..754ecb1 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpException.cls-meta.xml @@ -0,0 +1,5 @@ + + + 57.0 + Active + diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls new file mode 100644 index 0000000..c9e2ba6 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls @@ -0,0 +1,104 @@ +global virtual class fflib_HttpProblem +{ + protected List errors; + + // A uuid that can only be set once + protected final String uuid { get; set; } + + global fflib_HttpProblem() + { + this.errors = new List {new fflib_HttpProblemDetail()}; + this.uuid = new fflib_Uuid().getValue(); + } + + + global fflib_HttpProblem addNextProblem() + { + this.errors.add(new fflib_HttpProblemDetail()); + return this; + } + + global List 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); + } +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls-meta.xml new file mode 100644 index 0000000..754ecb1 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblem.cls-meta.xml @@ -0,0 +1,5 @@ + + + 57.0 + Active + diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls new file mode 100644 index 0000000..7b4484c --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls @@ -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() {} +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls-meta.xml new file mode 100644 index 0000000..754ecb1 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpProblemDetail.cls-meta.xml @@ -0,0 +1,5 @@ + + + 57.0 + Active + diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls new file mode 100644 index 0000000..a10dcc7 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls @@ -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 MESSAGE_BY_STATUS_CODE = new Map + { + 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; + } + } +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls-meta.xml new file mode 100644 index 0000000..754ecb1 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/http/fflib_HttpStatusCode.cls-meta.xml @@ -0,0 +1,5 @@ + + + 57.0 + Active + diff --git a/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls b/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls new file mode 100644 index 0000000..75c1d05 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls @@ -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 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 {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 {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 {} +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls-meta.xml b/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls-meta.xml new file mode 100644 index 0000000..7a51829 --- /dev/null +++ b/sfdx-source/apex-extensions/main/default/classes/uuid/fflib_Uuid.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active + diff --git a/sfdx-source/apex-extensions/main/logger/classes/fflib_LogEntry.cls b/sfdx-source/apex-extensions/main/logger/classes/fflib_LogEntry.cls index baefb4c..0a1c1f7 100644 --- a/sfdx-source/apex-extensions/main/logger/classes/fflib_LogEntry.cls +++ b/sfdx-source/apex-extensions/main/logger/classes/fflib_LogEntry.cls @@ -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 ); } } \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/logger/classes/fflib_Logger.cls b/sfdx-source/apex-extensions/main/logger/classes/fflib_Logger.cls index df56eb2..2e45421 100644 --- a/sfdx-source/apex-extensions/main/logger/classes/fflib_Logger.cls +++ b/sfdx-source/apex-extensions/main/logger/classes/fflib_Logger.cls @@ -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) diff --git a/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEntry__c/fields/Uuid__c.field-meta.xml b/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEntry__c/fields/Uuid__c.field-meta.xml new file mode 100644 index 0000000..f5dedf6 --- /dev/null +++ b/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEntry__c/fields/Uuid__c.field-meta.xml @@ -0,0 +1,15 @@ + + + Uuid__c + false + false + false + false + + 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 + A universally unique identifier (UUID) for the error + 36 + false + Text + false + diff --git a/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEvent__e/fields/Uuid__c.field-meta.xml b/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEvent__e/fields/Uuid__c.field-meta.xml new file mode 100644 index 0000000..f5dedf6 --- /dev/null +++ b/sfdx-source/apex-extensions/main/logger/objects/fflib_LogEvent__e/fields/Uuid__c.field-meta.xml @@ -0,0 +1,15 @@ + + + Uuid__c + false + false + false + false + + 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 + A universally unique identifier (UUID) for the error + 36 + false + Text + false + diff --git a/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls b/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls new file mode 100644 index 0000000..1dd196e --- /dev/null +++ b/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls @@ -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 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 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 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 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 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 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' + ); + } + } +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls-meta.xml b/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls-meta.xml new file mode 100644 index 0000000..754ecb1 --- /dev/null +++ b/sfdx-source/apex-extensions/tests/classes/http/fflib_HttpExceptionTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 57.0 + Active + diff --git a/sfdx-source/apex-extensions/tests/classes/logger/fflib_LoggerTest.cls b/sfdx-source/apex-extensions/tests/classes/logger/fflib_LoggerTest.cls index 290e032..ad24f53 100644 --- a/sfdx-source/apex-extensions/tests/classes/logger/fflib_LoggerTest.cls +++ b/sfdx-source/apex-extensions/tests/classes/logger/fflib_LoggerTest.cls @@ -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 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 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 getEntries() + { + return this.appendedEntries; + } + public List getAppendedMessages(System.LoggingLevel loggingLevel) { List result = new List(); diff --git a/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls b/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls new file mode 100644 index 0000000..a229c64 --- /dev/null +++ b/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls @@ -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(); + } +} \ No newline at end of file diff --git a/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls-meta.xml b/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls-meta.xml new file mode 100644 index 0000000..7a51829 --- /dev/null +++ b/sfdx-source/apex-extensions/tests/classes/uuid/fflib_UuidTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active +