The following example takes some parameters encoded as the URL parameters and returns a response in JSON format:
http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo
The response looks like this
which can be formatted to better readability:
Which reads as:
Error messages are formatted like this
{
"status": {
"message": "the daily limit of 20000 credits for demo has been exceeded....",
"value": 18
}
}
Which reads as:
Now, let us try to call this service from SAP - using following steps:
Structure of the EPO customizing for an outbound service:
Transaction SM59, create a new (test-)connection, which is only required to prove, that the connection is possible (maybe, You will need to configure a firewall, a proxy server etc.).
Create a HTTP connection to an external server:
Finally: save and press the 'connection' button
Every response from the remote server is a 'success', even if it is an error message (from that server). The popup for the geolocation - server means, that the server has been reached and asks for authentication. This is a sufficiently good result for the connection test. It is not necessary to enter any data, just close the dialog.
If You get a connection error (server not found, not allowed, certificate untrusted..), consult Your network team for help.
If not exists, define two number range. For EPO services, we use the number range '00' for incoming services and '01' for outgoing services
Transaction /EPO1/EXC
Create the intervals '00' and '01':
Usually, a SAP topology consists at least of an development/testsystem an a productive system - and the webservice might also expose a test- and a productive system. In order to distinguish the target URL (servername) for different environments, we need to prepare different service names:
In this simple case, we only have one service name (because the target server provides only one URL) and are using the suffix '_test'.
Transaction /EPO1/EXC
We choose the service name (case sensitive!) ZJSON_GEO_WEBSERVICE_test:
Important fields:
if the geo location would expose also an productive server, You would name the service name ZJSON_GEO_WEBSERVICE_prod for that server
The service ID will be mapped to a service name, using the current SAP system ID (SY-SYSID).
In our small case, only one system will be used. We are mapping the 'test/prod' - name to a service ID, which can be used consistent on all SAP systems in Your topology. The service ID should be the same as the service name, but without the suffix '_test' or '_prod'.
Transaction /EPO1/EXC
Important fields:
Create such an outbound operation for each service name (in our case, only for the 'test' name).
Transaction /EPO1/EXC,
Important fields:
All the names of the JSON response has to be mapped to ABAP fields. To make the life easier, we will use the table /EPO1/FIELD_MAP in order to maintain the mapping. Usually, the field mapping is required, when You POST some data to a webservice, which requires lower case fieldnames; in our case, we only GET data, which does not really require an explicit mapping (lower case letters would be matched to uppercase letters in ABAP fieldnames).
For this example, we will simply use the [camelCase] - mapping: every JSON-uppercase letter will be mapped to an ABAP letter, leaded by an underscore '_'. Only the inbound direction has to be maintained, because we have a GET request
Note: use the service _ID_ (without the suffix)
Sometimes, it is more convenient to have the mapping information directly in code instead of relying on customiziation.
The contents of the field name mapping - /EPO1/FIELD_MAP can also be defined in code, but the conversion ABAP <-> JSON has to be called manually (using the methods /EPO1/CL_TOOLS=>ABAP_TO_JSON or /EPO1/CL_TOOLS=>JSON_TO_ABAP).
The following code can be used in a report, a function module or in a method of a class.
The structure has to consist of a table with the fields (according to the selected field mapping - '[camelCase]') and additionally the structure for error messages:
" definition of the request structure
" assign field names according to the '[camelCase]' mapping
TYPES:
BEGIN OF lty_geoname_struc,
lng TYPE p,
geoname_id TYPE i,
countrycode TYPE string,
name TYPE string,
fcl_name TYPE string,
toponym_name TYPE string,
fcode_name TYPE string,
wikipedia TYPE string,
lat TYPE p,
fcl TYPE string,
population TYPE i,
fcode TYPE string,
END OF lty_geoname_struc,
" definition of the table
lty_geoname_tab TYPE STANDARD TABLE OF lty_geoname_struc,
BEGIN OF lty_status_struc,
message TYPE string,
value TYPE i,
END OF lty_status_struc.
DATA:
" definition of the response structure
BEGIN OF ls_response,
geoname TYPE lty_geoname_tab, " usable data
status TYPE lty_status_struc, " error info
END OF ls_response,
" definition of other data
lo_json TYPE REF TO /epo1/cl_json_base_out,
ls_callstatus TYPE /epo1/callstatus,
ls_geoname TYPE lty_geoname_struc.
In this example, the API path is hard coded. Imagine Your own logic how to create another path.
" init
CREATE OBJECT lo_json
EXPORTING
iv_service_id_outbound = 'ZJSON_GEO_WEBSERVICE'
iv_service_operation = 'cities'.
" call the webservice
lo_json->call_service(
EXPORTING
iv_api_path = '/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo'
iv_http_method = 'GET'
IMPORTING
es_response_data = ls_response
es_callstatus = ls_callstatus ).
IF ( ls_callstatus-type = 'W' ) OR
( ls_callstatus-type = 'E' ).
" error handling, webservice not reached
WRITE: ls_callstatus-type,
ls_callstatus-subject,
ls_callstatus-description.
RETURN.
ENDIF.
" check for status message
IF ls_response-status-message IS NOT INITIAL.
" error handling, webservice returned errors
WRITE: / ls_response-status-message,
/ ls_response-status-value.
RETURN.
ENDIF.
It might be possible to look into the HTTP response headers, retrieve the HTTP code and HTTP reason and other information, but we skip this data processing in this simple showcase.
" success: use the LS_RESPONSE - structure
LOOP AT ls_response-geoname INTO ls_geoname.
" so something with the data..
WRITE: / ls_geoname-lng,
/ ls_geoname-geoname_id,
/ ls_geoname-countrycode,
/ ls_geoname-name,
/.
ENDLOOP.
In JSON, a boolean will be represented as 'true' or 'false', while ABAP uses the values 'X' or ''.
Use the data type XSDBOOLEAN or a data type with the domain XSDBOOLEAN for such fields.
Already prepared is the basic authentication (see the instance methods 'ADD_HTTPHEADER_AUTH_BASIC( )' and 'GET_SERVICE_PARAMETER( )'). Redefine those methods, it the supplied coding does not match Your needs.
For the default coding: maintain the username and password with transaction /EPO1/EC_EB_WSSP12 for each SAP system and EPO service. Both values are stored in encoded form on the database.
Field | Description |
---|---|
SAP System | the SAP system ID |
Service | the EPO service ID (without suffix) |
Name | Parameter name ('UserName' for user name, 'UserPw' for the password) |
Value | the value for the parameter (enter the right user name and the password) |
If there are other authorization methods required, redefine the class and add your own method which supplies the required authorization data.
For the authentication with OAuth 2.0, see OAuth authentication.
During the handshake when starting a secured connection ('https'), the remote server will present its own public server certificate. By default, SAP will not trust any certificate and disrupts the connection process.
A connection test (SM59) will fail with the message, that SAP does not trust the certificate.
It is necessary to store certificates of trusted servers into a PSA (a container for certificates).
Transaction STRUST, change to edit mode;
in the RFC configuration (SM59) and in the EPO configuration (configout), the anonymous container will be designated by the name 'ANONYM', where the standard container is assigned to 'DFAULT' (this is our standard container for server certificates)
WARNING: on a productive system, do a restart outside Your main business hours, better in an anounced maintenance window (surely, You won't interrupt Your own business!)
Note: on some 'new' SAP releases (S/4), the ICM server is automatically triggered to use the newly stored certificate. On these systems, the connection test will succeed without a manual restart.
The restart of the ICM service will refresh the list of trusted certificates according to STRUST.
Transaction SMICM
and then
Note: 'Exit Soft' will finish currently running processes (this may take some time); 'Hard Shut Down' will immediately stop running processes, with the risk of losing (important?) data.
After the restart, return to SM59 and retry the secured connection.
Specify a field mapping for the outbound direction in table /EPO1/FIELD_MAP.
Similar to the response structure, define a structure which maps to the request structure of the webservice. Fill this structure and pass it to the method call:
" have a structure LS_REQUEST, filled with request data
" call the webservice
lo_json->call_service(
EXPORTING
is_request_data = ls_request
iv_api_path = '/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo'
iv_http_method = 'POST' " use POST instead of GET
IMPORTING
es_response_data = ls_response
es_callstatus = ls_callstatus ).
In rare cases, where the customizing of static defined HTTP headers and the dynamic creation of HTTP headers (in redefined method ADD_HTTPHEADER) is not sufficient, HTTP headers for the request can be passed to the methods CALL_SERVICE and CALL_SERVICE_DOWNLOAD.
This may be useful for requests, where an authorization - token has to be passed as HTTP header.
When cookies are to be send to a webservice, the optional cookie-table IT_COOKIES can be supplied to the methods CALL_SERVICE and CALL_SERVICE_DOWNLOAD.
Note: in order to extract cookies from HTTP headers, use the class /EPO1/CL_COOKIES.
Supply the parameter ET_RESPONSE_HEADERS, the HTTP-code has the name '~status_code', the text '~status_reason'. There are all HTTP headers, which has been returned from the webservice, plus some additional parameters which are supplied by the SAP runtime (these parameters starts with '~').
Adaptions of the code:
DATA:
...
lt_response_headers TYPE tihttpnvp,
ls_response_header LIKE LINE OF lt_response_headers.
lo_json->call_service(
...
IMPORTING
...
et_response_headers = lt_response_headers ).
READ TABLE lt_response_headers INTO ls_response_header WITH KEY name = '~status_code'.
IF ls_response_header-value <> '200'.
" not OK..
...
ENDIF.
The class /EPO1/CL_COOKIE, method READ_FROM_HEADERS can be used to extract all cookies from the HTTP-header table.
Transaction /EPO1/EXC
Select the data of Your choice:
A click on a 'META' hotspot will show a popup with the metadata (URL, HTTP headers..):
A double click on a line with message data ('DataLength' greater than 0) will show a popup with the JSON data; the button 'Pretty print' will try to format a JSON message for better readability
Please note, that METADATA for input messages could also help to solve issues, as they contain sometimes additional information. Change the log settings for meta data to 'X' in order to log for both directions.
Especially, when there are a lot of log entries, the usage of the foreign keys may help to filter for / find the right line of interest.
When running the called function module, simply supply some key fields (FKEY1..FKEY4) of interest. Preferrably - use the same FKEY field for the same kind of value, if there are multiple webservices used for similar data (like order creation, order change, order read ..) they should put the order number each into the same FKEY field.
Example:
...
DATA:
lv_fkey1 TYPE /epo1/fkey1,
lv_fkey2 TYPE /epo1/fkey2.
...
" init EPO logging
CALL FUNCTION 'CONVERSION_EXIT_ALPHA_OUTPUT'
EXPORTING
input = order_number
IMPORTING
OUTPUT = lv_fkey1
.
lv_fkey2 = position_number.
...
lo_json->call_service(
EXPORTING
...
iv_fkey1 = lv_fkey1
iv_fkey2 = lv_fkey2
...
Just to have a really complete coding (for simple copy/paste) of the report 'ZJSON_GEO_WEBSERVICE':
*&---------------------------------------------------------------------*
*& Report ZJSON_GEO_WEBSERVICE
*&---------------------------------------------------------------------*
*& example, how to call a JSON webservice with the EPO connector
*&---------------------------------------------------------------------*
REPORT ZJSON_GEO_WEBSERVICE.
" definition of the request structure
" assign field names according to the 'camelCase' mapping
TYPES:
BEGIN OF lty_geoname_struc,
lng TYPE p,
geoname_id TYPE i,
countrycode TYPE string,
name TYPE string,
fcl_name TYPE string,
toponym_name TYPE string,
fcode_name TYPE string,
wikipedia TYPE string,
lat TYPE p,
fcl TYPE string,
population TYPE i,
fcode TYPE string,
END OF lty_geoname_struc,
" definition of the table
lty_geoname_tab TYPE STANDARD TABLE OF lty_geoname_struc,
BEGIN OF lty_status_struc,
message TYPE string,
value TYPE i,
END OF lty_status_struc.
DATA:
" definition of the response structure
BEGIN OF ls_response,
geoname TYPE lty_geoname_tab, " usable data
status TYPE lty_status_struc, " error info
END OF ls_response,
" definition of other data
lo_json TYPE REF TO /epo1/cl_json_base_out,
ls_callstatus TYPE /epo1/callstatus,
ls_geoname TYPE lty_geoname_struc.
" init
CREATE OBJECT lo_json
EXPORTING
iv_service_id_outbound = 'ZJSON_GEO_WEBSERVICE'
iv_service_operation = 'cities'.
" call the webservice
lo_json->call_service(
EXPORTING
iv_api_path = '/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo'
iv_http_method = 'GET'
IMPORTING
es_response_data = ls_response
es_callstatus = ls_callstatus ).
IF ( ls_callstatus-type = 'W' ) OR
( ls_callstatus-type = 'E' ).
" error handling, webservice not reached
WRITE: ls_callstatus-type,
ls_callstatus-subject,
ls_callstatus-description.
RETURN.
ENDIF.
" check for status message
IF ls_response-status-message IS NOT INITIAL.
" error handling, webservice returned errors
WRITE: / ls_response-status-message,
/ ls_response-status-value.
RETURN.
ENDIF.
" success: use the LS_RESPONSE - structre
LOOP AT ls_response-geoname INTO ls_geoname.
" so something with the data..
WRITE: / ls_geoname-lng,
/ ls_geoname-geoname_id,
/ ls_geoname-countrycode,
/ ls_geoname-name,
/.
ENDLOOP.