Friday, 25 August 2017

SAP JCo Server Example

Introduction


When writing my first JCo server for I found it very cumbersome to find guiding code snippets and as well as a self-contains, fully working example. Thus I would like to put such an example here.

Scope:
  • Wrtiting a JCO server that handles a RFC from an ABAP backend
  • Self-contained destination handling (no .destination file creating/ usage)
  • ABAP sample code to “call” JCO server
  • ABAP backend customizing
Use Case:
  • Call an internet resource from an ABAP backend that has no direct internet connection.
  • A part of the JCO server provides a kind of relay function to forward an HTTP-get request and sent back its response to the caller in the ABAP backend.

Starting Point


From the ABAP backend the whole use case just looks like calling some function module remotely.

REPORT z_test_jco_server.

DATA: rfc_destination LIKE rfcdes-rfcdest VALUE 'NONE',
      lv_uri          TYPE string,
      lv_payload      TYPE string.

* Set the RFC destination name. It corresponds to the destination
* defined in SM59.
rfc_destination = 'MY_JCO_SRV'.

* Define the URI that should be called from the JCo server. So
* to say this is just some data that is handed to the FM.
lv_uri = |https://server/path/to/resource'|.

* Call the FM remotely and hand the data (iv_uri), but
* also retrieve the result (ev_response_payload).
CALL FUNCTION 'Z_SAMPLE_ABAP_CONNECTOR_CALL'
  DESTINATION rfc_destination
  EXPORTING
    iv_uri              = lv_uri
  IMPORTING
    ev_response_payload = lv_payload.

* Check for errors.
IF sy-subrc NE 0.
  WRITE: / 'Call Z_SAMPLE_ABAP_CONNECTOR_CALL         SY-SUBRC = ', sy-subrc.
ELSE.
* Display the result. In the exmaple we are expecting that the HTTP-get
* call - done by the JCo server - results in a XML response. We retrieved it from
* the JCo server (via ev_response_payload) and want to display it here in the ABAP backend.
  cl_abap_browser=>show_xml(
         EXPORTING xml_string = lv_payload
                   size       = cl_abap_browser=>xlarge ).
ENDIF.

Endpoint Maintenance


For now the ABAP program only knows to call some function module in destination MY_JCO_SRV. However so far there is no such destination. We need to maintain it in Tx SM59. Under TCP/IP Connections a new destination needs to be created (Edit -> Create). It is necessary to maintain the following data:
  • RFC destination name: MY_JCO_SRV
  • You could specify any other name here. However make sure this name is the same as in program Z_TEST_JCO_SERVER as well as in the JCo server program (see later below).
  • Provide some descriptive information in description 1,2,3
  • In tab ‘Technical Settings’ select ‘Registered Server Program’.
  • In tab ‘Technical Settings’ specify program ID ‘JCO_SERVER’
  • You could specify any other name here. However make sure this name is the same in the JCo server (see later below).
  • In tab ‘Technical Settings’ select start type of external program ‘Default Gateway Value’.
  • In tab ‘Technical Settings’ specify a gateway host. Most probable this host is the same you will find when checking the connection properties in the SAPLogon pad (right-click on the system -> properties -> tab connection -> Message Server).
  • In tab ‘Technical Settings’ specify a gateway server. This name ‘sapgwXX’ corresponds to a port. Check other TCP/IP connections in SM59 which service applies for your system or ask a system admin which one to use.
  • In tab ‘Logon & Security’ you could select ‘Do Not Send Logon Ticket’ and SNC inactive for test purposes.
  • In tab ‘Unicode’ I would recommend to select ‘Unicode’. Java is a fully unicode language. Moreover HTTP-requests and even more HTTP-responses most likely contain unicode characters.
  • In tab ‘Special Options’ I only selected ‘Default Gateway Value’ for tract export methods and keep-alive timeout. The transfer protcol I used is ‘Classic with tRFC’.
Save this connection. It does not make any sense to perform a connection test or a unicode test. There is not server program ‘JCO_SERVER’ registered for this destination so far. A test cannot work yet.

Java Part


Although I a fan of having one comprehensive code as an example, it does not make the code understandable. Even for this very simple show case it is hard to understand the code if put into one big .java file.

Server


The actual JCo server is a very simple program:
  • Some logging feature (private static Logger logger)
  • All necessary information to establish a connection (private Properties properties)
  • A thread to listen to the command line to be able to end the JCo server (private Runnable stdInListener = new Runnable() {…)
  • The actual JCo server object (private JCoServer server;)
  • Starting the server ( public void serve() {…)
  • Handler for exceptions (public void serverExceptionOccurred(…)
  • Handler for errors (public void serverErrorOccurred(…)
  • Handler for server state changes (public void serverStateChangeOccurred(…)
  • Java program entry point (public static void main(…)
package com.sap;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;

import org.apache.log4j.Logger;

import com.sap.conn.jco.JCoException;
import com.sap.conn.jco.ext.ServerDataProvider;
import com.sap.conn.jco.server.DefaultServerHandlerFactory;
import com.sap.conn.jco.server.JCoServer;
import com.sap.conn.jco.server.JCoServerContextInfo;
import com.sap.conn.jco.server.JCoServerErrorListener;
import com.sap.conn.jco.server.JCoServerExceptionListener;
import com.sap.conn.jco.server.JCoServerFactory;
import com.sap.conn.jco.server.JCoServerFunctionHandler;
import com.sap.conn.jco.server.JCoServerState;
import com.sap.conn.jco.server.JCoServerStateChangedListener;


public class SampleAbapConnectorServer implements JCoServerErrorListener, JCoServerExceptionListener, JCoServerStateChangedListener {

private static Logger logger = Logger.getLogger(ScpAbapConnectorServer.class);
 
/**
* The properties necessary to define the server and destination.
*/
    private Properties properties;
 
    public SampleAbapConnectorServer(String propertiesPath) throws IOException {
    InputStream propertiesInputStream = new FileInputStream(propertiesPath);
    properties = new Properties();
    properties.load(propertiesInputStream);
   
new MyDestinationDataProvider(properties);
new MyServerDataProvider(properties);
}
 
    /**
     * Runnable to listen to the standard input stream to end the server.
     */
    private Runnable stdInListener = new Runnable() {

@Override
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
try {
while((line = br.readLine()) != null) {
// Check if the server should be ended.
if(line.equalsIgnoreCase("end")) {
// Stop the server.
server.stop();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
};

private JCoServer server;
 
    public void serve() {
        try {
            server = JCoServerFactory.getServer(properties.getProperty(ServerDataProvider.JCO_PROGID));
        } catch(JCoException e) {
            throw new RuntimeException("Unable to create the server " + properties.getProperty(ServerDataProvider.JCO_PROGID) + ", because of " + e.getMessage(), e);
        }
     
        JCoServerFunctionHandler abapCallHandler = new AbapCallHandler();
        DefaultServerHandlerFactory.FunctionHandlerFactory factory = new DefaultServerHandlerFactory.FunctionHandlerFactory();
        factory.registerHandler(AbapCallHandler.FUNCTION_NAME, abapCallHandler);
        server.setCallHandlerFactory(factory);
     
        // Add listener for errors.
        server.addServerErrorListener(this);
        // Add listener for exceptions.
        server.addServerExceptionListener(this);
        // Add server state change listener.
        server.addServerStateChangedListener(this);
     
        // Add a stdIn listener.
        new Thread(stdInListener).start();
     
        // Start the server
        server.start();
        logger.info("The program can be stopped typing 'END'");
    }

@Override
public void serverExceptionOccurred(JCoServer jcoServer, String connectionId, JCoServerContextInfo arg2, Exception exception) {
logger.error("Exception occured on " + jcoServer.getProgramID() + " connection " + connectionId, exception);
}

@Override
public void serverErrorOccurred(JCoServer jcoServer, String connectionId, JCoServerContextInfo arg2, Error error) {
logger.error("Error occured on " + jcoServer.getProgramID() + " connection " + connectionId, error);
}
 
@Override
    public void serverStateChangeOccurred(JCoServer server, JCoServerState oldState, JCoServerState newState) {
        // Defined states are: STARTED, DEAD, ALIVE, STOPPED;
        // see JCoServerState class for details.
        // Details for connections managed by a server instance
        // are available via JCoServerMonitor
        logger.info("Server state changed from " + oldState.toString() + " to " + newState.toString() +
                " on server with program id " + server.getProgramID());
        if(newState.equals(JCoServerState.ALIVE)) {
        logger.info("Server with program ID '"+server.getProgramID()+"' is running");
        }
        if(newState.equals(JCoServerState.STOPPED)) {
        logger.info("Exit program");
        System.exit(0);
        }
    }

    public static void main(String[] args) throws Exception {
    if(args.length == 0) {
    logger.error("You must specify a properties file!");
    return;
    }
    new SampleAbapConnectorServer(args[0]).serve();
    }
}

What does this class do? First of all it retrieves the command line arguments. It expects that a path to a properties file is specifed. This path could be absolte or relative. Than it creates an instance of this class and calls the serve() method. The constructor read the properties file and creates a destination data provider instance as well as a server data provider instance. We will come to these later. The central method is serve(). Here the following is done:
  • The JCo server is started using the properties specified. Here you will find also the program ID specified at SM59. These values must match. What the call JCoServerFactory.getServer() does is it registers at the ABAP backend using the gateway host (jco.server.gwhost) and port (jco.server.gwserv). Both must match the system where you would like to call the JCo server from. The properties entry jco.server.repository_destination is used the connect to an ABAP repository. This is typically done by using a SAPLogon pad connection or rather a .destination file. However, when only connection to a single ABAP backend and not using such a .destination file, this value does not matter (although it must not be skipped). This value is used to distinguish between different repositories.
# Server
jco.server.connection_count=2
jco.server.gwhost=<host>
jco.server.progid=JCO_SERVER
jco.server.gwserv=<sapgw>
jco.server.repository_destination=does_not_matter​
  • A handler is registered to handle calls from the ABAP backend. We will look into that class later. What is done here is creating an instance of this class and registering it at the JCo server server.setCallHandlerFactory(). Every handle is registered for a certain function name: factory.registerHandler(<function name>, <handler instance>);
  • We register a handler for exceptions, errors and server state changes.
  • We start a listener to the command line for user input. This just gives the user the possibility to stop the JCo server without using control+c (which just kills the java program). When looking into the runnable stdInListener you will find a simple endless loop listening to the command line. Only if the user enters END or end the statement server.stop(); is made. However this (server.stop();) does not really stops the JCo server but triggers a server state change. The actual stopping of the server is done elsewhere.
  • The JCO server is finally started: server.start();
You will find that the listeners for exceptions and error just output the exception/error. This is very useful even for such a simple example. Without having these you might not be able to determine why your JCo server program is not working or why the call from the ABAP backend fails. I strongly recommend to use these. Looking into public void serverStateChangeOccurred(…) you will find that the Java program is terminated in case the server state reaches state JCoServerState.STOPPED.

Properties handling


In my opinion it is very cumbersome that most JCo (server) examples use files in the file system to establish a connection. As Java programmer I would like to have it all in the code, at least for a such simple scenario. Otherwise as a learner one does not really understand how the magic happens.

In order to establish a JCo server some information is necessary. JCo provides the possiblity to either use a destination file where these information are located or writing a so called ServerDataProvider.

package com.sap;

import java.util.Properties;

import org.apache.log4j.Logger;

import com.sap.conn.jco.ext.Environment;
import com.sap.conn.jco.ext.ServerDataEventListener;
import com.sap.conn.jco.ext.ServerDataProvider;

public class MyServerDataProvider implements ServerDataProvider {

private static Logger logger = Logger.getLogger(MyDestinationDataProvider.class);

/**
* From these properties all necessary destination
* data are gathered.
*/
private Properties properties;

/**
* Initializes this instance with the given {@code properties}.
* Performs a self-registration in case no instance of a
* {@link MyServerDataProvider} is registered so far
* (see {@link #register(MyServerDataProvider)}).
*
* @param properties
*            the {@link #properties}
*
*/
public MyServerDataProvider(Properties properties) {
super();
this.properties = properties;
// Try to register this instance (in case there is not already another
// instance registered).
register(this);
}

/**
* Flag that indicates if the method was already called.
*/
private static boolean registered = false;

/**
* Registers the given {@code provider} as server data provider at the
* {@link Environment}.
*
* @param provider
*            the server data provider to register
*/
private static void register(MyServerDataProvider provider) {
// Check if a registration has already been performed.
if (registered == false) {
logger.info("There is no " + MyServerDataProvider.class.getSimpleName()
+ " registered so far. Registering a new instance.");
// Register the destination data provider.
Environment.registerServerDataProvider(provider);
registered = true;
}
}

@Override
public Properties getServerProperties(String serverName) {
logger.info("Providing server properties for server '"+serverName+"' using the specified properties");
return properties;
}

@Override
public void setServerDataEventListener(ServerDataEventListener listener) {
}

@Override
public boolean supportsEvents() {
return false;
}
}

The class does two importand things:
  • It registers the instance at the JCO environment. This makes sure that an instance of this class is used to determine the necessary proerties: Environment.registerServerDataProvider(provider); Make sure this is only done once, otherwise an exception will occur (if (registered == false) { ….registered = true;…).
  • The properties are provided is requested: public Properties getServerProperties(…) Here, one could distinguish between different servers. However, for this simple use case we are only facing one server so this implementation does return always the same properties no matter which server name (serverName) is given.
I mentioned the repository already. In order to be able to register a function handler, it is also necessary to provide information about the “normal” connection. This is the same information as if you would call an RFM from Java. The very same approach as for server destination data is applied.

package com.sap;

import java.util.Properties;

import org.apache.log4j.Logger;

import com.sap.conn.jco.ext.DestinationDataEventListener;
import com.sap.conn.jco.ext.DestinationDataProvider;
import com.sap.conn.jco.ext.Environment;

public class MyDestinationDataProvider implements DestinationDataProvider {

private static Logger logger = Logger.getLogger(MyDestinationDataProvider.class);

/**
* From these properties all necessary destination
* data are gathered.
*/
private Properties properties;

/**
* Initializes this instance with the given {@code properties}.
* Performs a self-registration in case no instance of a
* {@link MyDestinationDataProvider} is registered so far
* (see {@link #register(MyDestinationDataProvider)}).
*
* @param properties
*            the {@link #properties}
*
*/
public MyDestinationDataProvider(Properties properties) {
super();
this.properties = properties;
// Try to register this instance (in case there is not already another
// instance registered).
register(this);
}

/**
* Flag that indicates if the method was already called.
*/
private static boolean registered = false;

/**
* Registers the given {@code provider} as destination data provider at the
* {@link Environment}.
*
* @param provider
*            the destination data provider to register
*/
private static void register(MyDestinationDataProvider provider) {
// Check if a registration has already been performed.
if (registered == false) {
logger.info("There is no " + MyDestinationDataProvider.class.getSimpleName()
+ " registered so far. Registering a new instance.");
// Register the destination data provider.
Environment.registerDestinationDataProvider(provider);
registered = true;
}
}

@Override
public Properties getDestinationProperties(String destinationName) {
logger.info("Providing destination properties for destination '"+destinationName+"' using the specified properties");
return properties;
}

@Override
public void setDestinationDataEventListener(DestinationDataEventListener listener) {
}

@Override
public boolean supportsEvents() {
return false;
}
}

Function handler


So far so good. But we did nothing about function Z_SAMPLE_ABAP_CONNECTOR_CALL that we used the the ABAP program.

package com.sap;

import javax.ws.rs.ProcessingException;

import org.apache.log4j.Logger;

import com.sap.conn.jco.JCoFunction;
import com.sap.conn.jco.server.JCoServerContext;
import com.sap.conn.jco.server.JCoServerFunctionHandler;

public class AbapCallHandler implements JCoServerFunctionHandler {

/**
* This handler only supports one function with name {@code Z_SAMPLE_ABAP_CONNECTOR_CALL}.
*/
public static final String FUNCTION_NAME = "Z_SAMPLE_ABAP_CONNECTOR_CALL";

private static Logger logger = Logger.getLogger(AbapCallHandler.class);

private void printRequestInformation(JCoServerContext serverCtx, JCoFunction function) {
logger.info("----------------------------------------------------------------");
        logger.info("call              : " + function.getName());
        logger.info("ConnectionId      : " + serverCtx.getConnectionID());
        logger.info("SessionId         : " + serverCtx.getSessionID());
        logger.info("TID               : " + serverCtx.getTID());
        logger.info("repository name   : " + serverCtx.getRepository().getName());
        logger.info("is in transaction : " + serverCtx.isInTransaction());
        logger.info("is stateful       : " + serverCtx.isStatefulSession());
        logger.info("----------------------------------------------------------------");
        logger.info("gwhost: " + serverCtx.getServer().getGatewayHost());
        logger.info("gwserv: " + serverCtx.getServer().getGatewayService());
        logger.info("progid: " + serverCtx.getServer().getProgramID());
        logger.info("----------------------------------------------------------------");
        logger.info("attributes  : ");
        logger.info(serverCtx.getConnectionAttributes().toString());
        logger.info("----------------------------------------------------------------");
}

public void handleRequest(JCoServerContext serverCtx, JCoFunction function) {
// Check if the called function is the supported one.
if(!function.getName().equals(FUNCTION_NAME)) {
logger.error("Function '"+function.getName()+"' is no supported to be handled!");
return;
}
        printRequestInformation(serverCtx, function);

        // Get the URI provided from Abap.
        String uri = function.getImportParameterList().getString("IV_URI");
     
HttpCaller main = new HttpCaller();
main.initializeSslContext();
main.initializeClient();
String payload = null;
try {
payload = main.invokeGet(uri);
} catch(ProcessingException pe) {
// Provide the exception as payload.
payload = pe.getMessage();
}
// Provide the payload as exporting parameter.
        function.getExportParameterList().setValue("EV_RESPONSE_PAYLOAD", payload);
    }
}

This class seems complex but this is only due to the massive debug output (which again is really helpful when prototyping such a use case or analyzing issues). The main method is public void handleRequest(). Looking into class SampleAbapConnector we already saw that an instance of this class is registered to handle function call to ‘Z_SAMPLE_ABAP_CONNECTOR_CALL’. Inside public void handleRequest(…) we
  • prevent that other function than Z_SAMPLE_ABAP_CONNECTOR_CALL are handled (if(!function.getName().equals(FUNCTION_NAME)) {…)
  • print debug information (printRequestInformation(serverCtx, function);)
  • Retrieve the given URI (String uri = function.getImportParameterList().getString(“IV_URI”);)
  • Utilize another class to make the HTTP call and get its result (HttpCaller main = new HttpCaller(); …)
  • Sent back the result to the ABAP backend or rather to the caller in the backend (function.getExportParameterList().setValue(“EV_RESPONSE_PAYLOAD”, payload);)
You could think of doing whatever you like inside this handler. You just have to be compliant with the interface of the function module ‘Z_SAMPLE_ABAP_CONNECTOR_CALL’.

HTTP-handling


This is nothing that should be to hard to implement for a Java developer. Moreover this is not really in the center of this example. However I will provide this code too as I said I do not like non-complete examples. By the way this code might provide some interessting aspects about HTTPS requests/responses.

package com.sap;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.Response;

import org.apache.log4j.Logger;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.logging.LoggingFeature.Verbosity;

public class HttpCaller {

public static Logger logger = Logger.getLogger(HttpCaller.class);

private SSLContext context;

private Client client;

public HttpCaller() {
}

public void initializeSslContext() {
// Initialize an SSL context.
try {
/*
* http://stackoverflow.com/questions/6047996/ignore-self-signed-ssl-cert-using-jersey-client
* for
* javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed:
* sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
*/
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager(){
   public X509Certificate[] getAcceptedIssuers(){return null;}
   public void checkClientTrusted(X509Certificate[] certs, String authType){}
   public void checkServerTrusted(X509Certificate[] certs, String authType){}
}};

context = SSLContext.getInstance("TLS");
context.init(null/*keyManagerFactory.getKeyManagers()*/, trustAllCerts, new SecureRandom());

} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error(e);
}
}

public void initializeClient() {
// Prepare verbose log output.
Feature feature = new LoggingFeature(java.util.logging.Logger.getLogger("test"), java.util.logging.Level.INFO, Verbosity.PAYLOAD_ANY, null);

// In case you are not using any proxy delete these lines.
                // In case you are using a proxy replace <host> and <port>.
System.setProperty ("https.proxyHost", "<host>");
System.setProperty ("https.proxyPort", "<port>");
System.setProperty ("http.proxyHost", "<host>");
System.setProperty ("http.proxyPort", "<port>");
 
// Build a client.
client = ClientBuilder.newBuilder()
.sslContext(context)
.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// Just accept any host. Do not do an actual verification.
return true;
}
})
// Make verbose output.
.register(feature)
.build();
}

public String invokeGet(String uri) {
logger.info("URL: "+uri);
WebTarget target = client.target(uri);
Invocation.Builder invocationBuilder = target.request();
// Set necessary headers.
invocationBuilder.header("Accept","*/*");
invocationBuilder.header("Accept-Encoding","gzip, deflate, sdch, br");
invocationBuilder.header("Accept-Language","de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4");
Response response = invocationBuilder.get();
return response.readEntity(String.class);
}
}

Other stuff


Just to provide full information:

log4j.properties

# Root logger option
#log4j.rootLogger=INFO, stdout, file
log4j.rootLogger=DEBUG, stdout

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
#log4j.appender.stdout.layout.ConversionPattern=%-5p [%c{1}:%L]: %m%n
log4j.appender.stdout.layout.ConversionPattern=%m%n

complete properties file used

# Server
jco.server.connection_count=2
jco.server.gwhost=<host>
jco.server.progid=JCO_SERVER
jco.server.gwserv=<sapgwXX>
jco.server.repository_destination=does_not_matter

# "Client"
jco.client.lang=en
jco.destination.peak_limit=10
jco.client.client=<client>
jco.client.sysnr=<number>
jco.destination.pool_capacity=3
jco.client.ashost=<host>
jco.client.user=<ABAP backend user>
jco.client.passwd=<ABAP backend password>

Note. This example uses user/password based authentication (just look into the properties above).

SAP Jco:


  • You will need to download and provide the SAP Jco library. Make sure you also provide the sapjco DLL (either 32 or 64bit).
  • This example uses SAP JCo 3 (more specific 3.0.13)

No comments:

Post a Comment