SAP JCo Server Example
SAP JCo Server Example
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.
* 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'|.
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;
@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();
}
}
};
@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);
}
}
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;
/**
* 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;
}
}
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;
/**
* 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;
/**
* 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";
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 HttpCaller() {
}
context = SSLContext.getInstance("TLS");
context.init(null/*keyManagerFactory.getKeyManagers()*/, trustAllCerts, new
SecureRandom());
// 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();
}
Other stuff
Just to provide full information:
log4j.properties
# 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).