Thursday, November 27, 2014

Adding Eval() support to Apex

In the Codefriar blog post EVAL() in Apex Kevin presents a technique to to allow programmatic evaluation of an Apex string and the extraction or the result via an Exceptions message. Here I present an alternative approach using anonymous Apex and the debug log in place of the intentional exception.

Reasons you may or may not want to eval() Apex

Benefits

  • Where a rollback may be required the separate context via the callout doesn't require the invoking Apex to rollback. You can still progress and make further callouts and subsequent DML operations.
  • JSON can be used in the response message to return structured data.
  • An odd way of increasing limits. E.g. Each anonymous apex context gets a new set of limits.
  • You can handle classes of exception that would otherwise be uncatchable, such as System.LimitException and System.ProcedureException

Disadvantages

  • The potential for security issues depending on how the anonymous Apex is composed.
  • The requirement for users to have sufficient permissions to call executeAnonymous. Typically this means having “Author Apex” or running with the restricted access as per Executing Anonymous Apex through the API and the “Author Apex” Permission.
  • The need to parse the DEBUG log message out of the response to get the result. Other code may also write DEBUG ERROR messages, which will interfere with parsing the response. This could be addressed by improving the parsing of the Apex log. I.e. Extract all the USER_DEBUG entries to a list and then read the last one. Another alternative is to use delimiters in the debug message to make it easier to parse out.
  • Each eval() call is a callout back to the Salesforce web services. This creates limits on the number of evals that can be made. (Don't forget to add the Remote Site Setting to allow the callout)

API Background

Both the Tooling API and the older Apex API provide an executeAnonymous web method. The main difference is that the older Apex API will return the resulting Apex debug log in a SOAP header. With the tooling API the Apex debug log is generated but needs to be queried separately from the ApexLog. The older Apex API becomes more attractive here as one API call can execute the dynamic Apex code and return the log that can contains the output of the call.

By setting the DebuggingHeader correctly the size of the Apex debug log can be kept to a minimum. For example, getting on the ERROR level Apex_code messages makes extracting the required USER_DEBUG output easier and reduces the amount of superfluous data returned.

It should be noted that using executeAnonymous won't execute in the same context the way a Javascript eval() does. Any inputs need to be explicitly included in the Apex string to execute. Also, any return values need to be returned via the log and then parsed out to bring them into the context of the calling Apex.

Note that the current native Salesforce version of WSDL2Apex won't read the responses DebuggingInfo soap header. Instead these need to be read via an HttpRequest and parsing the resulting XML response.

Sample Raw SOAP Request/Response

This is sent to the Apex API at https://na5.salesforce.com/services/Soap/s/31.0

Request

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
   <soapenv:Header>
      <apex:DebuggingHeader>
         <apex:categories>
            <apex:category>Apex_code</apex:category>
            <apex:level>ERROR</apex:level>
         </apex:categories>
         <apex:debugLevel>NONE</apex:debugLevel>
      </apex:DebuggingHeader>
      <apex:SessionHeader>
         <apex:sessionId>00D700000000001!AQoAQGrYU000NotARealSessionIdUseYourOwnswh4QHmaPFm2fRDgk1zuXcVvWTfB4L9n7BJf</apex:sessionId>
      </apex:SessionHeader>
   </soapenv:Header>
   <soapenv:Body>
      <apex:executeAnonymous>
         <apex:String>Integer i = 314159; System.debug(LoggingLevel.Error, i);</apex:String>
      </apex:executeAnonymous>
   </soapenv:Body>
</soapenv:Envelope>

Response

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://soap.sforce.com/2006/08/apex" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <soapenv:Header>
      <DebuggingInfo>
         <debugLog>31.0 APEX_CODE,ERROR
Execute Anonymous: Integer i = 314159; System.debug(LoggingLevel.Error, i);
13:24:24.027 (27564504)|EXECUTION_STARTED
13:24:24.027 (27573409)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
13:24:24.028 (28065096)|USER_DEBUG|[1]|ERROR|314159
13:24:24.028 (28098385)|CODE_UNIT_FINISHED|execute_anonymous_apex
13:24:24.029 (29024086)|EXECUTION_FINISHED</debugLog>
      </DebuggingInfo>
   </soapenv:Header>
   <soapenv:Body>
      <executeAnonymousResponse>
         <result>
            <column>-1</column>
            <compileProblem xsi:nil="true"/>
            <compiled>true</compiled>
            <exceptionMessage xsi:nil="true"/>
            <exceptionStackTrace xsi:nil="true"/>
            <line>-1</line>
            <success>true</success>
         </result>
      </executeAnonymousResponse>
   </soapenv:Body>
</soapenv:Envelope>

You can see the required output in the response next to |USER_DEBUG|[1]|ERROR|.

Given this the basic process becomes:

  • Build up the anonymous Apex string including any required inputs. Use a System.debug(LoggingLevel.Error, 'output here'); to send back the output data.
  • Call the Apex API executeAnonymous web method and capture the DebuggingInfo soap header in the response
  • Parse the USER_DEBUG Error message out of the Apex Log.
  • Convert the resulting string to the target data type if required.

Examples

Here are several examples showing parsing various data types from the Apex log.

 string output = soapSforceCom200608Apex.evalString('string first = \'foo\'; string second = \'bar\'; string result = first + second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals('foobar', output);
 System.debug(output);
 
 integer output = soapSforceCom200608Apex.evalInteger('integer first = 1; integer second = 5; integer result = first + second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals(6, output);
 System.debug(output);
 
 boolean output = soapSforceCom200608Apex.evalBoolean('boolean first = true; boolean second = false; boolean result = first || second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals(true, output);
 System.debug(output);

 string outputJson = soapSforceCom200608Apex.evalString('List<object> result = new List<object>(); result.add(\'foo\'); result.add(12345); System.debug(LoggingLevel.Error, JSON.serialize(result));');
 System.debug(outputJson);
 List<Object> result = 
    (List<Object>)JSON.deserializeUntyped(outputJson);
 System.assertEquals(2, result.size());
 System.assertEquals('foo', result[0]);
 System.assertEquals(12345, result[1]);

Modified Apex API wrapper class

This wrapper around the Apex API SOAP web service uses HTTP Requests so that the DebuggingHeader can be extracted from the response. I've added several methods to execute the eval requests and parse out the expected response type.

Fun Addendum

Question: What happens if you have you Anonymous Apex do a recursive eval against the Anonymous API again?

E.g.:

string evalScript = 'string output = soapSforceCom200608Apex.evalString(\'string input = \\\'foo\\\'; System.debug(LoggingLevel.Error, input);\');';
System.debug(evalScript);

string output = soapSforceCom200608Apex.evalString(evalScript);
System.debug(output);

Answer:
You get a ystem.CalloutException: Callout loop not allowed response.

There is a Sfdc-Stack-Depth header that prevents the recursive calls. See Callout loop not allowed error


See also: