Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

SAP JCo Server Example

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 14

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_PROGI
D));
        } 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).

You might also like