570 lines
21 KiB
OpenEdge ABL
570 lines
21 KiB
OpenEdge ABL
public class CLMDocGenCallout {
|
|
|
|
// S1 demo environment
|
|
private static final String CLM_ACCOUNT_ID_S1 = '2371cf36-eb8a-43fe-9f28-b5bbe7644397';
|
|
private static final String CLM_NAMED_CRED_S1 = 'callout:CLMs1NamedCreds';
|
|
private static final String CLM_DOWNLOAD_NAMED_CRED_S1 = 'callout:CLMs1Download';
|
|
|
|
// UAT demo environment
|
|
private static final String CLM_ACCOUNT_ID_UAT = 'bccae332-c7db-4892-ab85-257df0f70fea';
|
|
private static final String CLM_NAMED_CRED_UAT = 'callout:CLMuatNamedCreds';
|
|
private static final String CLM_DOWNLOAD_NAMED_CRED_UAT = 'callout:CLMuatDownload';
|
|
|
|
public static final Integer HTTP_TIMEOUT = 30000;
|
|
|
|
/** Resolve the correct CLM Named Credential base. env: 'UAT' or 'S1'. */
|
|
private static String clmNamedCredential(String env) {
|
|
return env == 'S1' ? CLM_NAMED_CRED_S1 : CLM_NAMED_CRED_UAT;
|
|
}
|
|
|
|
private static String clmDownloadNamedCredential(String env) {
|
|
return env == 'S1' ? CLM_DOWNLOAD_NAMED_CRED_S1 : CLM_DOWNLOAD_NAMED_CRED_UAT;
|
|
}
|
|
|
|
@TestVisible
|
|
static String defaultAccountId(String env) {
|
|
return env == 'S1' ? CLM_ACCOUNT_ID_S1 : CLM_ACCOUNT_ID_UAT;
|
|
}
|
|
|
|
@TestVisible
|
|
static String extractAccountId(String resourceOrHref) {
|
|
if (String.isBlank(resourceOrHref)) {
|
|
return null;
|
|
}
|
|
|
|
Integer v2Index = resourceOrHref.indexOf('/v2/');
|
|
if (v2Index < 0) {
|
|
return null;
|
|
}
|
|
|
|
String afterV2 = resourceOrHref.substring(v2Index + 4);
|
|
List<String> parts = afterV2.split('/');
|
|
return parts.isEmpty() ? null : parts[0];
|
|
}
|
|
|
|
@TestVisible
|
|
static String normalizeResourcePath(String resourceOrHref, String env) {
|
|
return normalizeResourcePathWithAccountId(resourceOrHref, defaultAccountId(env));
|
|
}
|
|
|
|
@TestVisible
|
|
static String normalizeResourcePathWithAccountId(String resourceOrHref, String fallbackAccountId) {
|
|
if (String.isBlank(resourceOrHref)) {
|
|
throw new IllegalArgumentException('resourceOrHref is required');
|
|
}
|
|
|
|
if (resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://')) {
|
|
Integer pathStart = resourceOrHref.indexOf('/', resourceOrHref.indexOf('//') + 2);
|
|
if (pathStart < 0) {
|
|
throw new IllegalArgumentException('Unable to determine resource path from href: ' + resourceOrHref);
|
|
}
|
|
return resourceOrHref.substring(pathStart);
|
|
}
|
|
|
|
if (resourceOrHref.startsWith('/v2/')) {
|
|
return resourceOrHref;
|
|
}
|
|
|
|
if (resourceOrHref.startsWith('v2/')) {
|
|
return '/' + resourceOrHref;
|
|
}
|
|
|
|
if (resourceOrHref.startsWith('/')) {
|
|
return '/v2/' + fallbackAccountId + resourceOrHref;
|
|
}
|
|
|
|
return '/v2/' + fallbackAccountId + '/' + resourceOrHref;
|
|
}
|
|
|
|
public static String buildEndpointForResource(String resourceOrHref, String env) {
|
|
return clmNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
|
|
}
|
|
|
|
public static String buildEndpointForResource(String resourceOrHref, String accountId, String apiNamedCredential) {
|
|
return 'callout:' + apiNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
|
|
}
|
|
|
|
public static String buildDownloadEndpointForResource(String resourceOrHref, String env) {
|
|
return clmDownloadNamedCredential(env) + normalizeResourcePath(resourceOrHref, env);
|
|
}
|
|
|
|
public static String buildDownloadEndpointForResource(String resourceOrHref, String accountId, String downloadNamedCredential) {
|
|
return 'callout:' + downloadNamedCredential + normalizeResourcePathWithAccountId(resourceOrHref, accountId);
|
|
}
|
|
|
|
/** Defaults to UAT environment. */
|
|
public static CLMDocGenResponse generateDocument(
|
|
String caseId,
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String destinationDocName
|
|
) {
|
|
return generateDocument(caseId, templateDocHref, destinationFolderHref, destinationDocName, 'UAT');
|
|
}
|
|
|
|
/**
|
|
* Generate a merged document via CLM documentxmlmergetasks (no user interaction).
|
|
* @param caseId Appraiser_Case__c record Id
|
|
* @param templateDocHref Full CLM Href URL of the template .docx document
|
|
* @param destinationFolderHref Full CLM Href URL of the destination folder
|
|
* @param destinationDocName Filename for the generated document, e.g. "Review_AC-00001.docx"
|
|
* @param env 'UAT' (apiuatna11.springcm.com) or 'S1' (api.s1.us.clm.demo.docusign.net)
|
|
*/
|
|
public static CLMDocGenResponse generateDocument(
|
|
String caseId,
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String destinationDocName,
|
|
String env
|
|
) {
|
|
return generateDocument(
|
|
caseId,
|
|
templateDocHref,
|
|
destinationFolderHref,
|
|
destinationDocName,
|
|
env,
|
|
defaultAccountId(env),
|
|
clmNamedCredential(env).substringAfter('callout:')
|
|
);
|
|
}
|
|
|
|
public static CLMDocGenResponse generateDocument(
|
|
String caseId,
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String destinationDocName,
|
|
String env,
|
|
String configuredAccountId,
|
|
String apiNamedCredential
|
|
) {
|
|
Map<String, Object> casePayload = AppraiserCasePayloadBuilder.buildPayload(caseId);
|
|
String accountId = firstNonBlank(
|
|
extractAccountId(templateDocHref),
|
|
extractAccountId(destinationFolderHref),
|
|
configuredAccountId,
|
|
defaultAccountId(env)
|
|
);
|
|
Map<String, Object> requestBody = buildRequestBodyMap(
|
|
casePayload,
|
|
templateDocHref,
|
|
destinationFolderHref,
|
|
destinationDocName
|
|
);
|
|
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint('callout:' + apiNamedCredential + '/v2/' + accountId + '/documentxmlmergetasks');
|
|
req.setMethod('POST');
|
|
req.setHeader('Content-Type', 'application/json');
|
|
req.setTimeout(HTTP_TIMEOUT);
|
|
req.setBody(JSON.serialize(requestBody));
|
|
|
|
try {
|
|
Http http = new Http();
|
|
HttpResponse res = http.send(req);
|
|
return parseTaskResponse(res);
|
|
} catch (Exception ex) {
|
|
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
|
|
}
|
|
}
|
|
|
|
public static String buildDataXmlForCase(String caseId) {
|
|
return buildDataXml(AppraiserCasePayloadBuilder.buildPayload(caseId));
|
|
}
|
|
|
|
public static String buildRequestBodyJson(
|
|
String caseId,
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String destinationDocName
|
|
) {
|
|
return JSON.serializePretty(
|
|
buildRequestBodyMap(
|
|
AppraiserCasePayloadBuilder.buildPayload(caseId),
|
|
templateDocHref,
|
|
destinationFolderHref,
|
|
destinationDocName
|
|
)
|
|
);
|
|
}
|
|
|
|
public static String buildDocumentXmlMergeTasksUrl(
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String env,
|
|
String configuredAccountId
|
|
) {
|
|
String accountId = firstNonBlank(
|
|
extractAccountId(templateDocHref),
|
|
extractAccountId(destinationFolderHref),
|
|
configuredAccountId,
|
|
defaultAccountId(env)
|
|
);
|
|
String baseUrl = firstNonBlank(
|
|
extractBaseUrl(templateDocHref),
|
|
extractBaseUrl(destinationFolderHref),
|
|
null,
|
|
defaultBaseUrl(env)
|
|
);
|
|
return baseUrl + '/v2/' + accountId + '/documentxmlmergetasks';
|
|
}
|
|
|
|
/** Poll the status of a submitted merge task by its GUID (defaults to UAT). */
|
|
public static CLMDocGenResponse getTaskStatus(String taskId) {
|
|
return getTaskStatus(taskId, 'UAT');
|
|
}
|
|
|
|
public static CLMDocGenResponse getTaskStatus(String taskId, String env) {
|
|
return getTaskStatus(taskId, env, defaultAccountId(env), clmNamedCredential(env).substringAfter('callout:'));
|
|
}
|
|
|
|
public static CLMDocGenResponse getTaskStatus(String taskId, String env, String configuredAccountId, String apiNamedCredential) {
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(buildEndpointForResource('/documentxmlmergetasks/' + taskId, configuredAccountId, apiNamedCredential));
|
|
req.setMethod('GET');
|
|
req.setTimeout(HTTP_TIMEOUT);
|
|
try {
|
|
Http http = new Http();
|
|
HttpResponse res = http.send(req);
|
|
return parseTaskResponse(res);
|
|
} catch (Exception ex) {
|
|
return new CLMDocGenResponse(false, 'HTTP Callout Error: ' + ex.getMessage(), null, null);
|
|
}
|
|
}
|
|
|
|
/** Probe any CLM resource path for debugging. env: 'UAT' or 'S1'. */
|
|
public static String probe(String resource) {
|
|
return probe(resource, 'UAT');
|
|
}
|
|
|
|
public static String probe(String resource, String env) {
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(buildEndpointForResource(resource, env));
|
|
req.setMethod('GET');
|
|
req.setTimeout(HTTP_TIMEOUT);
|
|
HttpResponse res = new Http().send(req);
|
|
return 'HTTP ' + res.getStatusCode() + ': ' + res.getBody();
|
|
}
|
|
|
|
public static DownloadedDocument downloadDocument(String resourceOrHref, String env) {
|
|
return downloadDocument(
|
|
resourceOrHref,
|
|
env,
|
|
defaultAccountId(env),
|
|
clmDownloadNamedCredential(env).substringAfter('callout:')
|
|
);
|
|
}
|
|
|
|
public static DownloadedDocument downloadDocument(String resourceOrHref, String env, String configuredAccountId, String downloadNamedCredential) {
|
|
HttpRequest req = new HttpRequest();
|
|
req.setEndpoint(buildDownloadEndpointForResource(resourceOrHref, configuredAccountId, downloadNamedCredential));
|
|
req.setMethod('GET');
|
|
req.setTimeout(HTTP_TIMEOUT);
|
|
|
|
HttpResponse res = new Http().send(req);
|
|
Integer statusCode = res.getStatusCode();
|
|
if (statusCode < 200 || statusCode >= 300) {
|
|
throw new CalloutException('CLM download error (HTTP ' + statusCode + '): ' + res.getBody());
|
|
}
|
|
|
|
DownloadedDocument document = new DownloadedDocument();
|
|
document.body = res.getBodyAsBlob();
|
|
document.contentType = res.getHeader('Content-Type');
|
|
document.fileName = extractFileName(res, resourceOrHref, document.contentType);
|
|
return document;
|
|
}
|
|
|
|
/**
|
|
* Build the DataXML string from the case payload.
|
|
* Flat fields become direct child elements of <TemplateFieldData>.
|
|
* DeficiencyList items expand into numbered elements:
|
|
* Deficiency_1_Number, Deficiency_1_Description, Deficiency_1_Resolution, etc.
|
|
*/
|
|
private static String buildDataXml(Map<String, Object> payload) {
|
|
String xml = '<TemplateFieldData>';
|
|
|
|
// Emit flat fields first
|
|
for (String key : payload.keySet()) {
|
|
if (key == 'DeficiencyList') continue;
|
|
xml += '<' + key + '>' + escapeXml(safeValue(payload.get(key))) + '</' + key + '>';
|
|
}
|
|
|
|
// Emit DeficiencyList as a nested list so templates can iterate dynamically
|
|
List<Object> deficiencies = (List<Object>) payload.get('DeficiencyList');
|
|
if (deficiencies != null && !deficiencies.isEmpty()) {
|
|
xml += '<DeficiencyList>';
|
|
for (Integer i = 0; i < deficiencies.size(); i++) {
|
|
Map<String, Object> d = (Map<String, Object>) deficiencies[i];
|
|
xml += '<Deficiency>';
|
|
xml += '<Number>' + escapeXml(safeValue(d.get('deficiencyNumber'))) + '</Number>';
|
|
xml += '<Description>' + escapeXml(safeValue(d.get('description'))) + '</Description>';
|
|
xml += '<Resolution>' + escapeXml(safeValue(d.get('resolution'))) + '</Resolution>';
|
|
xml += '<Reference>' + escapeXml(safeValue(d.get('reference'))) + '</Reference>';
|
|
xml += '</Deficiency>';
|
|
}
|
|
xml += '</DeficiencyList>';
|
|
xml += '<DeficiencyCount>' + deficiencies.size() + '</DeficiencyCount>';
|
|
}
|
|
|
|
xml += '</TemplateFieldData>';
|
|
return xml;
|
|
}
|
|
|
|
public static String prettyPrintXml(String xml) {
|
|
if (String.isBlank(xml)) {
|
|
return xml;
|
|
}
|
|
|
|
String normalized = xml
|
|
.replace('><', '>\n<')
|
|
.replace('\r\n', '\n')
|
|
.replace('\r', '\n');
|
|
|
|
List<String> lines = normalized.split('\n');
|
|
List<String> formatted = new List<String>();
|
|
Integer indent = 0;
|
|
|
|
for (String rawLine : lines) {
|
|
String line = rawLine == null ? '' : rawLine.trim();
|
|
if (line == '') {
|
|
continue;
|
|
}
|
|
|
|
Boolean isClosing = line.startsWith('</');
|
|
Boolean isDeclaration = line.startsWith('<?') || line.startsWith('<!');
|
|
Boolean isSelfClosing = line.endsWith('/>') || (line.contains('</') && line.indexOf('</') > 0);
|
|
|
|
if (isClosing && indent > 0) {
|
|
indent--;
|
|
}
|
|
|
|
formatted.add(repeatIndent(indent) + line);
|
|
|
|
if (!isClosing && !isSelfClosing && !isDeclaration) {
|
|
indent++;
|
|
}
|
|
}
|
|
|
|
return String.join(formatted, '\n');
|
|
}
|
|
|
|
private static Map<String, Object> buildRequestBodyMap(
|
|
Map<String, Object> casePayload,
|
|
String templateDocHref,
|
|
String destinationFolderHref,
|
|
String destinationDocName
|
|
) {
|
|
return new Map<String, Object>{
|
|
'TemplateDocument' => new Map<String, Object>{
|
|
'Href' => templateDocHref
|
|
},
|
|
'DataXML' => buildDataXml(casePayload),
|
|
'DestinationDocumentName' => destinationDocName,
|
|
'DestinationFolder' => new Map<String, Object>{
|
|
'Href' => destinationFolderHref
|
|
}
|
|
};
|
|
}
|
|
|
|
private static String extractBaseUrl(String resourceOrHref) {
|
|
if (String.isBlank(resourceOrHref)) {
|
|
return null;
|
|
}
|
|
if (!(resourceOrHref.startsWith('http://') || resourceOrHref.startsWith('https://'))) {
|
|
return null;
|
|
}
|
|
|
|
Integer schemeIndex = resourceOrHref.indexOf('//');
|
|
Integer pathStart = resourceOrHref.indexOf('/', schemeIndex + 2);
|
|
return pathStart > 0 ? resourceOrHref.substring(0, pathStart) : resourceOrHref;
|
|
}
|
|
|
|
private static String defaultBaseUrl(String env) {
|
|
return env == 'S1'
|
|
? 'https://api.s1.us.clm.demo.docusign.net'
|
|
: 'https://apiuatna11.springcm.com';
|
|
}
|
|
|
|
private static String repeatIndent(Integer indent) {
|
|
String value = '';
|
|
for (Integer i = 0; i < indent; i++) {
|
|
value += ' ';
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private static String safeValue(Object val) {
|
|
return val != null ? String.valueOf(val) : '';
|
|
}
|
|
|
|
private static String escapeXml(String s) {
|
|
if (s == null) return '';
|
|
return s.replace('&', '&')
|
|
.replace('<', '<')
|
|
.replace('>', '>')
|
|
.replace('"', '"')
|
|
.replace('\'', ''');
|
|
}
|
|
|
|
private static CLMDocGenResponse parseTaskResponse(HttpResponse res) {
|
|
Integer statusCode = res.getStatusCode();
|
|
String body = res.getBody();
|
|
if (statusCode >= 200 && statusCode < 300) {
|
|
Map<String, Object> m = (Map<String, Object>) JSON.deserializeUntyped(body);
|
|
String href = firstString(m, new List<String>{ 'Href', 'Uri', 'Location' });
|
|
String status = firstString(m, new List<String>{ 'Status', 'State' });
|
|
String taskId = href != null ? href.substringAfterLast('/') : null;
|
|
String generatedDocumentUrl = findFirstDocumentHref(m);
|
|
String generatedDocumentId = generatedDocumentUrl != null ? generatedDocumentUrl.substringAfterLast('/') : null;
|
|
String message = 'Task status: ' + (String.isNotBlank(status) ? status : 'Unknown');
|
|
return new CLMDocGenResponse(true, message, href, taskId, status, generatedDocumentUrl, generatedDocumentId, body);
|
|
} else {
|
|
return new CLMDocGenResponse(false, 'CLM API Error (HTTP ' + statusCode + '): ' + body, null, null, null, null, null, body);
|
|
}
|
|
}
|
|
|
|
private static String firstNonBlank(String firstValue, String secondValue, String thirdValue, String fallbackValue) {
|
|
if (String.isNotBlank(firstValue)) {
|
|
return firstValue;
|
|
}
|
|
if (String.isNotBlank(secondValue)) {
|
|
return secondValue;
|
|
}
|
|
if (String.isNotBlank(thirdValue)) {
|
|
return thirdValue;
|
|
}
|
|
return fallbackValue;
|
|
}
|
|
|
|
private static String firstString(Map<String, Object> source, List<String> keys) {
|
|
for (String key : keys) {
|
|
Object value = source.get(key);
|
|
if (value != null) {
|
|
String asString = String.valueOf(value);
|
|
if (String.isNotBlank(asString)) {
|
|
return asString;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static String findFirstDocumentHref(Object node) {
|
|
if (node instanceof Map<String, Object>) {
|
|
Map<String, Object> mapNode = (Map<String, Object>) node;
|
|
String href = firstString(mapNode, new List<String>{ 'Href', 'Uri', 'Location' });
|
|
if (String.isNotBlank(href) && href.contains('/documents/') && !href.contains('/documentxmlmergetasks/')) {
|
|
return href;
|
|
}
|
|
|
|
for (Object value : mapNode.values()) {
|
|
String nestedHref = findFirstDocumentHref(value);
|
|
if (String.isNotBlank(nestedHref)) {
|
|
return nestedHref;
|
|
}
|
|
}
|
|
} else if (node instanceof List<Object>) {
|
|
for (Object item : (List<Object>) node) {
|
|
String nestedHref = findFirstDocumentHref(item);
|
|
if (String.isNotBlank(nestedHref)) {
|
|
return nestedHref;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static String extractFileName(HttpResponse res, String resourceOrHref, String contentType) {
|
|
String disposition = res.getHeader('Content-Disposition');
|
|
if (String.isNotBlank(disposition)) {
|
|
Integer marker = disposition.toLowerCase().indexOf('filename=');
|
|
if (marker >= 0) {
|
|
String candidate = disposition.substring(marker + 9).trim();
|
|
if (candidate.startsWith('"') && candidate.endsWith('"') && candidate.length() >= 2) {
|
|
candidate = candidate.substring(1, candidate.length() - 1);
|
|
}
|
|
if (String.isNotBlank(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
String baseName = String.isNotBlank(resourceOrHref) ? resourceOrHref.substringAfterLast('/') : 'generated-document';
|
|
String extension = inferExtension(contentType);
|
|
if (String.isNotBlank(extension) && !baseName.toLowerCase().endsWith(extension)) {
|
|
return baseName + extension;
|
|
}
|
|
return baseName;
|
|
}
|
|
|
|
private static String inferExtension(String contentType) {
|
|
if (String.isBlank(contentType)) {
|
|
return '.docx';
|
|
}
|
|
|
|
String normalizedType = contentType.toLowerCase();
|
|
if (normalizedType.contains('pdf')) {
|
|
return '.pdf';
|
|
}
|
|
if (normalizedType.contains('wordprocessingml') || normalizedType.contains('officedocument')) {
|
|
return '.docx';
|
|
}
|
|
if (normalizedType.contains('msword')) {
|
|
return '.doc';
|
|
}
|
|
if (normalizedType.contains('json')) {
|
|
return '.json';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public class CLMDocGenResponse {
|
|
@AuraEnabled
|
|
public Boolean success;
|
|
@AuraEnabled
|
|
public String message;
|
|
@AuraEnabled
|
|
public String documentUrl;
|
|
@AuraEnabled
|
|
public String documentId;
|
|
@AuraEnabled
|
|
public String taskStatus;
|
|
@AuraEnabled
|
|
public String generatedDocumentUrl;
|
|
@AuraEnabled
|
|
public String generatedDocumentId;
|
|
@AuraEnabled
|
|
public String taskDetailsJson;
|
|
|
|
public CLMDocGenResponse(Boolean success, String message, String documentUrl, String documentId) {
|
|
this(success, message, documentUrl, documentId, null, null, null, null);
|
|
}
|
|
|
|
public CLMDocGenResponse(
|
|
Boolean success,
|
|
String message,
|
|
String documentUrl,
|
|
String documentId,
|
|
String taskStatus,
|
|
String generatedDocumentUrl,
|
|
String generatedDocumentId,
|
|
String taskDetailsJson
|
|
) {
|
|
this.success = success;
|
|
this.message = message;
|
|
this.documentUrl = documentUrl;
|
|
this.documentId = documentId;
|
|
this.taskStatus = taskStatus;
|
|
this.generatedDocumentUrl = generatedDocumentUrl;
|
|
this.generatedDocumentId = generatedDocumentId;
|
|
this.taskDetailsJson = taskDetailsJson;
|
|
}
|
|
}
|
|
|
|
public class DownloadedDocument {
|
|
public Blob body;
|
|
public String fileName;
|
|
public String contentType;
|
|
}
|
|
}
|