Michael Bluteau

New SAP AFX Connector - JavaCodeBased Tutorial

Blog Post created by Michael Bluteau Employee on Jun 15, 2016

I posted SAP collectors and connectors earlier on RSA Link, and this time I am porting my recent AFX Connector for SAP with extra capabilities(AddAccountToGroup, AddProfile or AddCompositeProfile, Lock/Unlock) and attributes(e.g. email, department) to the JavaCodeBased AFX Connector, which is pretty much aimed at developers as a SDK.  However, good thing, once a connector is created with custom Java and jars using the toolkit(available here:  https://knowledge.rsasecurity.com/scolcms/set.aspx?id=10689 ) the connector can be exported and imported to a new environment without having to manually recreate it.

 

My original connector calling similar Java code but using SSH Connector required the use of a properties file, and was also using output files for the SAP Destination provider.  I improve on those aspects by pushing the settings and credentials to where it should belong, the Settings tab for the connector, and I eliminated the output file Destination provider, and cleaned up the Java a bit.  The original connector can be found here:  SAP BAPI/Jco Connector using AFX(SSH + Java)

 

Let's get started first with a high level overview, then we can dive into the JavaCodeBased tutorial portion.  First you will need an instance of L&G that can reach the outside world or your test SAP, and then if you don't have a test SAP environment, you can get one from IDES (see http://idesaccess.com/  ).  Then you can import the connector, change the credentials and system info on the Settings page, and then you can start testing the capabilities.  Note that not all instances from IDES seem to provide the right permissions through Jco for the service account they provide, you may need to ask for the permissions.

 

After importing the connector, you need to provide your SAP information and credentials:

SAP-JavaCodeBase-1.JPG

SAP-JavaCodeBase-2.JPG

Then you can take a look at the capabilities, which you need to create manually if you start with a brand new JavaCodeBased connector:

SAP-JavaCodeBase-3.JPG

SAP-JavaCodeBase-4.JPG

SAP-JavaCodeBase-5.JPG

SAP-JavaCodeBase-6.JPG

SAP-JavaCodeBase-7.JPG

Now there is a gap with the 7.0 version of JavaCodeBased connector, it only allows for importing jar files, while SAP requires also libsapjco3.so library which is not a jar.  The temporary workaround(until a fix or better workaround is identified) is to upload the file manually under the connector, and touch the config file to restart the connector without redeploying it(which would wipe out the file):

SAP-JavaCodeBase-9.JPG

SAP-JavaCodeBase-10.JPG

Now we can start testing capabilities.  Let's start with CreateAccount:

 

SAP-JavaCodeBase-8.JPG

SAP-JavaCodeBase-11.JPG

In order to test that CreateAccount worked, you can just try to login to SAP with new account/temporary password.  You will be prompted to select a new password:

SAP-JavaCodeBase-12.JPG

 

SAP-JavaCodeBase-13.JPG

Then you can test AddAccountToGroup.  For this, we will use SU01D:

SAP-JavaCodeBase-14.JPG

SAP-JavaCodeBase-15.JPG

SAP-JavaCodeBase-16.JPG

SAP-JavaCodeBase-17.JPG

SAP-JavaCodeBase-18.JPG

Then add Profile(AddAppRoleToAccount):

SAP-JavaCodeBase-19.JPG

SAP-JavaCodeBase-20.JPG

Then add Role to Account:

SAP-JavaCodeBase-21.JPG

SAP-JavaCodeBase-22.JPG

Then Lock and Unlock Account.  We will use SE11 to show USR02 table to confirm that the status is changed on the SAP side:

SAP-JavaCodeBase-23.JPG

SAP-JavaCodeBase-24.JPG

SAP-JavaCodeBase-25.JPG

SAP-JavaCodeBase-26.JPG

SAP-JavaCodeBase-27.JPG

SAP-JavaCodeBase-28.JPG

SAP-JavaCodeBase-29.JPG

Then lastly, ResetPassword, for which you can just try to login again with new temporary password:

SAP-JavaCodeBase-30.JPG

 

That's already more capabilities and attributes than the official SAP connector.  It is possible to add support for other capabilities and attributes, and one way to figure out what ABAP functions are available and what is the type for each attribute is to 1- Google to find out the name of the BAPI function that does what you want 2- Use SE37 Function Builder to test those functions(see last section below).

 

Once you know which ABAP you want, you can edit the source code and recompile.  Since a jar is just a renamed zip file, you can expand the jar archive and rezip/rename after your changes.  When you update the connector, upload the new jar with a different name, then delete the old jar and click OK even if the screen does not come back(seems to be a bug in 7.0).  Confirm that your new jar is uploaded in config.

 

Since I am not a developer, I don't have Eclipse or fancy tools on my desktop, so I just use Notepad to edit the Java source file.  Here is a quick tutorial on the way the file works for the SAP capabilities, but you could use this example or the one provided with the toolkit(Oracle) to build a new connector.

 

First we will create the Java as a package, and then add the needed dependencies(extra ones here, we could clean that up):

-----------------------------------------------------

 

package jcbc.src.sap;

import java.util.ArrayList;

import java.util.List;

import java.util.Map;

import java.io.File;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.PrintWriter;

import java.io.OutputStream;

import java.io.PrintStream;

import java.util.Hashtable;

import java.util.Properties;

import java.io.FileInputStream;

import java.io.InputStream;

import java.io.FileWriter;

import com.aveksa.afx.common.connector.service.JavaCodeBasedConnectorBase;

import com.aveksa.afx.common.connector.service.data.JCBCProperty;

import com.aveksa.afx.common.connector.service.data.JCBCPropertyType;

import com.aveksa.afx.common.connector.service.response.JCBCStatus;

import com.sap.conn.jco.AbapException;

import com.sap.conn.jco.JCoDestination;

import com.sap.conn.jco.JCoDestinationManager;

import com.sap.conn.jco.JCoException;

import com.sap.conn.jco.JCoField;

import com.sap.conn.jco.JCoFunction;

import com.sap.conn.jco.JCoStructure;

import com.sap.conn.jco.JCoTable;

import com.sap.conn.jco.JCoRecordFieldIterator;

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

import com.sap.conn.jco.ext.JCoSessionReference;

import com.sap.conn.jco.ext.SessionException;

import com.sap.conn.jco.ext.SessionReferenceProvider;

import com.sap.conn.jco.ext.DestinationDataEventListener;
------------------------------------------------------------
Then we need to create a class that extends JavaCodeBasedConnectorBase :
------------------------------------------------------------

public class SapImpl extends JavaCodeBasedConnectorBase {

 

private static final String PARAM_JCO_USER = "JcoUser";

private static final String PARAM_JCO_PASSWD = "JcoPassword";

private static final String PARAM_JCO_CLIENT = "JcoClient";

private static final String PARAM_JCO_ASHOST = "JcoASHost";

private static final String PARAM_JCO_SYSNR = "JcoSysNr";

private static final String PARAM_JCO_LANG = "JcoLang";

private static final String PARAM_JCO_POOL_CAPACITY = "JcoPoolCapacity";

private static final String PARAM_JCO_PEAK_LIMIT = "JcoPeakLimit";

//private static final String DRIVER_CLASS_NAME = "com.sap.conn.jco";



@Override

public List<JCBCProperty> getSettings() {

final String category = "Authentication";

JCBCProperty JcoASHost  = createProperty(PARAM_JCO_ASHOST, PARAM_JCO_ASHOST, PARAM_JCO_ASHOST, category,JCBCPropertyType.STRING);

JCBCProperty userName = createProperty(PARAM_JCO_USER, PARAM_JCO_USER, PARAM_JCO_USER, category, JCBCPropertyType.STRING);

JCBCProperty password = createProperty(PARAM_JCO_PASSWD, PARAM_JCO_PASSWD, PARAM_JCO_PASSWD, category, JCBCPropertyType.PASSWORD);

JCBCProperty JcoClient  = createProperty(PARAM_JCO_CLIENT, PARAM_JCO_CLIENT, PARAM_JCO_CLIENT, category,JCBCPropertyType.STRING);

JCBCProperty JcoSysNr  = createProperty(PARAM_JCO_SYSNR, PARAM_JCO_SYSNR, PARAM_JCO_SYSNR, category,JCBCPropertyType.STRING);

JCBCProperty JcoLang  = createProperty(PARAM_JCO_LANG, PARAM_JCO_LANG, PARAM_JCO_LANG, category,JCBCPropertyType.STRING);

JCBCProperty JcoPoolCapacity  = createProperty(PARAM_JCO_POOL_CAPACITY, PARAM_JCO_POOL_CAPACITY, PARAM_JCO_POOL_CAPACITY, category,JCBCPropertyType.STRING);

JCBCProperty JcoPeakLimit  = createProperty(PARAM_JCO_PEAK_LIMIT, PARAM_JCO_PEAK_LIMIT, PARAM_JCO_PEAK_LIMIT, category,JCBCPropertyType.STRING);



List<JCBCProperty> parameterList = new ArrayList<JCBCProperty>(8);

parameterList.add(JcoASHost);

parameterList.add(userName);

parameterList.add(password);

parameterList.add(JcoClient);

parameterList.add(JcoSysNr);

parameterList.add(JcoLang);

parameterList.add(JcoPoolCapacity);

parameterList.add(JcoPeakLimit);      

return parameterList;

}
---------------------------------------------
We are basically capturing the settings from the Settings tab of the Connector, 8 values, and storing that in a reusable container.
We also need a private method that we call in that class:
---------------------------------------------

private JCBCProperty createProperty(String name, String displayName, String description, String category, JCBCPropertyType type) {

JCBCProperty prop =  new JCBCProperty(name, displayName, description);

prop.setCategory(category);

prop.setRequired(true);

prop.setEncrypted(false);

prop.setPropertyType(type);

prop.setNumRows(0);

return prop;

}
--------------------------------------------
Then we need to define our Destination Data Provider for SAP:
--------------------------------------------

    static class MyDestinationDataProvider implements DestinationDataProvider

    {

        private DestinationDataEventListener eL;



        private Properties ABAP_AS_properties;

      

        public Properties getDestinationProperties(String destinationName)

        {

            if(destinationName.equals("ABAP_AS") && ABAP_AS_properties!=null)

                return ABAP_AS_properties;

          

            return null;

            //alternatively throw runtime exception

            //throw new RuntimeException("Destination " + destinationName + " is not available");

        }



        public void setDestinationDataEventListener(DestinationDataEventListener eventListener)

        {

            this.eL = eventListener;

        }



        public boolean supportsEvents()

        {

            return true;

        }

      

        void changePropertiesForABAP_AS(Properties properties)

        {

            if(properties==null)

            {

                ABAP_AS_properties = null;

                eL.deleted("ABAP_AS");

            }

            else

            {

                if(ABAP_AS_properties==null || !ABAP_AS_properties.equals(properties))

                {

                    ABAP_AS_properties = properties;

                    eL.updated("ABAP_AS");

                }

            }

        }

    }
---------------------------------------------------------------------
Then we are down to the Capabilities.  First one is CreateAccount.  Here is the first portion, almost identical for all capabilities except the list of Parameters is mapped to Connector capabilities parameters:
---------------------------------------------------------------------



@Override
public JCBCStatus createAccount(Map<String, String> settings, Map<String, Object> params) throws Exception {
  String username = (String) params.get("UserName");
  String password = (String) params.get("Password");
  String firstname = (String) params.get("firstname");
  String lastname = (String) params.get("lastname");
  String email = (String) params.get("email");
  String department = (String) params.get("department");
              if(!com.sap.conn.jco.ext.Environment.isDestinationDataProviderRegistered()){
        Properties connectProperties = new Properties();
        connectProperties.setProperty(DestinationDataProvider.JCO_ASHOST, settings.get(PARAM_JCO_ASHOST));
        connectProperties.setProperty(DestinationDataProvider.JCO_SYSNR,  settings.get(PARAM_JCO_SYSNR));
        connectProperties.setProperty(DestinationDataProvider.JCO_CLIENT, settings.get(PARAM_JCO_CLIENT));
        connectProperties.setProperty(DestinationDataProvider.JCO_USER,   settings.get(PARAM_JCO_USER));
        connectProperties.setProperty(DestinationDataProvider.JCO_PASSWD, settings.get(PARAM_JCO_PASSWD));
        connectProperties.setProperty(DestinationDataProvider.JCO_LANG,   settings.get(PARAM_JCO_LANG));
        connectProperties.setProperty(DestinationDataProvider.JCO_POOL_CAPACITY, settings.get(PARAM_JCO_POOL_CAPACITY));
        connectProperties.setProperty(DestinationDataProvider.JCO_PEAK_LIMIT,    settings.get(PARAM_JCO_PEAK_LIMIT));
    
        MyDestinationDataProvider myProvider = new MyDestinationDataProvider();  
          
        com.sap.conn.jco.ext.Environment.registerDestinationDataProvider(myProvider);  
        myProvider.changePropertiesForABAP_AS(connectProperties);
            }
     JCoDestination destination = JCoDestinationManager.getDestination("ABAP_AS");

 

 

 

 

-----------------------------------------------------------

Note that we perform a test for every capability to check is DestinationDataProvider is already registered. Otherwise if you try a second capability without restarting the connector, you will get a Java error stating that the Destination Data Provider is already registered.

 

Another approach, similar to the one I used in my previous SSH connector, was to define the connectProperties in first main/first class, but the JavaCodeBased is complaining about a missing sapjco3 library, so it looks like it is performing some extra test when jar is uploaded, and this error was preventing me from uploading the jar. So a little extra code here, and the if condition to prevent the connector from trying to register the Destination multiple times.

----------------------------------------------------------

And now the second portion, which is specific to each capability:

----------------------------------------------------------

 

 

           

  JCoFunction bapiUserCreate = destination.getRepository().getFunctionTemplate("BAPI_USER_CREATE").getFunction();
            
              if(bapiUserCreate != null){
              
                JCoStructure myAddress = bapiUserCreate.getImportParameterList().getStructure("ADDRESS");
                  myAddress.setValue("FIRSTNAME",firstname);
                  myAddress.setValue("LASTNAME",lastname);
                  myAddress.setValue("FULLNAME",firstname+" "+lastname);
                  myAddress.setValue("E_MAIL",email);
                  myAddress.setValue("DEPARTMENT",department);                      
               
                   bapiUserCreate.getImportParameterList().setValue("USERNAME", username);
                JCoStructure myPassword = bapiUserCreate.getImportParameterList().getStructure("PASSWORD");
                  myPassword.setValue("BAPIPWD",password);
                     bapiUserCreate.execute(destination);
             
  }
  return JCBCStatus.success();
}

 

 

---------------------------------------------------------------------

Note that the ADDRESS is a structure that contains the attributes(FIRSTNAME, LASTNAME, etc) and that PASSWORD is also a structure with BAPIPWD as an attribute.  You can figure out the type of attribute and structure by investigating the BAPI_USER_CREATE function in SE37, best way is in Test mode.

 

The 2nd portion for DeleteAccount is simpler, and you will find similar examples with Lock and Unlock account:

---------------------------------------------------------------------

 

           

JCoFunction bapiUserDelete = destination.getRepository().getFunctionTemplate("BAPI_USER_DELETE").getFunction();
            
              if(bapiUserDelete != null){
              
                   bapiUserDelete.getImportParameterList().setValue("USERNAME", username);
                     bapiUserDelete.execute(destination);
             
  }
  return JCBCStatus.success();
}

-------------------------------------------------------------

Reset Password is a little bit different than setting the initial password, since we need to change the existing value:

-------------------------------------------------------------

             

JCoFunction bapiUserChange = destination.getRepository().getFunctionTemplate("BAPI_USER_CHANGE").getFunction();
            
              if(bapiUserChange != null){
              
                   bapiUserChange.getImportParameterList().setValue("USERNAME", username);
                JCoStructure myPasswordX = bapiUserChange.getImportParameterList().getStructure("PASSWORDX");
                  myPasswordX.setValue("BAPIPWD","X");
                JCoStructure myPassword = bapiUserChange.getImportParameterList().getStructure("PASSWORD");
                  myPassword.setValue("BAPIPWD",password);
                     bapiUserChange.execute(destination);
             
  }
  return JCBCStatus.success();

----------------------------------------------------------

AddAccountToGroup is different since we are introducing tables:

----------------------------------------------------------

 

             

JCoFunction bapiUserChange = destination.getRepository().getFunctionTemplate("BAPI_USER_CHANGE").getFunction();
            
              if(bapiUserChange != null){
              
                JCoStructure myGroupsX = bapiUserChange.getImportParameterList().getStructure("GROUPSX");
                  myGroupsX.setValue("USERGROUP","X");
         JCoTable inputTable = bapiUserChange.getTableParameterList().getTable("GROUPS");
         inputTable.appendRow();
         inputTable.setValue("USERGROUP", group);
                                        
                   bapiUserChange.getImportParameterList().setValue("USERNAME", username);
                     bapiUserChange.execute(destination);            
  }
  return JCBCStatus.success();
}

----------------------------------------------------------

We also use tables for Profiles and Roles:

----------------------------------------------------------

           

JCoFunction bapiUserChange = destination.getRepository().getFunctionTemplate("BAPI_USER_ACTGROUPS_ASSIGN").getFunction();
            
              if(bapiUserChange != null){
              
         JCoTable inputTable = bapiUserChange.getTableParameterList().getTable("ACTIVITYGROUPS");
         inputTable.appendRow();
         inputTable.setValue("AGR_NAME", approle);
                        
               
                   bapiUserChange.getImportParameterList().setValue("USERNAME", username);
                     bapiUserChange.execute(destination);          
  }
  return JCBCStatus.success();
}

-------------------------------------------------------

Usually I just copy my source file to the L&G server and that is where I compile, using the simple script provided in jar archive.  I point to /home/oracle/mysap to find the sapjco3.jar and libsapjco3.so at compilation time.

 

Now if you want to explore and add new capabilities and attributes, here is a quick walkthrough for SE37:

See: List of BAPI's - ABAP Development - SCN Wiki

 

SAP-JavaCodeBase-31.JPG

SAP-JavaCodeBase-32.JPG

SAP-JavaCodeBase-33.JPG

SAP-JavaCodeBase-34.JPG

COMPANYX is associated to COMPANY etc, so for a Change you need to deal with both as shown in code above.

SAP-JavaCodeBase-35.JPG

For Structure, you need to deal with both the attribute in address and its associated X value in ADDRESSX as illustrated in code above.

SAP-JavaCodeBase-36.JPG

Example for a table.

Outcomes