Skip to main content

SvfCloudProcedure (Common Apex Class)

Common Apex classes that are used by all samples.

/**
    Classes for using SVF Cloud procedure feature
    Generate and use a new SvfCloudProcedure object for each execution of procedure.
**/
global with sharing class SvfCloudProcedure {
    
    // Base URL of SVF Cloud Web API
    private final String svfCloudBaseURL;
    
    // Name of the client certificate registered in "Certificate and Key Management" used for communication to SVF Cloud
    private final String httpsClientCertificateName;
    
    // Procedure name to execute
    private final String procedureName;
    
    // Salesforce username running Salesforce REST API from SVF Cloud
    private final String execSalesforceUsername;
    
    // REST for accepting SVF Cloud procedure process
    private final String svfCloudProcedurePath = '/procedures/';
    // REST for getting SVF Cloud print status
    private final String svfCloudActionPath= '/actions/';
    
    // URL of artifact obtained as a response to procedure call
    global String locationUrl  { get; set; }
    // Action ID obtained as a response to procedure call
    global String actionId  { get; set; }
    
    // Activities obtained as a response to print status acquisition
    global Action action { get; set; }
    
    // content-type obtained as a response when downloading the artifact
    global String artifactContentType { get; set; }
    /**
        Common exception class indicating that SVF Cloud procedure execution caused an HTTP error
    **/
    global virtual with sharing class SvfCloudException extends Exception {
        
        SvfCloudException(String stage, String actionId){
            this.stage = stage;
            this.actionId = actionId;
        }
        
        // Error occurrence point in this sample code
        global String stage { get; set; }
        
        // Action ID (Process ID). Obtained when the procedure accepts the process.
        // It is displayed as "Process ID" on the Activities screen of SVF Cloud Manager.
        global String actionId { get; set; } 
        
    }
    
    /**
        Exception class indicating that SVF Cloud procedure execution caused an HTTP error
    **/
    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 response status when receiving error
        global Integer httpStatus { get;  set; }
        // HTTP response body when receiving error
        global String httpBody { get;  set; }
    }
    /**
        Exception class indicating that SVF Cloud procedure caused an error
    **/
    global with sharing class SvfCloudActionException extends SvfCloudException {
        
        SvfCloudActionException(String stage, String actionId, String actionState){
            super(stage,actionId);
            this.actionState = actionState;  
        }
        
        // Print status when error occurred
        global String actionState { get;  set; }
        
    }
    
    /**
        Represents the artifact information contained in the JSON object for the print status response of SVF Cloud Web API.
    **/
    global with sharing class Artifact {
        global String path {get; set;} // Artifact file name
        global String name {get; set;} // Artifact name
        global Integer pages {get; set;} // Number of pages
    }
    
    /**
        Represents the JSON object for the print status response of SVF Cloud Web API.
    **/
    global with sharing class Action {
        global String id {get; set;} // Action ID (Process ID)
        global String code {get; set;} // Error code
        global String state {get; set;} // Print status
        global Artifact artifact {get; set;} // Artifact information
    }
    
    /**
        Constructor
        
        @param svfCloudTenantId <Tenant_ID_of_SVF_Cloud>
        @param certName <Name_of_the_certificate_registered_in_"Certificate_and_Key"_of_Salesforce_to_be_used_as_a_client_certificate>
        @param exec SalesforceUsername <Salesforce_username_used_to_access_Salesforce_from_SVF_Cloud>
        @param procedureName <Name_of_procedure_created_in_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;
    }
    
    /**
        Requests SVF Cloud to execute the procedure.
        SVF Cloud accepts procedure execution and immediately returns a response. The process is later performed in the background.
        @param recordIdList <List_of_record_IDs_to_be_processed_by_SVF_Cloud>.
            The record IDs enclosed in single quotes are concatenated with comma-separated and passed to the parameter 'id' of procedure setting.
        @param paramsMap <Map_information_that_overwrites_items_set_in_parameters_of_SVF_Cloud_procedure>.
            Map key is set the parameter name of procedure setting. Map values are enclosed in single quotes.
                
        @return : Action 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');
        
        // Specify the name of the certificate set in "Certificate and Key Management".
        req.setClientCertificateName(httpsClientCertificateName);
        
        // Specify a Salesforce username. It is used as the user who executes retrieving data from Salesforce.
        req.setHeader('X-SVFCloud-Subject', execSalesforceUsername);
        
        // Specify the time zone. It is used for the date-time processing when creating the form data in 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 ) {
            // Creates parameters "id" and "limit" of procedure setting.
            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 redirect is always executed.
            if (String.isEmpty(res.getStatus()) ||  res.getStatusCode()!=307 ){
                throw new SvfCloudHttpException('CALL_SECURE_PROCEDURE', null, res.getStatusCode(), res.getBody());
            }
            
            // Forwards to redirect destination.
            endpoint = res.getHeader('Location');
            System.debug('Redirect URL: '+endpoint);
            req.setEndpoint(endpoint);
            res = http.send(req);
            System.debug('statusCode: ' + res.getStatusCode());
            
            // Direct print returns 202 and others return 303 when the result of the procedure call is normal.
            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());
            }
            
            // The Location header is set the URL for acquiring artifacts such as PDF file generated as a result of procedure call.
            // Also, the URL contains the action ID required to get the print status.
            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;
    }
    
    /**
        Gets the print status of the procedure call executed with #executeProcedure ().
        Waits for restTimeout seconds specified in the parameter until it terminates normally or an error occurs.
        Specify a value for restTimeout so as not to violate callout restrictions or Apex governor restrictions.
        An exception is generated if the procedure processing error is found in print status.
        
        If this method is called multiple times for one procedure call,
        The second and subsequent calls may have a wait time of 0 seconds regardless of the value of restTimeout.
        Therefore, do not call this method in a loop.
        
        @param restTimeout : Maximum wait time (seconds)
        @return : Indicating whether the print status is after print data creation is complete
        
    **/
    global void waitStatus(Integer restTimeout) {
        
        action = retrieveActionStatus(this.actionId, restTimeout);
        System.debug('current state: '+action.state);
        if (
                action.state == '3' // [Print Status] Abnormally terminated
             || action.state == '5' // [Print Status] Cancel
             || action.state == '6' // [Print Status] Terminated
        ) {
            // The status where it could not terminated normally              
            throw new SvfCloudActionException('ACTION_ERROR', this.actionId, action.state );
        }
        return;
    }
    
    /**
        Get the print status of the specified action from SVF Cloud.
        
        @param actionId : Action ID (Process ID)
        @param restTimeout : Timeout value (seconds)
    **/
    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);
        
        // Specify a Salesforce username.
        req.setHeader('X-SVFCloud-Subject', execSalesforceUsername);
        
        // Specify the time zone.
        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 redirect is always executed.
            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();
            
            // Get the activities from the response.
            action = null;
            JSONParser parser = JSON.createParser(body);
            if ( parser.nextToken() != null ) {
                action = (Action)parser.readValueAs(Action.class);
            }
            if ( action == null || action.state == null ){
                // If the response does not contain print status
                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) {
        
        // Get the Action ID from URL of Location header.
        // In case of other than Direct Print, the query parameter of URL contains the 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;
        }
        
        // In case of Direct Print, the URL path contains the 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);
    }
    
    /**
        Downloads the file generated by the action executed with #executeProcedure() from SVF Cloud.
    **/
    global Blob downloadArtifact() {
        
        String downloadUrl = this.locationUrl;
        HttpRequest req = new HttpRequest();
        Http http = new Http();
        HttpResponse res = new HttpResponse();
        
        // Downloads the artifact.
        req.setMethod('GET');
        req.setEndpoint(downloadUrl);
        req.setCompressed(false);
        
        try {
            System.debug('download URL: ' + downloadUrl);
            res = http.send(req);
            System.debug('statusCode(1): ' + res.getStatusCode());
            if ( !String.isEmpty(res.getStatus()) &&  res.getStatusCode()==303 ){
                // Redirects if 303 See Other is returned.
                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;
        }
    }
 }