Skip to Content
Technical Articles

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

Business background

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.

 

Available API

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

 

Technical specification

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.

 

Development

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).

Testing

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).

Summary

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:

 

8 Comments
You must be Logged on to comment or reply to a post.
  • Thank you very much for your article, It has  been very helpful to understand  concept of Multipart Form Data .I am working  similar Requirement where  in I need to send  PDF as attachment  which Invoice Document  along  with  metadata  where  in which it Provides the details  such as  Invoice # Invoice Type and Customer as Json .Followed your way  of JAVA mapping.

     

    My scenario SAP Proxy-PI-Rest

    Rest Adapter Version 7.5 SP15

    here  is my sample data  in Validation  Step in ICO

    –WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name=”file”; filename=”test.pdf”
    Content-Type: application/pdf

    %PDF-1.4

    %%EOF

    –WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name=”metadata”
    Content-Type: application/json

    { “Customer”: “1234”, “Inv”: “12341”, “InvType”: “ZINV” }
    –WebKitFormBoundary7MA4YWxkTrZu0gW–

    Rest Adapter Config

    When it reaches the Target  ,I always get an error 400 Bad Request

    ‘Required request part ‘metadata’ is not present’.Not sure What I am missing here  .Any help greatly  appreciated  .

    Tried  with HTTP_AAE adapter  getting same Error messages .

    If I run  same data  using Postman  I can successfully  Post  data to URL

    Thanks,

    Madhu

     

     

     

    • Hello Madhu,

      Probably PO messes something with HTTP headers and/or request body (especially boundary parameter). I had similar issues (worked with postman and all requests via PO ended up with 400), best option to track problems is to use some kind of endpoint mockup and send full request from postman there and from PO, then compare both requests (their headers and bodies). Here’s an example service: https://beeceptor.com/. You can create dummy service and post requests using endpoint created by it. My suggestion is to:

      • change URL to created in beeceptor for postman, post a message (same way your’re doing it right now), save header and body;
      • do the same with REST adapter;
      • compare headers and bodies;

      Hope it will be helpful.

      Damian

       

      • Hello Damian ,

        Thanks  for Responding..  

        I tried matching  the  Postman (Header/Body)Structure  to  PO ,However our Rest adapter adds  additional messages  . little bit curious  on your screenshot for  Message Log  I see  addattachment mapping step is getting executed  .But I don’t see any mapping step getting executed   like that in my Log.I know  service interface  dummy  here  since  JAVA mapping  writing output stream .I was wondering  due to this  my http body not getting Passed  any thoughts  on this  and I am still  having  same Issue  .

           Receiver Log  :Looks like  only receiving  HTTP header no  body .

        Any suggestion in this regard  will  be helpful.

         

        Thanks,

        Madhu

         

        • Hi,

          Regarding CC configuration, please disable attachments. I can see that you set this to multipart/form-data (I don’t have such option, I guess it’s a matter of different adapter’s version) – if you want to use attachments please follow up Rajesh’ blog (it describes how to use attachments in multipart data).

          Regarding MM – if you iFlow and ICO are built properly, please try to refresh cache in PO administration panel. At this point it looks like you don’t have mapping configured (at least from log you attached).

          BR,

          Damian

        • Hi Damian  ,

          Boundary  did the  trick for me  .In the  REST adapter  HTTP header  tab the  Value for  Boundary should be  in quotes.I was  missing them all along!!.

          Thanks for your help  in the direction of boundary.

           

          Thanks,

          Madhu

          /
  • Damian Kolasa

    Nice one. Thanks for my blog mention😀.

     

    Well, I tried the same and get the below error. May need your inputs/ did I missing any🤔.

     

    Error is  “body”:”Missing initial multi part boundary”

     

    HTTP request looks good to me.Checked this from XPI Inspector.

    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}–

    Cheers,

    Rajesh PS

    • Damian Kolasa

      There was an issue with below string declaration(highlighted in bold). Now Looks good 😀

       

      String paramName1Value = "Content-Disposition: form-data; name=\"tableId\"" + LINE_FEED + LINE_FEED + tableID;

      String paramName2Value = "Content-Disposition: form-data; name=\"file\"; filename=\"" + fileNameString + "\""
      + LINE_FEED + "Content-Type: application/octet-stream" + LINE_FEED + LINE_FEED + fileContents;

      String boundaryStart = "--" + boundary;

      String boundaryEnd = "--" + boundary + "--";

      String requestBody = boundaryStart + LINE_FEED + paramName1Value + LINE_FEED + boundaryStart + LINE_FEED
      + paramName2Value + LINE_FEED + boundaryEnd;


       

      Cheers,

      Rajesh PS