Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
DamianKolasa
Participant
593

Background

This blog aims to recreate groovyIDE using tools and platforms that every SAP developer should have access to.
My goal was to avoid local development (using IntelliJ or Eclipse), as setting up such an environment is often difficult due to employer restrictions.

What do you need?

To pull this off, you need:

  • Developer access to any Cloud Integration (CI) tenant
    (I recommend a separate tenant, ideally DEMO or TRIAL)

  • Developer access to an ABAP system that can connect to CI

Nice to have:

  • abapGit – as there is a repository with the backend report

Setup

Cloud integration

Cloud Integration is used as the execution engine. In practice, this is a CI-based IDE exposed as a REST service. I would like to thank Vadim Klimov for exploring this idea. His blog (and the unavailability of groovyIDE) inspired me to create this solution. At its core, this is a single iFlow with a few steps. It uses GroovyShell to execute a script passed in the request payload.

Integration flow

DamianKolasa_0-1769781434648.png

As you can see, the iFlow is simple and straightforward. It performs the following steps:

  1. Decodes the payload, script, headers, and properties from the request
  2. Dynamically executes the Groovy script
  3. Creates a well-formatted JSON response, encoding all data using Base64

There is also an exception subprocess used to catch Groovy execution errors and return them in a structured JSON format.

Request

The interface accepts a JSON payload with four parameters, all Base64-encoded:

{
  "inputScript": "<base64encoded>",
  "inputPayload": "<base64encoded>",
  "inputParams": "<base64encoded>",
  "inputHdrs": "<base64encoded>"
}

After decoding, parameters (properties) and headers follow this format:

Name=Value;Name2=Value2;...

Cloud Integration encodes them in the same way in the final response (both in success and error cases).

Decoder
import com.sap.gateway.ip.core.customdev.util.Message;
import groovy.json.JsonSlurper;

Message processData(Message message) {
    def body = message.getBody(java.io.Reader)
    def inputJSON = new JsonSlurper().parse(body)
    String inputHdrs = inputJSON.inputHdrs
    if(inputHdrs ){
        byte[] decodedHdrs = inputHdrs.decodeBase64()
        inputHdrs = new String(decodedHdrs)
        inputHdrs.split(';').each{ 
            def param_val_pair = it.split('=')
            message.setHeader(param_val_pair[0],param_val_pair[1])
        }
    }

    String inputParams = inputJSON.inputParams
    if(inputParams){
        byte[] decodedParams = inputParams.decodeBase64()
        inputParams = new String(decodedParams)
        inputParams.split(';').each{ 
            def param_val_pair = it.split('=')
            message.setProperty(param_val_pair[0],param_val_pair[1])
        }
    }

    String inputScript = inputJSON.inputScript
    if(inputScript){
        message.setProperty("inputScript",inputScript)
    }

    String inputPayload = inputJSON.inputPayload
    if(inputPayload){
        byte[] decodedPayload = inputPayload.decodeBase64()
        inputPayload = new String(decodedPayload)
        message.setBody(inputPayload)
    }

    return message
}

The decoder script processes headers and properties and sets them for the current message processing. The payload is decoded and set as plain text (String) in the message body. The actual Groovy script is stored as a separate property named "inputScript" and is later used in the execution step.

Script execution
import com.sap.gateway.ip.core.customdev.util.Message
import java.io.StringWriter
import java.io.PrintWriter

Message processData(Message message) {
    //Get script
    String inputPayload = message.getProperty("inputScript")
    byte[] decodedPayload = inputPayload.decodeBase64()
    String scriptCode = new String(decodedPayload, "UTF-8")
    //Setup output capture
    StringWriter logWriter = new StringWriter()
    PrintWriter printWriter = new PrintWriter(logWriter)    
    //Create Binding and inject 'out' for println support
    Binding binding = new Binding()
    binding.setVariable("out", printWriter)     
    //Initialize shell
    GroovyShell shell = new GroovyShell(binding)
    Script script = shell.parse(scriptCode)
    //call script
    message = script.processData(message)    
    //Save logs to property 
    printWriter.flush()
    message.setProperty("consoleLog", logWriter.toString())
    
    return message
}

The main difference between this script and the one provided in Vadim’s blog is the use of an additional binding with a PrintWriter object. This allows capturing console output (println statements) and returning it to the caller as part of the response.

Wrapper
import com.sap.gateway.ip.core.customdev.util.Message

Message processData(Message message) {
    def consoleLog = message.getProperties().get("consoleLog") as String
    message.setProperty("consoleLog", null)
    def mapProperties = message.getProperties();
    def mapHeaders = message.getHeaders();
    def body = message.getBody(java.lang.String) as String;
    String Pbody = ""
    String Hbody = ""
    //encode props and hdrs to name=value; format
    mapProperties.each { it -> Pbody += "$it.key = $it.value;" }
    mapHeaders.each { it -> Hbody += "$it.key = $it.value;" }
    def builderJSON = new groovy.json.JsonBuilder()
    builderJSON{
        properties(Pbody.bytes.encodeBase64().toString())
        headers(Hbody.bytes.encodeBase64().toString())
        payload(body.bytes.encodeBase64().toString())
        console(consoleLog.bytes.encodeBase64().toString())
    }
    message.setBody(builderJSON.toPrettyString())
    return message
}

The wrapper executes the decoded Groovy script and captures console output. After execution, all logs are stored in a message property and later included in the JSON response.

Error Handler
import com.sap.gateway.ip.core.customdev.util.Message;
def Message processData(Message message) {
    def mapProperties = message.getProperties();
    def body = message.getBody(java.lang.String) as String;
    String Pbody = ""
    String Hbody = ""
    mapProperties.each { it -> Pbody += "$it.key = $it.value; \n" }
    def ex = mapProperties.get("CamelExceptionCaught");
    def mapHeaders = message.getHeaders();
    mapHeaders.each { it -> Hbody += "$it.key = $it.value; \n" }
    def builderJSON = new groovy.json.JsonBuilder()
    builderJSON{
        properties(Pbody.bytes.encodeBase64().toString())
        headers(Hbody.bytes.encodeBase64().toString())
        payload(body.bytes.encodeBase64().toString())
        error(ex.message.bytes.encodeBase64().toString())
    }
    message.setHeader("CamelHttpResponseCode", "400");
    message.setBody(builderJSON.toPrettyString());
    return message;
}

The error handler is very similar to the wrapper but additionally captures execution exceptions and includes them in the response. This is important during testing, as errors such as missing variables or null references are common. The main idea of this tool is that any Groovy-related error is returned to the caller. To distinguish between script errors and unexpected system failures (HTTP 500), the service returns HTTP 400 for Groovy execution errors. This makes sense, as the script itself is part of the request.

ABAP

There is not much logic on the ABAP side: a table, a data element, and a report.

DamianKolasa_1-1769781876826.png

The table ZSAPERGROOVYTST serves as a test case repository. In the current version, it stores only a cache – the last modified script, payload, and input properties.

DamianKolasa_2-1769782115545.png

All text inputs are stored as Base64-encoded strings. Headers and properties are stored as name=value pairs, following the same format used by Cloud Integration.

DamianKolasa_3-1769782245582.png

When the program is cancelled, it closes without updating the cache table. When exited normally, the current editor and ALV content is stored as a TMP record for the current user and loaded the next time the IDE is opened.

I will not go into ABAP-specific details here. You can check the Git repository or import the objects into your system and test the solution yourself.

Connection

Connectivity is based on an RFC destination (type G) configured in transaction SM59. The destination name is stored in lcl_groovy=>cv_destination and defaults to "CPI_GROOVY". 

DamianKolasa_0-1769782673129.png

You can adjust it to your needs in method "call_script".

  METHOD call_script.
    cl_http_client=>create_by_destination( EXPORTING destination = cv_destination
                                           IMPORTING client      = DATA(lo_http) ).
    DATA(ls_current_screen_data) = me->get_screen_data( ).
    DATA(lv_payload) =
    |\{| &&
      |"inputScript":"{ ls_current_screen_data-script  }",| &&
      |"inputPayload":"{ ls_current_screen_data-payload  }",| &&
      |"inputParams":"{ ls_current_screen_data-props  }",| &&
      |"inputHdrs":"{ ls_current_screen_data-hdrs  }"| &&
    |\}|.
    lo_http->request->set_cdata( lv_payload ).
    lo_http->send( ).
    lo_http->receive( ).
    lo_http->response->get_status( IMPORTING code = DATA(lv_status_code) ).
    IF lv_status_code EQ 500.
      MESSAGE 'Internal server error occured' TYPE 'I' DISPLAY LIKE 'E'.
    ELSE.
      me->parse_service_call_response( iv_response = lo_http->response->get_cdata( ) iv_code = lv_status_code ).
    ENDIF.
  ENDMETHOD.

Testing

Let’s start with the default script. It simply rewrites the input to the output and adds a console log line: "test1".

test_groovy_1.gif

Now let’s break something – for example, by dividing by zero.

test_groovy_2.gif

As expected, the error is visible in the console output:

"java.lang.Exception: java.lang.ArithmeticException: Division by zero @ line 7 in ExecuteScript.groovy".

This demonstrates the usage of JsonSlurper and JsonBuilder, as well as property and header handling.

test_groovy_3.gif

Security

This solution executes dynamic Groovy code using GroovyShell and therefore must be treated as a development and testing tool only. Access to the integration flow should be restricted, and the endpoint must not be connected to productive systems. The script executes with the runtime permissions of Cloud Integration and is not sandboxed. For this reason, the tool should be deployed only in isolated CI tenants (for example TRIAL tenant). 

External links

 

 

2 Comments