Disclaimer

 

Important: the mechanism described in this article is intended as an example only and should not be interpreted as a generally accepted or endorsed way of connecting to and working with a SAP back-end systems. In particular, additional security measures: securing access to the web application, securing transport of data obtained via RFC calls, even though not being addressed here, are absolutely essential to the real-world application. Another important consideration: the usage of a generic system user to connect to the back-end system and execute BAPIs, from outside an SAP managed landscape, is most likely a subject to a specific licensing restrictions. The same might be the case with the use of SAPUI5 libraries when the framework will be released.

 

Motivation

 

With appearance of front-end tools like SAPUI5 (pure JavaScript), there is an alternative to the development of SAP compliant applications using standard IDEs like the popular NetBeans platform. This blog post will describe the way to set up a simple application using NetBeans which uses SAPUI5 demo kit, Jersey RESTful service and SAP JCo technology to execute RFC calls on the back-end system. The application is built and tested via Maven using an embedded Tomcat server.

 

Prerequisites

 

  • Download and install locally a distribution of JCo connector available from SAP Marketplace which corresponds to your architecture. The code used here was tested with SAP JCo version 3.0.8 for Windows 7 (x64).
  • RFC enabled system user for connection to ABAP back-end system.
  • Familiarity with NetBeans platform and Maven build framework.
  • JavaScript libraries from SAPUI5 demo kit, the code is based on version 1.8.4.

 

Setting up

 

Create a simple Maven based web application project. In order to use SAP JCo libraries as Maven dependencies, install the sapjco JAR in your local repository. Here are the relevant contents of pom.xml file for the project.

 

<dependencies>

    <dependency>

        <!-- install sap-jco-3.0.8.jar in local repository ~.m2/repository/com/sap/sap-jco/3.0.8, for example -->

        <groupId>com.sap</groupId>

        <artifactId>sap-jco</artifactId>

        <version>3.0.8</version>

    </dependency>

    <dependency>

        <groupId>com.sun.jersey</groupId>

        <artifactId>jersey-json</artifactId>

        <version>1.9.1</version>

    </dependency>

    <dependency>

        <groupId>com.sun.jersey</groupId>

        <artifactId>jersey-server</artifactId>

        <version>1.9.1</version>

    </dependency>

</dependencies>

<build>

    <plugins>

           <!-- other plugins: compile, war -->

        <plugin>

            <groupId>org.apache.tomcat.maven</groupId>

            <artifactId>tomcat7-maven-plugin</artifactId>

            <version>2.0</version>

            <configuration>

                <port>8080</port>

                <path>/</path>

            </configuration>

        </plugin>

    </plugins>

</build>

 

Notice that we are using Tomcat Maven plugin to launch the web application in an embedded instance of Tomcat server.

The JARs from SAPUI5 demo kit go to WEB-INF/lib directory.

 

 

JCo service

 

SAP Java Connector (JCo) infrastructure allows to connect to a back-end ABAP system from any Java client. First we need to implement com.sap.conn.jco.ext.DestinationDataProvider describing the necessary connection properties.

 

public class JCoDataProvider implements DestinationDataProvider {

 

    public static final String RFC_SYSTEM_ID = "BACK_END_SYSTEM";

    private DestinationDataEventListener evtListener;

    private Properties props;

 

    @Override

    public Properties getDestinationProperties(String name) {

        if (name.equals(RFC_SYSTEM_ID)) {

            if (props == null) {

                try {

                    props = new Properties();

                    props.load(getClass().getClassLoader().getResourceAsStream("jco.properties"));

                } catch (IOException ex) {

                    throw new RuntimeException("Cannot load JCo properties for destination " + name + ". " + ex.getMessage());

                }

            }

        } else {

            throw new IllegalArgumentException("Not configured for " + name);

        }

        return props;

    }

 

    @Override

    public boolean supportsEvents() {

        return true;

    }

 

    @Override

    public void setDestinationDataEventListener(DestinationDataEventListener dl) {

        this.evtListener = dl;

    }

}

 

This implementation just reads the properties from a flat properties file. See examples from the distribution of sapjco for more examples.

Out JCo manager then uses the specified data provider to connect to the back-end system and execute RFC calls using JCo API.

 

public class JCoManager {

 

    private static final String EXPORT_PARAMETERS_LIST = "export_parameters_list";

    private static final String TABLE_PARAMETERS_LIST = "table_parameter_list";

 

    public JCoManager() {

        if (!Environment.isDestinationDataProviderRegistered()) {

            Environment.registerDestinationDataProvider(new JCoDataProvider());

        }

    }

 

    private Map<String, JCoRecord> executeRfc(String bapi, Map<String, Object> inParams) {

        Map<String, JCoRecord> records = new HashMap<String, JCoRecord>();

        try {

            JCoDestination jcoDestination = JCoDestinationManager.getDestination(JCoDataProvider.RFC_SYSTEM_ID);

            JCoFunction jcoFunction = jcoDestination.getRepository().getFunction(bapi);

            if (jcoFunction == null) {

                throw new JCoRuntimeException(JCoException.JCO_ERROR_FUNCTION_NOT_FOUND, bapi);

            }

 

            //see if we need to set the input parameters

            if (inParams != null) {

                //see if the function accepts any input parameters

                JCoParameterList jcoInParams = jcoFunction.getImportParameterList();

                if (jcoInParams == null) {

                    throw new JCoRuntimeException(JCoException.JCO_ERROR_CONFIGURATION,

                            "JCo function " + bapi + " does not have any input parameters");

                }

                //loop through the supplied parameter map and set the function parametres

                for (Iterator<String> iter = inParams.keySet().iterator(); iter.hasNext();) {

                    String key = iter.next();

                    Object value = inParams.get(key);

                    try {

                        jcoInParams.setValue(key, value);

                    } catch (JCoRuntimeException e) {

 

                        throw new JCoRuntimeException(e.getGroup(), e.getKey(), "Problem setting input parameter "

                                + key

                                + " for bapi " + bapi + ". " + e.getMessage(), e);

                    }

                }

            }

 

            //execute call

            jcoFunction.execute(jcoDestination);

 

            //set the export parameters

            JCoParameterList jcoOutParams = jcoFunction.getExportParameterList();

            if (jcoOutParams != null) {

                records.put(EXPORT_PARAMETERS_LIST, jcoOutParams);

            }

 

            //set the table parameters

            JCoParameterList jcoTabParams = jcoFunction.getTableParameterList();

            if (jcoTabParams != null) {

                records.put(TABLE_PARAMETERS_LIST, jcoTabParams);

            }

        } catch (JCoException e) {

            throw new JCoRuntimeException(e.getGroup(), e.getKey(), e.getMessage(), e);

        }

        return records;

    }

 

    /**

     * Executes STFC_CONNECTION BAPI. Can be used to check JCo connection to the

     * back-end.

     */

    public String[] checkConnection(String requText) {

        String[] result = new String[2];

 

        Map<String, Object> inParams = new HashMap<String, Object>();

        inParams.put("REQUTEXT", requText);

 

        Map<String, JCoRecord> outRecords = executeRfc("STFC_CONNECTION", inParams);

        JCoRecord outParams = outRecords.get(EXPORT_PARAMETERS_LIST);

        result[0] = outParams.getString("ECHOTEXT");

        result[1] = outParams.getString("RESPTEXT");

        return result;

    }

}

 

For our example we'll use STFC_CONNECTION BAPI for checking the connection to the back-end, it's called from checkConnection() method.

 

Jersey service

 

Create a simple Jersey resource and an implementation of com.sun.jersey.spi.inject.InjectableProvider to inject our JCo service as the singleton.

Here is the annotation used by the injectable provider.

 

@Target({ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

public @interface SingletonComponent {

 

}

 

Here is the injectable provider itself.

 

@Provider

public class SingletonComponentInjectableProvider implements InjectableProvider<SingletonComponent, Type>, Injectable<Object> {

 

    private Type type;

 

    @Override

    public ComponentScope getScope() {

        return ComponentScope.Singleton;

    }

 

    @Override

    public Injectable getInjectable(ComponentContext ctx, SingletonComponent annot, Type type) {

        this.type = type;

        return this;

    }

 

    @Override

    public Object getValue() {

        Object obj = null;

        try {

            obj = ((Class) type).newInstance();

        } catch (InstantiationException ex) {

            throw new RuntimeException(ex);

        } catch (IllegalAccessException ex) {

            throw new RuntimeException(ex);

        }

        return obj;

    }

}

 

And here is the resource which uses our JCo service and exposes the results of RFC calls as JSON objects.

 

@Path("/")

public class FlightResource {

 

    @SingletonComponent

    private JCoManager jCoManager;

 

    @Path("/check/{echo}")

    @GET

    @Consumes(MediaType.APPLICATION_JSON)

    @Produces(MediaType.APPLICATION_JSON)

    public Map check(@PathParam("echo") String echo) {

        Map map = new HashMap();

        String[] response = jCoManager.checkConnection(echo);

        map.put("str1", response[0]);

        map.put("str2", response[1]);

        return map;

    }

}

 

Do not forget to configure Jersey servlet in web.xml file.

 

<servlet>

        <servlet-name>jersey-rest-ws</servlet-name>

        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>

        <init-param>

            <param-name>com.sun.jersey.config.property.packages</param-name>

            <param-value>ch.unil.demo.jco</param-value>

        </init-param>

        <init-param>

            <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>

            <param-value>true</param-value>

        </init-param>

    </servlet>

    <servlet-mapping>

        <servlet-name>jersey-rest-ws</servlet-name>

        <url-pattern>/rest/*</url-pattern>

    </servlet-mapping>

 

SAPUI5 front-end and testing

 

Create a simple HTML page with JS view and controller which allows to check the connection to the back-end.

Here is the details of the controller. It uses a JQuery driven AJAX call to our Jersey resource.

 

sap.ui.controller("flight.search", {

    check: function(echo){

      this._doAjax("/check/"+echo, null, "GET", false).done(function(data){

          console.log("Check connection returned: ", data);

          sap.ui.commons.MessageBox.alert(data.str2, null, data.str1);

      }); 

    },

 

    _doAjax: function(path, content, type, async) {

        var params = {

            url: "/rest" + path,

            dataType: "json",

            contentType: "application/json",

            context: this,

            cache: false

        };

 

        params["type"] = type || "POST";

 

        if (content) {

            params["data"] = JSON.stringify(content);

        }

 

        if (async === false) {

            params["async"] = async;

        }

 

        return jQuery.ajax(params);

    }

});

 

Here is the view.

 

sap.ui.jsview("flight.search", {

 

    getControllerName: function() {

        return "flight.search";

    },

 

    createContent: function(ctrl) {

        var btn1;

 

        btn1 = new sap.ui.commons.Button({

            text: "Check"

        });

        btn1.attachPress(function(evt) {           

            ctrl.check("test");

        });

        return [btn1];

    }

});

 

We can run the server with mvn tomcat7:run command and access our application at localhost.

 

In this post I will show how we can setup and work with a popular code source and revision control tool -- TortoiseSVN -- together with the NetWeaver Developer Studio for AS version 7.0. Normally, you should take advantage of NetWeaver Developer Infrastructure (NWDI) when developing with Java in NWDS, which takes care of revision control for you (among other things). But to setup a fully functioning NWDI is not always an option. Sometimes you just need to be able to revert to or compare a file in your project with its version in the repository. Moreover, you are not always develop using Development Components (DCs) architecture required by NWDI. All this, naturally, might get you thinking about using an SVN repository to track changes to your NWDS projects. The problem is that, unlike the recent NWDS for AS 7.3, the older NWDS for AS 7.0 (in my case 7.0.25) is built on an old Eclipse version 2.1.2, which does not have many SVN plugins. The solution to the problem is to use an external SVN client to keep track of changes in the NWDS workspace, without Eclipse being aware of it.

 

Until recently, the problem with this solution was that SVN protocol required an .SVN folder with metadata being placed in every folder of the project. When working with some type of projects like WDJ (or with DCs in Local Development), NWDS has to copy some contents around (gen* and bin folders) and recreate some folders upon every build. This caused, SVN clients to complain (since .SVN folders were copied as well). Fortunately, the recent versions of SVN protocol specify one top-level .SVN folder containing all of the SVN metadata for the project. This made it a breeze to be able to use an external SVN client with any type of NWDS project.

 

Download and install Tortoise SVN client, it integrates seamlessly with Windows file explorer interface.

 

I assume we are going to create a new NWDS workspace under the path c:\svn\workspace. I also assume you have an SVN repository set up on a remote server. Use SVN Repo Browser from the context menu in c:\svn folder to browse to the root folder of the SVN repository where you want to import your workspace.

 

 

Create a remote folder workspace.

 

 

Once the workspace (remote) folder has been created, check it out to your local c:\svn\workspace directory. You should now have a SVN controlled workspace directory ready for NWDS projects.

 

 

Start NWDS with -data argument pointing to the c:\svn\workspace directory (copy NWDS shortcut, right click on the new shorcut, select Properties). NWDS will complete the setup for the new workspace. We will create a simple WDJ and Java projects for illustration purposes. Java project has a POJO (Person.java) and WDJ project imports this POJO as Java Bean Model and maps it all the way to the visual controller in the main Web Dynpro component. This is how the project files look in the Resource Perspective of the NWDS.

 

 

Notice that in the WDJ project we have bin, gen_ddic, gen_wdp folders that will be populated by WDJ at compile time (thus they should not be included in SVN).

Before we go futher, you can add some useful shortcuts to the context menu of TortoiseSVN by selecting TortoiseSVN --> Settings, then General --> Context Menu from the context menu. You can, for example, add Check for modifications to the context menu, since you'll probably use it often.

 

 

The first thing we want to do is to add .metadata folder in our workspace to SVN ignore list. This is were NWDS stores all the workspace metadata files and it could be quite voluminous and contain many subfolders. To do it, select c:\workspace\.metadata folder and select TortoiseSVN --> Add to ignore list --> .metadata.

 

 

Then add Java project to SVN control by selecting it and selecting TortoiseSVN --> Add... from context menu. You can deselect all of the .class files (or any other files or directories that you don't want to have under SVN control).

 

 

 

Do the same with WDJ project, I don't add any bin, gen_ddic, or gen_wdp folders to version control since they are automatically generated by WDJ.

 

 

To commit project files to SVN, select SVN Commit... from the context menu of workspace root folder, then select all but non-versioned (ignored) files and select Commit... from the context menu.

 

 

Enter the commit comment.

 

 

And TortoiseSVN commits the selected files to the remote repository.

 

 

 

If you decided not to add some folders to SVN, you should probably add them to the ignore list now. This can also be done for any non-versioned entires from the Commit... or Check for modifications screens as well. Do not forget to commit the changes to the ignore list.

 

 

 

As an exercise, let's try to modify one of our projects slightly and see if we can the difference with the committed version. We add a comment to the POJO in the Java project. If we then select the project and choose Check for modifications, we see that the Java class was modified and we can see where exactly if we select Compare with base from the context menu.

 

 

 

TortoiseSVN is a great tool, here I have just shown how easy it is to use it together with NWDS 7.0 to add version control to local projects (or DCs without NWDI). Also check out WinMerge, which works well with TortoiseSVN, to compare files.