はじめに
この記事は、Apexを使ったSVF Cloud(帳票ツール)連携の話になります。
SVF Cloudの文献がほぼないので残します。
SalesforceとSVF Cloudの連携
まずは、下記のマニュアルに従って必要なパッケージをSalesforceへインストールします。
・SVF Cloud for Salesforce セットアップガイド
https://repo.svfcloud.com/manual/release/ja/setup_sfdc/ja/3994405.html
ApexでSVF Cloudを使用するためには、「プロシージャー」機能を使用することになります。
下記のマニュアルに従って、「テナントの設定」「アクセス権の付与」「プロシージャーの作成」などの設定を行ってください。
・SVF Cloud for Salesforce開発ガイド /プロシージャーを使って帳票を出力する /準備をする
https://repo.svfcloud.com/manual/release/ja/devGuide/sc4sfdev/ja/887605.html
※「プロシージャー」機能を使うためには、SVF Cloud上に帳票が作成されている前提となります。
SVF Cloud連携でやったこと
①あるオブジェクト「プロシージャー」機能を使って帳票PDFを作成し、ファイル(Attachment)へ保存
②ファイル(Attachment)をBoxへアップロード
①あるオブジェクト「プロシージャー」機能を使って帳票PDFを作成し、ファイル(Attachment)へ保存
(参考ソース)
こちらは、下記のマニュアルを参考に作成しています。(ほぼ同じです。)
https://repo.svfcloud.com/manual/release/ja/devGuide/sc4sfdev/ja/887611.html
global with sharing class SvfCloudPDFAttachmentQueueable implements Queueable, Database.AllowsCallouts{
private SvfCloudProcedure proc;
private String recordId;
private String fileName;
global SvfCloudPDFAttachmentQueueable(SvfCloudProcedure proc, String recordId, String fileName){
this.proc = proc;
this.recordId = recordId;
this.fileName = fileName;
}
public void execute(QueueableContext context) {
List<String> recordIds = new List<String>{ recordId };
Blob pdf = null;
try {
// プロシージャーを実行します。
String actionId = proc.executeProcedure(recordIds, null);
System.debug('actionID: '+actionId);
// 処理状況を確認します。
proc.waitStatus(60);
// PDFファイルを取得します。
pdf = proc.downloadArtifact();
// レコードに添付します。
Attachment attachment = new Attachment();
attachment.Name = fileName;
attachment.ParentId = recordId;
attachment.Body = pdf;
attachment.ContentType = proc.artifactContentType;
Database.SaveResult saveResult = Database.insert(attachment, false);
if (!saveResult.isSuccess()) {
// ERROR
System.debug('error attachement.');
}
}
catch (SvfCloudProcedure.SvfCloudException e){
System.debug(e);
throw e;
}
catch (CalloutException e){
System.debug(e);
throw e;
}
}
}
SvfCloudProcedureクラス(長いです)
global with sharing class SvfCloudProcedure {
// SVF Cloud WebAPIのベースURL
private final String svfCloudBaseURL;
// SVF Cloudへの通信で使用する、「証明書と鍵の管理」に登録されたクライアント証明書の名前
private final String httpsClientCertificateName;
// 実行するプロシージャー名
private final String procedureName;
// SVF CloudからSalesforce Rest APIを実行するSalesforceユーザ名
private final String execSalesforceUsername;
// SVF Cloud プロシージャー処理受付REST
private final String svfCloudProcedurePath = '/procedures/';
// SVF Cloud 印刷状況の取得REST
private final String svfCloudActionPath= '/actions/';
// プロシージャー呼び出しのレスポンスとして得られる、成果物URL
global String locationUrl { get; set; }
// プロシージャー呼び出しのレスポンスとして得られる、アクションID
global String actionId { get; set; }
// 印刷状況取得のレスポンスとして得られる、処理状況
global Action action { get; set; }
// 成果物ダウンロード時のレスポンスとして得られる、content-type
global String artifactContentType { get; set; }
/**
SVF Cloud プロシージャー実行でHTTPエラーが発生したことを示す共通例外クラス
**/
global virtual with sharing class SvfCloudException extends Exception {
SvfCloudException(String stage, String actionId){
this.stage = stage;
this.actionId = actionId;
}
// 本サンプルコードにおける、エラー発生箇所
global String stage { get; set; }
// アクションID(処理ID)。プロシージャーが処理を受け付けた時に得られます。
// SVF Cloud Managerの処理状況画面に「処理ID」として表示されます。
global String actionId { get; set; }
}
/**
SVF Cloud プロシージャー実行でHTTPエラーが発生したことを示す例外クラス
**/
global with sharing class SvfCloudHttpException extends SvfCloudException {
SvfCloudHttpException(String stage, String actionId, Integer httpStatus, String HttpBody){
super(stage,actionId);
this.httpStatus = httpStatus;
this.httpBody = httpBody;
}
// エラー受信時のHTTPレスポンスステータス
global Integer httpStatus { get; set; }
// エラー受信時のHTTPレスポンスボディ
global String httpBody { get; set; }
}
/**
SVF Cloud プロシージャーでエラーが発生したことを示す例外クラス
**/
global with sharing class SvfCloudActionException extends SvfCloudException {
SvfCloudActionException(String stage, String actionId, String actionState){
super(stage,actionId);
this.actionState = actionState;
}
// エラー発生時の印刷ステータス
global String actionState { get; set; }
}
/**
SVF Cloud WebAPIの印刷状況レスポンスJSONオブジェクトに含まれる成果物情報を表します。
**/
global with sharing class Artifact {
global String path { get; set; } // 成果物ファイル名
global String name { get; set; } // 成果物名
global Integer pages { get; set; } // ページ数
}
/**
SVF Cloud WebAPIの印刷状況レスポンスJSONオブジェクトを表します。
**/
global with sharing class Action {
global String id { get; set; } // アクションID(処理ID)
global String code { get; set; } // エラーコード
global String state { get; set; } // 印刷ステータス
global Artifact artifact { get; set; } // 成果物情報
}
/**
コンストラクタ
@param svfCloudTenantId SVF CloudのテナントID
@param certName クライアント証明書として使用する、Salesforceの「証明書と鍵」に登録した証明書の名前
@param execSalesforceUsername SVF CloudからSalesforceへアクセスする際のSalesforceユーザ名
@param procedureName SVF Cloudに作成したプロシージャーの名前
**/
global SvfCloudProcedure(String svfCloudTenantId, String certName, String execSalesforceUsername, String procedureName){
this.svfCloudBaseURL = 'https://'+svfCloudTenantId+'.secure.svfcloud.com/api/v1';
this.httpsClientCertificateName = certName;
this.procedureName = procedureName;
this.execSalesforceUsername = execSalesforceUsername;
}
/**
SVF Cloudへプロシージャーの実行を依頼する。
SVF Cloud側は、プロシージャー実行を受付しレスポンスをすぐに返す。処理自体は、その後にバックグラウンドで実行される。
@param recordIdList SVF Cloudで処理するレコードIDのリスト。
プロシージャー設定のパラメーター'id'に、シングルクォーテーションで囲まれたレコードIDがカンマ区切りで連結されて渡される。
@param paramsMap SVF Cloudのプロシージャーのパラメーターに設定された項目を上書きするMap情報。
Mapのkeyは、プロシージャー設定のパラメーター名を指定する。Mapのvalueは、シングルクォーテーションで囲まれる。
@return アクションID
**/
global String executeProcedure( List<String> recordIdList, Map<String,String> paramsMap ) {
String location;
HttpRequest req = new HttpRequest();
Http http = new Http();
HttpResponse res = new HttpResponse();
req.setMethod('POST');
// 「証明書と鍵の管理」に設定した証明書の名前を指定します。
req.setClientCertificateName(httpsClientCertificateName);
// Salesforceユーザ名を指定します。Salesforceからデータを取得する際の実行ユーザとして使用されます。
req.setHeader('X-SVFCloud-Subject', execSalesforceUsername);
// タイムゾーンを指定します。SVF Cloudで帳票データ作成時の日時処理に使用されます。
req.setHeader('X-SVFCloud-TimeZone', UserInfo.getTimeZone().toString());
String endpoint = svfCloudBaseURL + svfCloudProcedurePath + procedureName;
req.setEndpoint(endpoint);
List<String> paramList = new List<String>{};
if ( recordIdList != null && recordIdList.size()>=0 ) {
// プロシージャー設定のパラメーター"id"と"limit"を作成します。
String ids = '\'' + String.join(recordIdList,'\',\'') + '\'';
paramList.add('id='+EncodingUtil.urlEncode(ids,'UTF-8'));
paramList.add('limit='+String.valueOf(recordIdList.size()));
}
if ( paramsMap!=null && paramsMap.size()>0 ){
for ( String key: paramsMap.keySet() ){
paramList.add(key + '=' + EncodingUtil.urlEncode(paramsMap.get(key),'UTF-8'));
}
}
String body = String.join(paramList,'&');
System.debug('form body: '+body);
req.setBody(body);
req.setHeader('Content-type', 'application/x-www-form-urlencoded');
req.setHeader('Content-Length',String.ValueOf(body.length()));
req.setCompressed(false);
try {
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
//通常時
res = http.send(req);
}else{
//テスト時
res.setStatus('OK');
res.setStatusCode(307);
}
System.debug('statusCode: ' + res.getStatusCode());
// 307リダイレクトは必ず行われます。
if (String.isEmpty(res.getStatus()) || res.getStatusCode()!=307 ){
throw new SvfCloudHttpException('CALL_SECURE_PROCEDURE', null, res.getStatusCode(), res.getBody());
}
// リダイレクト先へ転送します。
endpoint = res.getHeader('Location');
System.debug('Redirect URL: '+endpoint);
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
req.setEndpoint(endpoint);
}
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
res = http.send(req);
}else{
//テスト時
res.setStatus('OK');
res.setStatusCode(202);
}
System.debug('statusCode: ' + res.getStatusCode());
// プロシージャー呼び出しの結果、正常な場合は、ダイレクトプリントでは202、その他は303が返ってきます。
if (!String.isEmpty(res.getStatus()) && res.getStatusCode()!=303 && res.getStatusCode()!=202 ){
System.debug(res.getBody());
throw new SvfCloudHttpException('CALL_EXECUTE', null, res.getStatusCode(), res.getBody());
}
// Location ヘッダーには、プロシージャー呼び出しの結果生成されるPDFファイル等の成果物を取得するためのURLが設定されます。
// また、URLには印刷状況の取得に必要なアクションIDが含まれます。
location = res.getHeader('Location');
System.debug('location: ' + location);
}catch(System.CalloutException e) {
System.debug('Callout error: '+ e);
throw e;
}
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
this.locationUrl = location;
this.actionId = parseActionId(location);
}else{
location = 'https://api.svfcloud.com/v1/artifacts/3dce0d82-74c3-4ed2-9174-6bbc83136546?action=9b42d519-49af-9999-85b5-7ec4a20628e4&ticket=99998e85392cb837020390a6dd86ca6aac12c1b539ea80f6f0b7a8d91681889d';
this.locationUrl = location;
this.actionId = parseActionId(location);
}
return this.actionId;
}
/**
#executeProcedure()で実行したプロシージャー呼び出しの印刷ステータスを取得します。
正常終了するかエラーになるまで、パラメーターに指定したrestTimeout秒待機します。
コールアウトの制限事項やApexガバナ制限に抵触しないように、restTimeoutの値を指定してください。
印刷ステータスでプロシージャー処理がエラーになったことが確認できた場合、例外を発生させています。
なお、1回のプロシージャー呼び出しに対して本メソッドを複数回呼び出した場合、
2回目以降はrestTimeoutの値にかかわらず待機時間が0秒となることがありますので、
本メソッドをループ呼び出しすることはしないでください。
@param restTimeout 最大待機時間(秒)
@return 印刷ステータスが 印刷データ作成終了以降かどうか
**/
global void waitStatus(Integer restTimeout) {
action = retrieveActionStatus(this.actionId, restTimeout);
System.debug('current state: '+action.state);
if (
action.state == '3' // [印刷ステータス] 異常終了
|| action.state == '5' // [印刷ステータス] キャンセル
|| action.state == '6' // [印刷ステータス] 強制終了
) {
// 正常に終了しなかった状態
throw new SvfCloudActionException('ACTION_ERROR', this.actionId, action.state );
}
return;
}
/**
指定されたアクションの印刷状況をSVF Cloudから取得します。
@param actionId アクションID(処理ID)
@param restTimeout タイムアウト値(秒)
**/
global Action retrieveActionStatus(String actionId, Integer restTimeout){
Action action;
HttpRequest req = new HttpRequest();
Http http = new Http();
HttpResponse res = new HttpResponse();
req.setMethod('GET');
req.setClientCertificateName(httpsClientCertificateName);
// Salesforceユーザ名を指定します。
req.setHeader('X-SVFCloud-Subject', execSalesforceUsername);
// タイムゾーンを指定します。
req.setHeader('X-SVFCloud-TimeZone', UserInfo.getTimeZone().toString());
String endpoint = svfCloudBaseURL+svfCloudActionPath+actionId+'?timeout='+String.valueOf(restTimeout);
System.debug('Action URL: '+endpoint);
req.setEndpoint(endpoint);
req.setHeader('Accept','application/json');
req.setCompressed(false);
try {
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
res = http.send(req);
}else{
res.setStatus('OK');
res.setStatusCode(307);
}
System.debug('statusCode: ' + res.getStatusCode());
// 307リダイレクトは必ず行われます。
if (String.isEmpty(res.getStatus()) || res.getStatusCode()!=307 ){
throw new SvfCloudHttpException('CALL_SECURE_ACTION', null, res.getStatusCode(), res.getBody());
}
endpoint = res.getHeader('Location');
System.debug('Action Redirect URL: '+endpoint);
req = new HttpRequest();
req.setMethod('GET');
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
req.setEndpoint(endpoint);
}
req.setHeader('Accept','application/json');
req.setTimeout(120000);
Long startTime = DateTime.now().getTime();
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
res = http.send(req);
}else{
res.setStatus('OK');
res.setStatusCode(200);
}
Long finishTime = DateTime.now().getTime();
System.debug('elapsed msec: '+(finishTime-startTime)+' statusCode: ' + res.getStatusCode());
if(String.isEmpty(res.getStatus()) || res.getStatusCode()!=200){
throw new SvfCloudHttpException('ACTION', actionId, res.getStatusCode(), res.getBody());
}
String body = res.getBody();
// レスポンスから処理状況を取得します。
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
action = null;
}else{
//疑似的にアクションを作成
Artifact af = new Artifact();
af.path = 'testfile.pdf';
af.name = 'pdf';
af.pages = 1;
action = new Action();
action.id = 'xxx';
action.code = '0';
action.state = '0';
action.artifact = af;
}
JSONParser parser = JSON.createParser(body);
if ( parser.nextToken() != null ) {
action = (Action)parser.readValueAs(Action.class);
}
if ( action == null || action.state == null ){
// レスポンスに印刷ステータスが含まれていない場合
System.debug(body);
throw new SvfCloudActionException('INVALID_ACTION_RESULT', actionId, null);
}
System.debug('artifact: '+action.artifact.path);
}catch(System.CalloutException e) {
System.debug('Callout error: '+ e);
throw e;
}
return action;
}
private String parseActionId(String locationUrl) {
// LocationヘッダのURLからアクションIDを取得します。
// ダイレクトプリント以外の場合、URLのquery parameterにAction IDが入っています。
Pattern pt = Pattern.compile('action=(.+)&ticket');
Matcher matcher = pt.matcher(locationUrl);
String id = null;
if ( matcher.find() ) {
id = matcher.group(1);
return id;
}
// ダイレクトプリントの場合は、URLのパスにAction IDが入っています。
pt = Pattern.compile('actions/([0-9a-f-]*)$');
matcher = pt.matcher(locationUrl);
if ( matcher.find() ) {
id = matcher.group(1);
return id;
}
System.debug('invalid location URL: '+locationUrl);
throw new SvfCloudActionException('GET_ACTION_ID',null,null);
}
/**
#executeProcedure()で実行したアクションが生成したファイルをSVF Cloudからダウンロードする。
**/
global Blob downloadArtifact() {
String downloadUrl = this.locationUrl;
HttpRequest req = new HttpRequest();
Http http = new Http();
HttpResponse res = new HttpResponse();
// 成果物のダウンロードを行います。
req.setMethod('GET');
req.setEndpoint(downloadUrl);
req.setCompressed(false);
try {
System.debug('download URL: ' + downloadUrl);
//Apexテスト時のエラー回避
if (Test.isRunningTest() == false) {
res = http.send(req);
}else{
Blob b = Blob.valueOf('test');
res.setStatus('OK');
res.setStatusCode(200);
res.setBodyAsBlob(b);
}
System.debug('statusCode(1): ' + res.getStatusCode());
if ( !String.isEmpty(res.getStatus()) && res.getStatusCode()==303 ){
// 303 See Other が返ってきたら、リダイレクトします。
String endpoint = res.getHeader('Location');
System.debug('redirect URL: '+endpoint);
req = new HttpRequest();
req.setMethod('GET');
req.setEndpoint(endpoint);
res = http.send(req);
System.debug('statusCode(2): ' + res.getStatusCode());
}
if (!String.isEmpty(res.getStatus()) && res.getStatusCode()==200 ){
artifactContentType = res.getHeader('Content-Type');
System.debug('download content type = '+artifactContentType);
return res.getBodyAsBlob();
}
// ERROR
throw new SvfCloudHttpException('DOWNLOAD_ERROR', this.actionId, res.getStatusCode(), res.getBody());
}catch(System.CalloutException e) {
System.debug('Callout error: '+ e);
throw e;
}
}
}
使い方の例:フローから見積書を作成するためのサンプル
public class SvfCloudForFlow_sample {
static final String PROCEDURE_NAME = 'proc_sample'; // プロシージャ名
@InvocableMethod(label='SVFCloudファイル作成' description='SVFCloudでPDFを作成し、ファイルに保存')
static public void createAttachmentSVFCloudPDF(List<String> IDList) {
// Apex ジョブキューにプロシージャー呼び出し処理を追加し、非同期に実行します。
SvfCloudProcedure proc = new SvfCloudProcedure(
System.label.SVFCloudTenantID, // テナントID
System.label.SVFCloudCertificateName,// 「証明書と鍵の管理」に登録した証明書の名前
System.label.SVFCloudExecutingUser, // SVF CloudからSalesforce RESTを実行するSalesforceユーザ名
PROCEDURE_NAME // SVF Cloud Managerで定義したプロシージャーの名前
);
if(IDList != null){
String fileName = null;
//見積を取得
Quote quote
= [SELECT Id,
Name
FROM Quote WHERE Id = :IDList[0]];
//ファイル名を生成
fileName = String.format('見積書_{0}.pdf', new List<Object>{quote.Name});
System.enqueueJob( new SvfCloudPDFAttachmentQueueable(
proc,
IDList[0],// レコードID
fileName // ファイル名
));
}
}
}
②ファイル(Attachment)をBoxへアップロード
おわりに
「プロシージャー」機能により、Apexから帳票印刷、PDFファイルの作成など自動化ができます。
また、今回は実装していませんが大量の印刷・PDFファイル作成にも対応しております。
機会がありましたら、ぜひご活用ください。