1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ApexでSalesforceとSVF Cloudを使ったお話

Last updated at Posted at 2024-10-07

はじめに

この記事は、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

SvfCloudPDFAttachmentQueueableクラス
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クラス(長いです)
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;
        }
    }    
 }

使い方の例:フローから見積書を作成するためのサンプル

SvfCloudForFlow_sampleクラス
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へアップロード

私が以前投稿した、下記の記事をご参考ください。
https://qiita.com/sio403/items/75d90b11d3c699b564d8#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%ABattachment%E3%82%92box%E3%81%B8%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89box%E3%81%B8%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%97%E3%81%9F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%85%B1%E6%9C%89%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%AE%E7%94%9F%E6%88%90

おわりに

「プロシージャー」機能により、Apexから帳票印刷、PDFファイルの作成など自動化ができます。
また、今回は実装していませんが大量の印刷・PDFファイル作成にも対応しております。
機会がありましたら、ぜひご活用ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?