Wrapping your Mapping Lookup API code in easy-to-use Java classes

 

Introduction

The Mapping Lookup API, introduced in SP13, enables XI developers to call RFCs from user-defined functions and Java mapping programs. However, using the API means handling lower-level details like communicating with the RFC adapter, building the XML request and parsing the XML response. This can lead to programs that contain lots of boilerplate code and are messy and hard to reuse. In this blog entry I outline a a cleaner, more object-oriented solution. I demonstrate the technique by wrapping calls to BAPI_CUSTOMER_GETLIST in an easy-to-use Java class.

Taking an OOP approach

Whenever you call an RFC in a remote system using the Mapping Lookup API, the code that handles the request and response documents is specific to the function module being called. However, other parts of the code are independent of the remote function module, e.g. the code that delivers XML to and receives XML from the RFC adapter. An OOP approach to this situation would be moving the common code to a superclass and keeping the function module specific code in subclasses that extend this superclass. The benefit of this approach is less code duplication and a cleaner separation of responsibilities.

The sample code below consists of three classes: LookupBase, CountryLookup and LookupException. LookupBase is an abstract class that contains the common code. Its abstract methods, buildRequestXml and parseRfcResponse, are specific to the function module being called and must be implemented by all concrete subclasses. CountryLookup extends LookupBase and contains the code that is specific to our requirement, which is looking up a customer's country of residence by executing BAPI_CUSTOMER_GETLIST. LookupException is an exception indicating that a problem occurred during the lookup.

The sample code

DISCLAIMER: Please note that the following source code is provided for educational purposes only. It is not intended for production use.

LookupBase.java

 

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

package dk.applicon.xi.mapping.lookup;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import com.sap.aii.mapping.api.MappingTrace;
import com.sap.aii.mapping.lookup.Channel;
import com.sap.aii.mapping.lookup.LookupService;
import com.sap.aii.mapping.lookup.RfcAccessor;
import com.sap.aii.mapping.lookup.XmlPayload;

public abstract class LookupBase {
    private String bservice;
    private String cchannel;
    private MappingTrace trace;

    /*
     * Getters and setters
     */

    public String getService() {
        return bservice;
    }

    public void setService(String bservice) {
        if (bservice == null) {
            throw new IllegalArgumentException("Null bservice");
        }
        this.bservice = bservice;
    }

    public String getChannel() {
        return cchannel;
    }

    public void setChannel(String cchannel) {
        if (cchannel == null) {
            throw new IllegalArgumentException("Null cchannel");
        }
        this.cchannel = cchannel;
    }

    public void setTrace(MappingTrace trace) {
        if (trace == null) {
            throw new IllegalArgumentException("Null trace");
        }
        this.trace = trace;
    }

    /*
     * Public methods
     */
    public void lookup() throws LookupException {
        checkState(); // Throws an IllegalStateException if the object hasn't been configured correctly
        InputStream rfcResponse = executeRemote();
        parseRfcResponse(rfcResponse);
    }      

    /*
     * Protected methods (i.e. available to subclasses)
     */
    protected void checkState() {
        if (bservice == null) {
            throw new IllegalStateException("Incorrect state: Business Service has not been set");
        }
        if (cchannel == null) {
            throw new IllegalStateException("Incorrect state: Communication Channel has not been set");
        }
    }
    protected void writeTrace(String info) {
        if (trace != null) {
            trace.addInfo(info);
        }
    }

    /*
     * Private methods (i.e. not available to subclasses)
     */
    private InputStream executeRemote() throws LookupException {
        InputStream rv = null;
        RfcAccessor accessor = null;
        try {
            Channel channel = LookupService.getChannel(bservice, cchannel);
            String req = buildRequestXml();
            accessor = LookupService.getRfcAccessor(channel);
            InputStream xmlIn = new ByteArrayInputStream(req.getBytes("UTF-8"));
            XmlPayload payload = LookupService.getXmlPayload(xmlIn);
            XmlPayload result = accessor.call(payload);
            rv = result.getContent();
        } catch (Exception e) {
            throw new LookupException("RFC execution failed", e);
        } finally {
            if (accessor != null) {
                try {
                    accessor.close();
                } catch (Exception e) {
                    // Oh well, at least we tried
                }
            }
        }
        return rv;
    }

    /*
     * Abstract methods (i.e. must be implemented by all concrete subclasses)
     */

    protected abstract String buildRequestXml() throws LookupException;
    protected abstract void parseRfcResponse(InputStream rfcResponse) throws LookupException;
}

 

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

CountryLookup.java

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

package dk.applicon.xi.mapping.lookup;

import java.io.InputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class CountryLookup extends LookupBase {

    private static final String COUNTRY_ELEMENT_NAME = "COUNTRYISO";
    private static final String DEFAULT_VALUE = "";
    private String customerId;
    private String country;
    private boolean lookupPerformed;

    /*
     * Constructors
     */
    public CountryLookup() {
        super();
        lookupPerformed = false;
    }

    /*
     * Getters and setters
     */
    public String getCustomerId() {
        return customerId;
    }

    public void setCustomerId(String customerId) {
        if (customerId == null) {
            throw new IllegalArgumentException("Null customerId");
        }
        this.customerId = customerId;
    }
    public String getCountry() {
        if (!lookupPerformed) {
            throw new IllegalStateException("Lookup not performed");
        }
        return country;
    }

    /*
     * Implement the superclass's abstract methods
     */
    protected String buildRequestXml() throws LookupException {
        try {
            Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            Node docRoot = doc.appendChild(doc.createElementNS("urn:sap-com:document:sap:rfc:functions", "ns:BAPI_CUSTOMER_GETLIST"));
            Node item = docRoot.appendChild(doc.createElement("IDRANGE"))
                               .appendChild(doc.createElement("item"));
            item.appendChild(doc.createElement("SIGN"))
                .appendChild(doc.createTextNode("I"));
            item.appendChild(doc.createElement("OPTION"))
                .appendChild(doc.createTextNode("EQ"));
            item.appendChild(doc.createElement("LOW"))
                .appendChild(doc.createTextNode(customerId));
            String requestXml = doc.toString();
            writeTrace("Request XML: " + requestXml);
            return requestXml;
        } catch (Exception e) {
            throw new LookupException("Error building request XML", e);
        }
    }

    protected void parseRfcResponse(InputStream rfcResponse) throws LookupException {
        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document doc = builder.parse(rfcResponse);
            writeTrace("Response XML: " + doc.toString());
            NodeList nl = doc.getElementsByTagName(COUNTRY_ELEMENT_NAME);
            if (nl.getLength() != 1) {
                // Expecting one element
                throw new AssertionError(nl.getLength());
            }
            Node countryNode = nl.item(0);
            if (countryNode.hasChildNodes()) {
                country = countryNode.getFirstChild().getNodeValue();
            } else {
                country = DEFAULT_VALUE;
            }
            lookupPerformed = true;
        } catch (Exception e) {
            throw new LookupException("Error parsing response", e);
        }
    }

    protected void checkState() {
        super.checkState();
        if (customerId == null) {
            throw new IllegalStateException("Incorrect state: Customer ID has not been set");
        }
    }
}

 

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

LookupException.java

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

package dk.applicon.xi.mapping.lookup;

public class LookupException extends Exception {

    public LookupException(String message) {
        super(message);
    }
    public LookupException(String message, Throwable cause) {
        super(message, cause);
    }
    public LookupException(Throwable cause) {
        super(cause);
    }

}

 

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

Using the CountryLookup class

Using the CountryLookup class in a user-defined function is straightforward:

// First, get a CountryLookup instance.
CountryLookup cl = new CountryLookup();

// Set the Business Service representing
// the remote system.
cl.setService("R3LookupSystem");

// Set the Communication Channel used for
// calling RFCs in the remote system.
cl.setChannel("RFC");

// Set the ID of the customer.
cl.setCustomerId(customerId);

// Execute the RFC in the remote system
// (might throw a LookupException).
try {
cl.lookup();
} catch (LookupException e) {
// Your error handling goes here.
}

// Finally, return the customer's
// country of residence.
return cl.getCountry();



If you'd like to write the request and response documents to the trace information (at level 2 = info), add the following line before the call to lookup:




cl.setTrace(container.getTrace());



Rolling your own


Follow these steps if you'd like to create a wrapper class for a different RFC:





  1. Create a new subclass of LookupBase




  2. Add the instance variables that will hold the information required by the RFC and the information returned by the RFC




  3. Add getter and setter methods that grant read/write access to the information required by the RFC




  4. Add getter methods that grant read access to the information returned by the RFC




  5. Override checkState, refer to CountryLookup for an example




  6. Build the request document in method buildRequestXml




  7. Parse the response document in method parseRfcResponse, storing the returned information in the appropriate instance variables




  8. There is no step 8 :-)




DOM vs SAX


In the example code I use DOM to parse the response document. Keep in mind, though, that DOM may not always be the right choice. With DOM, your parsing code will generally be easier to read and write, but parsing large documents can potentially consume a lot of resources. The reason for this is that an in-memory node tree is built for each document you parse. SAX parsing, on the other hand, requires far less resources. However, the event-driven nature of SAX will generally result in more complex code, since the state of the parsing has to be maintained by the developer.



Further reading


In the above I've outlined an OOP approach to Mapping Lookup API code. For a completely different take on how to execute RFCs in remote systems using the Mapping Lookup API, I suggest you take a look at "Easy RFC Lookup From XSLT Mappings Using a Java Helper Class", written by my colleague Thorsten Nordholm Søbirk.



SAP XI Interview Questions



SAP XI/PI Tutorials



SAP XI Training

SAP Developer Network SAP Weblogs: SAP Process Integration (PI)