Skip to main content

SvfCloudProcedure(共通のApex クラス)

すべてのサンプルが使用する、共通のApex クラスです。

/**
    SVF Cloudプロシージャー機能を使用するためのクラス
    1回のプロシージャー実行ごとに、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 {
            res = http.send(req);
            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);
            req.setEndpoint(endpoint);
            res = http.send(req);
            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');
            
        }catch(System.CalloutException e) {
            System.debug('Callout error: '+ e);
            throw e;
        }
        
        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 {
            res = http.send(req);
            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');
            req.setEndpoint(endpoint);
            req.setHeader('Accept','application/json');
            req.setTimeout(120000); 
                
            Long startTime = DateTime.now().getTime();
            res = http.send(req);
            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();
            
            // レスポンスから処理状況を取得します。
            action = null;
            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);
        req.setHeader('Accept','application/octet-stream');
        
        try {
            System.debug('download URL: ' + downloadUrl);
            res = http.send(req);
            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);
                req.setHeader('Accept','application/octet-stream');
                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;
        }
    }
 }