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.
Also Read: SAP PO Certification Preparation Guide
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;
- filename – name of the file, which will be passed to Content-Disposition for data stream in included in this structure;
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:
- ERP:
- File is uploaded as binary;
- Binary data stream is converted to encoded base64 string (let’s call it data_stream);
- User name and password are concatenated (user:pass string) and encoded (base64);
- 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;
- File is uploaded as binary;
- PO:
- Mapping parses inbound message (XML FM structure) and corresponding parameters are read;
- mapping sets dynamic configuration parameters:
- ISSUE_NO – part of target url path, issue we want to update;
- BOUNDARY_VAL – calculated (in our example hard coded) boundary for multipart request;
- AUTH_STRING – authorization string to authenticate ourselves at target endpoint;
- ISSUE_NO – part of target url path, issue we want to update;
- request body is built in required format:
- –{boundary} is added at the beginning;
- Content-Disposition is set (filename according to sent filename for data_stream);
- Content-Type is set set to sent mime type;
- data_stream is decoded and write as binary payload;
- -{boundary}– is added as a closure tag;
- –{boundary} is added at the beginning;
- Payload is sent to adapter engine;
- Adapter reads dynamic configuration and sets:
- target URL (put ISSUE_NO);
- Authorization (according to AUTH_STRING);
- Content-Type (as multipart/form with boundary set by JAVA mapping);
- target URL (put ISSUE_NO);
- Mapping parses inbound message (XML FM structure) and corresponding parameters are read;
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).