EDUCAÇÃO E TECNOLOGIA

Create multipart/form-data service with forwarded basic authorization using JIRA REST API

Recently I’ve received interesting requirement: integrate SAP system with Atlassian’s JIRA software to enable creating new issues directly from SAP GUI. Because this software provides its own REST API it seemed to be an easy task. However, creating new issue is not enough, because user reporting some kind of an issue would like to attach at least some screenshots and maybe other files documenting what actually happened/what should be fixed. And this part appeared to be more difficult than I had expected… So in this blog, I’ll explain my solution to multipart/form-data requests created on SAP PO.

JIRA’s API is really well described and if you get stuck there’s a lot of useful information on their community side. Basically I’d like to focus on adding attachment method. For my little project I had to implement also methods creating issue and retrieving dictionary data (projects, issue types, custom fields), but these are simple GET/POST methods and won’t be described here.

Method is available at path api/2/issue/{issueIdOrKey}/attachments. If you are familiar with HTTP_AAE adapter at first glance you can tell that this adapter won’t work for us. We need to specify dynamic parameter inside method’s path, which is not possible using http_aae adapter. That’s why I decided to use REST instead. Regarding method itself it requires multipart-form-data, with binary payload (base64 won’t work), and also so

What we want to achieve is to get at the adapters’ output something looking like this:

POST /rest/api/2/issue/{issue}/attachments HTTP/1.1
Host: {our.jira.host}
X-Atlassian-Token: nocheck
Authorization: Basic {basic authorization string}
Content-Type: multipart/form-data; boundary={our_boundary}
Content-Length: <calculated length>

–{our_boundary}
Content-Disposition: form-data; name=”file”; filename=”filename.ext”
Content-Type: <Content-Type header here>

(data in binary)
–{our_boundary}–

Where:

  • X-Atlassian-Token: nocheck is required by JIRA spec;
  • Authorization: Basic {basic authorization string} is something we want to pass from ERP to authenticate by user who is actually creating issue. We can’t use technical user here.
  • Content-Type: multipart/form-data; boundary={our_boundary} this is essential for this posting method. Boundary must be unique and should be calculated on the fly (this is what i.e. Postman does and HTTP_AAE adapter).

Actual content must be included between boundary indicators, starting with –{boundary} and ending with –{boundary}–. We can attach more than one file, but each one of them must be included into separated boundaries like that.

Ok, so we know now what we want to send out from PO system. Now – how to do it? Because, in my case, issue should be created in a dialog SAP GUI session, i decided to use synchronous RFC functions. So the goal is to have synchronous RFC->REST interface, from ERP to JIRA system.

ABAP side

First thing first – need an entry point for SAP request. I created simple, RFC enabled, function module. Module is just an empty interface for RFC call so no ABAP code is needed:

IS_ATTACHMENT has following structure:

Of course you can modify it to have all fields as strings (I have a strange habit to put existing text-based component types instead of using string everywhere), or predefined custom/standard types. This can be also used later to replace structure with table type and send multiple attachments at once. Data is base64 encoded binary stream.

Regarding interface’s parameters:

  • iv_issue_key – issue number/key we want to add attachment to;
  • iv_authstring – concatenated user name and password, separated by “:” and encoded in base64. This is something adapter can create itself, but we want to pass data entered by the user on ERP side instead of hardcoding it on adapter level;
  • is_attachment – attachments structure with following fields:
    • filename – name of the file, which will be passed to Content-Disposition for data stream in included in this structure;
    • mimetype – mimetype, also passed to request data (between boundaries);
    • data – encoded, in base64, data stream;

For testing purposes simple test program would be useful, here you can find one I wrote (but you can try directly from se37 using some simple and short base64 string):

*&---------------------------------------------------------------------* *& Report ZTMP_TEST_JIRA_UPL *&---------------------------------------------------------------------* *& *&---------------------------------------------------------------------* REPORT ZTMP_TEST_JIRA_UPL. SELECTION-SCREEN BEGIN OF BLOCK BL0 WITH FRAME TITLE TEXT-T00. SELECTION-SCREEN BEGIN OF BLOCK BL1 WITH FRAME TITLE TEXT-T01. PARAMETERS: p_unam TYPE string LOWER CASE, p_pass TYPE string LOWER CASE. SELECTION-SCREEN END OF BLOCK BL1. SELECTION-SCREEN BEGIN OF BLOCK BL2 WITH FRAME TITLE TEXT-T02. PARAMETERS: p_tick TYPE string, p_file TYPE string LOWER CASE. SELECTION-SCREEN END OF BLOCK BL2. SELECTION-SCREEN END OF BLOCK Bl0. AT SELECTION-SCREEN OUTPUT. "have a little decency and hide password field LOOP AT SCREEN. IF screen-name = 'P_PASS'. screen-invisible = 1. MODIFY SCREEN. ENDIF. ENDLOOP. AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_file. "file selection help DATA: gv_rc TYPE i, gt_file_table TYPE TABLE OF file_table. cl_gui_frontend_services=>file_open_dialog( CHANGING file_table = gt_file_table rc = gv_rc EXCEPTIONS others = 1 ). IF gt_file_table IS NOT INITIAL. p_file = gt_file_table[ 1 ]-filename. ENDIF. START-OF-SELECTION. IF p_file IS NOT INITIAL. IF p_unam IS NOT INITIAL AND p_pass IS NOT INITIAL. "upload file DATA: gv_length TYPE i, gv_string TYPE xstring, gt_datatab TYPE TABLE OF x255. cl_gui_frontend_services=>gui_upload( EXPORTING filename = p_file filetype = 'BIN' IMPORTING filelength = gv_length CHANGING data_tab = gt_datatab EXCEPTIONS others = 1 ). IF sy-subrc = 0. "get mime type from file DATA: gv_mimetype TYPE SKWF_MIME. CALL FUNCTION 'SKWF_MIMETYPE_OF_FILE_GET' EXPORTING filename = CONV SKWF_FILNM( p_file ) IMPORTING mimetype = gv_mimetype. "encode binary data to base64 "part 1 CALL FUNCTION 'SCMS_BINARY_TO_XSTRING' EXPORTING input_length = gv_length IMPORTING buffer = gv_string tables binary_tab = gt_datatab EXCEPTIONS failed = 1 . IF sy-subrc <> 0. "hammer time ENDIF. "and part 2 DATA: gv_base64 TYPE string. gv_length = xstrlen( gv_string ). CALL FUNCTION 'SSFC_BASE64_ENCODE' EXPORTING bindata = gv_string binleng = gv_length IMPORTING b64data = gv_base64. "authorization encoding DATA gv_auth_xstring TYPE xstring. DATA(gv_auth_string) = p_unam && `:` && p_pass. DATA: gv_auth_string_base64 TYPE string. gv_auth_xstring = gv_auth_string. CALL FUNCTION 'SCMS_STRING_TO_XSTRING' EXPORTING text = gv_auth_string IMPORTING buffer = gv_auth_xstring. CALL FUNCTION 'SCMS_BASE64_ENCODE_STR' EXPORTING input = gv_auth_xstring IMPORTING output = gv_auth_string_base64. "get filename from filepath SPLIT p_file AT `\` INTO TABLE DATA(gt_file). IF gt_file IS NOT INITIAL. DATA(gv_filename) = gt_file[ lines( gt_file ) ]. ELSE. gv_filename = sy-datum && '_file'. ENDIF. "call RFC fm DATA gv_http_code TYPE string. CALL FUNCTION 'ZFM_JIRA_ADD_ATTACHMENT_RFC' DESTINATION 'PI_RFC' EXPORTING iv_issue_key = p_tick iv_authstring = gv_auth_string_base64 is_attachment = VALUE zjira_attachment_s( filename = gv_filename mimetype = gv_mimetype data = gv_base64 ) IMPORTING ev_http_code = gv_http_code EXCEPTIONS system_failure = 1. DATA(gv_msg) = sy-subrc && ` - sy-subrc. Has status : ` && gv_http_code. MESSAGE gv_msg TYPE 'S'. ENDIF. ELSE. MESSAGE 'Enter credentials' TYPE 'S' DISPLAY LIKE 'E'. ENDIF. ELSE. MESSAGE 'Select file' TYPE 'S' DISPLAY LIKE 'E'. ENDIF.

PO side

Enterprise service repository

There’re standard steps and objects to be created here, nothing new and fancy:

  • Import FM interface
  • Message type for POST request
  • Message type for POST response
  • Service interface for POST request
  • JAVA mapping
  • Operation mapping

Importing FM interface from SAP

Under selected Software Component click on imported objects, put your credentials, select RFC node and download previously created FM. In my case:

Message type for POST request

Because I won’t use xml payload from service interface, and JAVA mapping will create whole content which should be send by adapter to endpoint, it really doesn’t matter how inbound request looks like. In my case it’s an empty message:

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:jira:projects" targetNamespace="urn:jira:projects"> <xsd:element name="EmptyRequest" type="empty" /> <xsd:simpleType name="empty"> <xsd:restriction base="xsd:string" /> </xsd:simpleType> </xsd:schema>

Message type for POST response

You can find response structure in API documentation. However for purposes of this interface we don’t need these details. What I’m interested in is status code (HTTP status) which will indicate what happened with my request (statuses are also available in documentation). So what I used instead:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="qualified"> <xs:element name="root"> <xs:complexType> <xs:sequence> <xs:element type="xs:string" name="status" minOccurs="0" maxOccurs="1" /> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>

Request JAVA mapping

In order to generate content properly I used JAVA mapping. What needs to be filled here is HTTP request body and HTTP header fields to be used later by REST adapter – this is done via dynamic configuration:

package com.zooplus.mapping; import java.io.*; import com.sap.aii.mapping.api.*; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import java.util.Base64; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class AddAttachmentJIRAMap extends AbstractTransformation { private static final String LINE_FEED = "\r\n"; public void transform(TransformationInput inStream, TransformationOutput outStream) throws StreamTransformationException { AbstractTrace trace = (AbstractTrace) getTrace(); trace.addInfo("attachment mapping started"); String boundary = "--r_BWbX54zeRleg";//TODO: auto generate String body = ""; String base64AuthString = ""; String filename = ""; String contentType = ""; String base64File = ""; String issueName = ""; //parse input stream InputStream inputstream = inStream.getInputPayload().getInputStream(); DocumentBuilderFactory docBuildFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { //get data from input xml docBuilder = docBuildFactory.newDocumentBuilder(); Document doc = docBuilder.parse(inputstream); NodeList oElementsAuthString = doc.getElementsByTagName("IV_AUTHSTRING"); NodeList oElementIssueName = doc.getElementsByTagName("IV_ISSUE_KEY"); NodeList oElementFilename = doc.getElementsByTagName("FILENAME"); NodeList oElementContentType = doc.getElementsByTagName("MIMETYPE"); NodeList oElementBase64File = doc.getElementsByTagName("DATA"); base64AuthString = oElementsAuthString.item(0).getTextContent(); issueName = oElementIssueName.item(0).getTextContent(); filename = oElementFilename.item(0).getTextContent(); contentType = oElementContentType.item(0).getTextContent(); base64File = oElementBase64File.item(0).getTextContent(); } catch (ParserConfigurationException e1) { e1.printStackTrace(); } catch (SAXException | IOException e1) { e1.printStackTrace(); } //set dynamic conf String namespace = "http://sap.com/xi/XI/System/REST"; DynamicConfiguration DynConfig = inStream.getDynamicConfiguration(); DynamicConfigurationKey key = DynamicConfigurationKey.create(namespace, "ISSUE_NO");//for dynamic URL DynConfig.put(key,issueName); key = DynamicConfigurationKey.create(namespace, "BOUNDARY_VAL");//for header DynConfig.put(key, "multipart/form-data; boundary=" + boundary); key = DynamicConfigurationKey.create(namespace, "AUTH_STRING");//for authorization DynConfig.put(key, "Basic " + base64AuthString); //set payload //decode base64 to byte table byte[] decodedBytes = Base64.getDecoder().decode(base64File); //create 1st part of a body body = "--" + boundary; body = this.addKey( "Content-Disposition", "form-data; name=\"file\"; filename=\"" + filename + "\"", body ); body = this.addKey( "Content-Type", contentType, body ); body = this.addLine( body ); body = this.addLine( body ); //3rd part (body end) String body_end = LINE_FEED + "--" + boundary + "--"; //write to output stream try { OutputStream outputstream = outStream.getOutputPayload().getOutputStream(); outputstream.write(body.getBytes()); outputstream.write(decodedBytes); outputstream.write(body_end.getBytes()); trace.addInfo("attachment mapping ended"); } catch (IOException e) { throw new StreamTransformationException(e.getMessage()); } } private String addKey(final String key, final String value, final String body){ final String body_c = body + LINE_FEED + key + ": " + value; return body_c; } private String addLine(final String body) { return body + LINE_FEED; } }

Response message mapping

This is quite simple – adapter passes HTTP status to predefined payload and this is passed to SAP:

Operation mapping

Put all the pieces together in an operation mapping:

Integration builder

Receiver communication channel

As a receiver channel we’ll use REST adapter channel:

At adapter-specific tab:

  • General – leave as it is by default. Leave Use Basic Authentication blank;
  • REST URL – we need to import dynamic configuration parameters and set POST URL:

  • REST Operation – set as POST;
  • Data Format – for request important is to set data format as Binary and leave Binary request Content-Type header empty (we’ll overwrite this later, if this is set it won’t be possible). Response set to Binary (adapter will overwrite it anyway):

  • HTTP Headers – here we need to set few parameters. X-Atlassian-Token is required by API, rest of them are transmitted from JAVA mapping by dynamic configuration:

  • Error handling – always overwrite response with predefined payload and use http_status as indicator.

iFlow

Create point-to-point scenario (RFC->REST):

Set some logging:

Check, activate and deploy. We should be good. So… what we expect to achieve? Including ERP side it should work as follows:

  1. ERP:
    1. File is uploaded as binary;
    2. Binary data stream is converted to encoded base64 string (let’s call it data_stream);
    3. User name and password are concatenated (user:pass string) and encoded (base64);
    4. data_stream and rest of parameters, read from local file (mime type, filename, authorization string (point 3)), are being sent via FM RFC interface to PO;
  2. PO:
    1. Mapping parses inbound message (XML FM structure) and corresponding parameters are read;
    2. mapping sets dynamic configuration parameters:
      1. ISSUE_NO – part of target url path, issue we want to update;
      2. BOUNDARY_VAL – calculated (in our example hard coded) boundary for multipart request;
      3. AUTH_STRING – authorization string to authenticate ourselves at target endpoint;
    3. request body is built in required format:
      1. –{boundary} is added at the beginning;
      2. Content-Disposition is set (filename according to sent filename for data_stream);
      3. Content-Type is set set to sent mime type;
      4. data_stream is decoded and write as binary payload;
      5. -{boundary}– is added as a closure tag;
    4. Payload is sent to adapter engine;
    5. Adapter reads dynamic configuration and sets:
      1. target URL (put ISSUE_NO);
      2. Authorization (according to AUTH_STRING);
      3. Content-Type (as multipart/form with boundary set by JAVA mapping);

As a results we should receive nice 200 status code and JSON file with saved attachment’s details (or other status, but still mapped to response, so we can produce meaningful message).

Easiest option is to use test program from previous step (or se37).

Positive scenario

Quick look at the issue:

How does it look in message monitor?

HTTP headers and target URL were changed successfully. What about payload? In Message Editor final message can be found and it looks exactly as we expected:

Negative scenario

Let’s try out a negative scenarios. Because I expect that user may enter incorrect credentials (probably most common error), let us find out if I can handle that on SAP GUI level and throw appropriate message (instead of throwing SYSTEM_ERROR or other shortdump).

Same program, almost same input data(put incorrect password):

CC passed http_status in prepared payload, and this was then sent to message mapping. sy-subrc equals 0, so no communication error happened (we can easily distinguish between PO communication error and endpoint errors: 401,403,500). How it looks on PO side? Check logs:

Message is a success – no errors happened during message processing, so this is expected result (status was handled correctly and passed to MM).

This is blog describes one possible approach using: REST adapter, JAVA mapping and dynamic configuration to handle authorizations (passed from ERP system).

There’re some others, really helpful blogs out there describing how to tackle this problem: