├── README.md ├── ExecuteAnonymousBatchTest.cls └── ExecuteAnonymousBatch.cls /README.md: -------------------------------------------------------------------------------- 1 | # Execute Anonymous Batch 2 | 3 | Author: [Enrico Murru](https://enree.co) 4 | 5 | Read details @ http://blog.enree.co/2017/08/salesforce-apex-execute-anonymous-batch.html 6 | 7 | The `ID_LIST` *global* variable is of type `List` and contains all the IDs provided by the Batch's scope. 8 | 9 | Remember to configure a named credential called `EXECUTE_ANONYMOUS` that points to your instance (and that is logged with a valid session ID). 10 | 11 | Example of usage: 12 | 13 | ```java 14 | String script = 'List acList = [Select Id, Name From Account Where Id IN :ID_LIST];' 15 | +'\nfor(Account acc : acList){' 16 | +'\n acc.BillingCity = \'Gnite City\';' 17 | +'\n}' 18 | +'\n update acList;'; 19 | 20 | String query = 'Select Id From Account Where BillingCity != \'Gnite City\''; 21 | 22 | Boolean sendEmailOnFinish = true; 23 | 24 | ExecuteAnonymousBatch batch = new ExecuteAnonymousBatch(query, script, sendEmailOnFinish); 25 | Database.executeBatch(batch, 100); 26 | ``` 27 | 28 | The job sends a completion email with all eventual errors on finish. -------------------------------------------------------------------------------- /ExecuteAnonymousBatchTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Test class for ExecuteAnonymousBatch 3 | * 4 | * @author Enrico Murru (http://enree.co) 5 | * @version 1.0 6 | * @history 7 | * 2017-08-02 : Enrico Murru - Original version 8 | */ 9 | @IsTest 10 | private class ExecuteAnonymousBatchTest { 11 | 12 | public class MockHttpResponseGenerator implements HttpCalloutMock { 13 | private String response{get;set;} 14 | private Integer status{get;set;} 15 | public MockHttpResponseGenerator(String response, Integer status){ 16 | this.response = response; 17 | this.status = status; 18 | } 19 | 20 | public HTTPResponse respond(HTTPRequest req) { 21 | HttpResponse res = new HttpResponse(); 22 | res.setHeader('Content-Type','text/xml; charset=utf-8'); 23 | res.setBody(this.response); 24 | res.setStatusCode(this.status); 25 | return res; 26 | } 27 | } 28 | 29 | @testsetup 30 | private static void testSetup(){ 31 | insert new Account(Name = 'Test'); 32 | } 33 | 34 | @IsTest 35 | private static void test_method_batch_execution(){ 36 | Test.startTest(); 37 | Test.setMock(HttpCalloutMock.class, 38 | new MockHttpResponseGenerator('Invalid response', 500)); 39 | ExecuteAnonymousBatch btc = new ExecuteAnonymousBatch('Select Id From Account','update ID_LIST;',true); 40 | Database.executeBatch(btc, 1); 41 | Test.stopTest(); 42 | } 43 | 44 | @IsTest 45 | private static void test_method_batch_execute(){ 46 | Test.startTest(); 47 | 48 | //Invalid Http Status Code 49 | Test.setMock(HttpCalloutMock.class, 50 | new MockHttpResponseGenerator('Invalid response', 500)); 51 | ExecuteAnonymousBatch btc = new ExecuteAnonymousBatch('Select Id From Account','update ID_LIST;',true); 52 | btc.execute(null, [Select Id From Account]); 53 | System.assert(btc.errors.isEmpty() == false, 'Why empty?'); 54 | System.assert(btc.errors[0].contains('Unexpected server response'), 'Invalid error format: '+btc.errors); 55 | 56 | //Parsing error 57 | Test.setMock(HttpCalloutMock.class, 58 | new MockHttpResponseGenerator('' 59 | +'' 60 | +'' 61 | +'1Compilation Problem' 62 | +'false' 63 | +'' 64 | +'5' 65 | +'false' 66 | +'', 200)); 67 | btc = new ExecuteAnonymousBatch('Select Id From Account','update ID_LIST;',true); 68 | btc.execute(null, [Select Id From Account]); 69 | System.assert(btc.errors.isEmpty() == false, 'Why empty?'); 70 | System.assert(btc.errors[0].contains('Compilation Problem'), 'Invalid error format: '+btc.errors); 71 | 72 | //Code Exception 73 | Test.setMock(HttpCalloutMock.class, 74 | new MockHttpResponseGenerator('' 75 | +'' 76 | +'' 77 | +'1' 78 | +'true' 79 | +'Exception Thrown' 80 | +'Stack Trace5' 81 | +'false' 82 | +'', 200)); 83 | btc = new ExecuteAnonymousBatch('Select Id From Account','update ID_LIST;',true); 84 | btc.execute(null, [Select Id From Account]); 85 | System.assert(btc.errors.isEmpty() == false, 'Why empty?'); 86 | System.assert(btc.errors[0].contains('Exception Thrown') 87 | && btc.errors[0].contains('Stack Trace'), 'Invalid error format: '+btc.errors); 88 | 89 | //Everything is gonna be alright 90 | Test.setMock(HttpCalloutMock.class, 91 | new MockHttpResponseGenerator('' 92 | +'' 93 | +'' 94 | +'1' 95 | +'true' 96 | +'' 97 | +'5' 98 | +'true' 99 | +'', 200)); 100 | btc = new ExecuteAnonymousBatch('Select Id From Account','update ID_LIST;',true); 101 | btc.execute(null, [Select Id From Account]); 102 | System.assert(btc.errors.isEmpty() == true, 'Found errors: '+btc.errors); 103 | Test.stopTest(); 104 | } 105 | } -------------------------------------------------------------------------------- /ExecuteAnonymousBatch.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * Batch for Execute Anonymous 3 | * 4 | * @author Enrico Murru (http://enree.co) 5 | * @version 1.0 6 | * @description Execute custom code in batch. Needs a named credentials with OAuth 2.0 on running user. 7 | * Example 8 | * String script = 'List acList = [Select Id, Name From Account Where Id IN :ID_LIST];' 9 | * +'\nfor(Account acc : acList){' 10 | * +'\n acc.BillingCity = \'Gnite City\';' 11 | * +'\n}' 12 | * +'\n update acList;'; 13 | * ExecuteAnonymoutBatch batch = new ExecuteAnonymoutBatch('Select Id From Account',script, true); 14 | * Database.executeBatch(batch, 100); 15 | * @history 16 | * 2017-08-02 : Enrico Murru - Original version 17 | */ 18 | public class ExecuteAnonymousBatch implements Database.Batchable, Database.AllowsCallouts, Database.Stateful { 19 | private static final String API_VERSION = '40.0'; 20 | private static final String NAMED_CREDENTIAL = 'EXECUTE_ANONYMOUS'; 21 | 22 | private String executeScript{get;set;} 23 | private String soqlQuery{get;set;} 24 | @testVisible 25 | private List errors{get;set;} 26 | private Boolean sendEmail{get;set;} 27 | 28 | /* 29 | * Constructor 30 | * @param soqlQuery - SOQL query to be issued 31 | * @param executeScript - Apex script to be executed: the script has the "ID_LIST" list of type List 32 | * @param sendEmail - send a finish email 33 | */ 34 | public ExecuteAnonymousBatch(String soqlQuery, String executeScript, Boolean sendEmail){ 35 | this.executeScript = executeScript; 36 | this.soqlQuery = soqlQuery; 37 | this.errors = new List(); 38 | this.sendEmail = sendEmail; 39 | } 40 | 41 | /* 42 | * Batchable Start method 43 | */ 44 | public Database.QueryLocator start(Database.BatchableContext BC){ 45 | return Database.getQueryLocator(this.soqlQuery); 46 | } 47 | 48 | /* 49 | * Executes the anonymous code on the batch objects. 50 | * The original script is "augmented" by the ID_List variable (of type List) 51 | * that contains the list of current batch's ids. 52 | */ 53 | public void execute(Database.BatchableContext BC, List scope){ 54 | List scopeIds = new List(); 55 | for(Sobject obj : scope){ 56 | scopeIds.add((ID)obj.get('Id')); 57 | } 58 | 59 | String idList = '\''+String.join(scopeIds, '\',\'')+'\''; 60 | String script = 'List ID_LIST = new List{' 61 | + idList 62 | + '};\n' 63 | + this.executeScript; 64 | String result = executeAnonymous(script); 65 | if(String.isNotBlank(result)){ 66 | errors.add(idList+': '+result); 67 | } 68 | } 69 | 70 | /* 71 | * Sends an email when done 72 | */ 73 | public void finish(Database.BatchableContext BC) { 74 | 75 | if(sendEmail == true){ 76 | String subject = 'Elaboration completed: '; 77 | if(this.errors.isEmpty()){ 78 | subject += ' no errors.'; 79 | }else{ 80 | subject += ' with '+this.errors.size()+' errors.'; 81 | } 82 | String body = 'Query: \n\t' 83 | +this.soqlQuery 84 | +'\nExecute anonymous code: \n\t' 85 | +this.executeScript.replace('\n','\n\t') 86 | +'\nErrors:\n\t' 87 | +String.join(this.errors,'\n\t') 88 | +'\n\n\nSent by: '+userinfo.getUserName(); 89 | sendEmail(subject,body); 90 | } 91 | } 92 | 93 | /* 94 | * Executes the execute anonymous script. 95 | * Uses a named credential to get a valid session ID. 96 | * @param script - script to be executed (contains the ID_LIST variable) 97 | * @return (String) error message or null in case of success 98 | */ 99 | private static String executeAnonymous(String script){ 100 | String apexNS = 'http://soap.sforce.com/2006/08/apex'; 101 | String soapNS = 'http://schemas.xmlsoap.org/soap/envelope/'; 102 | String url = 'callout:'+NAMED_CREDENTIAL+'/services/Soap/s/'+API_VERSION; 103 | //SOAP call gives you the possibility to get the debuglog as well 104 | String body = '' 105 | +'' 106 | +'' 107 | +'{!$Credential.OAuthToken}' 108 | +'' 109 | +'' 110 | +'' 111 | +'' 112 | +'' 115 | +'' 116 | +'' 117 | +''; 118 | Http h = new Http(); 119 | HttpRequest request = new HttpRequest(); 120 | request.setTimeout(120000); 121 | request.setMethod('POST'); 122 | request.setHeader('Content-Type','text/xml; charset=utf-8'); 123 | request.setHeader('SOAPAction','executeAnonymous'); 124 | 125 | request.setEndpoint(url); 126 | request.setBody(body); 127 | try{ 128 | HttpResponse resp = h.send(request); 129 | if(resp.getStatusCode() != 200){ 130 | return 'Unexpected server response ['+resp.getStatusCode()+']: '+resp.getBody(); 131 | } 132 | 133 | Dom.Document doc = resp.getBodyDocument(); 134 | Dom.XMLNode rootNode = doc.getRootElement(); 135 | Dom.XMLNode bodyNode = rootNode.getChildElement('Body', soapNS); 136 | Dom.XMLNode executeAnonymousResponseNode = bodyNode.getChildElement('executeAnonymousResponse', apexNS); 137 | Dom.XMLNode resultNode = executeAnonymousResponseNode.getChildElement('result', apexNS); 138 | String success = resultNode.getChildElement('success',apexNS).getText(); 139 | if(success != 'true'){ 140 | String rslt = (resultNode.getChildElement('exceptionMessage', apexNS).getTExt() +' -- ' 141 | + resultNode.getChildElement('exceptionStackTrace',apexNS).getText()) 142 | + ((resultNode.getChildElement('compiled',apexNS).getText() == 'false')? 143 | ('Compilation problem: '+resultNode.getChildElement('compileProblem',apexNS).getText() 144 | + ' Line: '+resultNode.getChildElement('line',apexNS).getText() 145 | + ' Column: '+resultNode.getChildElement('column',apexNS).getText()):''); 146 | return rslt; 147 | } 148 | 149 | return null; 150 | }catch(Exception e){ 151 | return 'Fatal exception on batch code: '+e.getMessage()+' | '+e.getStackTraceString().replaceAll('\\n',' >> '); 152 | } 153 | } 154 | 155 | /** 156 | * Sends an email to current user 157 | * @param subject - email's subject 158 | * @param message - email's body 159 | */ 160 | private static void sendEmail(String subject, String message) 161 | { 162 | Messaging.SingleEmailMessage mail=new Messaging.SingleEmailMessage(); 163 | mail.setTargetObjectId(UserInfo.getUserId()); 164 | mail.setSaveAsActivity(false); 165 | subject = '[Execute Anonymous Batch] '+subject; 166 | mail.setSubject(subject); 167 | mail.setPlainTextBody(message); 168 | if(Test.isRunningTest()==false){ 169 | Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); 170 | } 171 | } 172 | } 173 | --------------------------------------------------------------------------------