XI Mapping Module for AFW

Introduction

I have recently been searching for a module that could execute a generic mapping program, but I had no satisfaction. If you are wondering why I needed it, keep up reading and you'll probably find out yourself many scenarios in which this technique can be applied.
I searched standard doc first, only finding the MessageTransformBean which has serious limitations: first of all it was born for XI 2.0, and it needs the mapping program to be packaged and deployed along with the module in a .sda file.
Sure, a module that can execute a Java or XSLT mapping program packaged in the module .jar itself could have been enough, but I just never stop, I'm not satisfied by half-solutions...

The Goal

So the goal was to have my adapter module perform these basic tasks:

  • read its own parameters in the Communication Channel configuration to get mapping coordinates and other settings
  • dynamically retrieve the mapping (be that graphical, Java or XSLT)
  • execute the mapping by invoking the mapping runtime
  • put the resulting document as message payload
Graphically Speaking

In a common and simplified XI scenario, data flow can be represented by the picture below. Notice that steps 3 and 4, in which data are sent via RFC by the Integration Engine to the Mapping Runtime and the other way around (synchronous call to a Jco Server instance), is exactly what I aimed to avoid. Performance is seriously affected by this step. More, suppose you have an ABAP mapping as the main transformation program in your scenario, but you need a kind of pre-mapping Java or XSLT program to be executed before the ABAP class can process data... Don't you think it's really an overhead to go back and forth between the two stacks?
image
So comes the modified scenario, in which steps 3 and 4 (which is actually a single logical step, that is one mapping) are removed. In the picture below the Mapping Runtime is now invoked by the Adapter Framework in step 2 before the message is sent to the ABAP stack, and in step 5 before the message is sent the Messaging System for final delivery. In this kind of scenario, the two steps can be both present, or only one of them: one step represents one logical step, that is one transformation.
image
The module is flexible enough to be used both in sender and receiver communication channels: if you use it in step 2, say it acts like a kind of pre-processor, while if you use it in step 5, it acts like a post-processor. You can even have multiple instances of the same module in one communication channel, so that you can emulate a multi-mapping, where the result document of previous mapping will be the source of the following one...

NWDS project

So let's get it built.
For general information about how to build the right project in NetWeaver Developer Studio, please refer to this SAP howto. Here I will just underline and focus on needed additional settings.

image

In addition to common .jar files required by any module project, you need to download a couple more from your XI box, and put them in your project build path (not as external jars, but using the “Add Variable...” and “Extend” functions, otherwise the .jar would be packaged in your final .ear file, which is not dangerous but really useless).

JarName
Can be found at...

guidgenerator.jar
/usr/sap/<SID>/DVEBMGS00/j2ee/cluster/server0/bin/ext/com.sap.guid

aii_utilxi_misc.jar
/usr/sap/<SID>/DVEBMGS00/j2ee/cluster/server0/apps/sap.com/com.sap.xi.services

aii_mt_rt.jar
/usr/sap/<SID>/DVEBMGS00/j2ee/cluster/server0/apps/sap.com/com.sap.xi.services

aii_ibrun_server.jar
/usr/sap/<SID>/DVEBMGS00/j2ee/cluster/server0/apps/sap.com/com.sap.xi.services/ EJBContainer/applicationjars

Very important now are J2EE references, that you can find in the application-j2ee-engine.xml of your Enterprise Application addition project. In addition to common references needed by a module tu run smoothly in the J2EE, you need a couple more. You can find here below the xml source needed.
<reference reference-type="weak"> <reference-target provider-name="sap.com" target-type="application">com.sap.xi.services</reference-target> </reference> <reference reference-type="weak"> <reference-target provider-name="sap.com" target-type="library">com.sap.guid</reference-target> </reference>

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

<reference reference-type="weak">
    <reference-target provider-name="sap.com"
        target-type="application">com.sap.xi.services</reference-target>
</reference>

<reference reference-type="weak">
    <reference-target provider-name="sap.com"
        target-type="library">com.sap.guid</reference-target>
</reference>

 

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

Hint. Remember to set up also the JNDI name in your ejb-j2ee-engine.xml, which must match the name you'll use to invoke the module from a communication channel.
Now you're almost there... Just missing one thing more: the bean code.
I won't comment the code here in this weblog: it's really full of comments in itself.

 

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

package com.guarneri.xi.afw.modules;

import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.ejb.CreateException;

// XI specific imports
import com.sap.aii.af.mp.module.ModuleContext;
import com.sap.aii.af.mp.module.ModuleData;
import com.sap.aii.af.mp.module.ModuleException;
import com.sap.aii.af.ra.ms.api.*;
import com.sap.aii.af.service.auditlog.*;

// XML manipulation imports
import com.sap.aii.ibrun.sbeans.mapping.*;
import com.sap.aii.ibrun.server.mapping.MappingHandler;
import com.sap.aii.ibrun.server.mapping.api.TraceList;
import com.sap.guid.*;

// Other imports
import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
* @ejbHome <{com.guarneri.xi.afw.modules.AFWJavaMapHome}>
* @ejbLocal <{com.guarneri.xi.afw.modules.AFWJavaMapLocal}>
* @ejbLocalHome <{com.guarneri.xi.afw.modules.AFWJavaMapLocalHome}>
* @ejbRemote <{com.guarneri.xi.afw.modules.AFWJavaMap}>
* @stateless
* @transactionType Container
*/
public class AFWJavaMapBean implements SessionBean {

    final String auditStr = "*** AFWJavaMap - ";
    ModuleContext mc;
    boolean hangOnError;
    Object obj = null; // Handler to get Principle data
    Message msg = null; // Handler to get Message object
    Hashtable mp = null; // Module parameters
    AuditMessageKey amk = null;
    // Needed in order to write out on the message audit log

    public ModuleData process(
        ModuleContext moduleContext,
        ModuleData inputModuleData)
        throws ModuleException {

        Date dstart = new Date();
        byte[] out = null; // Mapping result

        // Creation of basic instances
        try {
            mc = moduleContext;
            obj = inputModuleData.getPrincipalData();
            msg = (Message) obj;
            mp =
                (Hashtable) inputModuleData.getSupplementalData(
                    "module.parameters");

            if (msg.getMessageDirection() == MessageDirection.INBOUND)
                amk =
                    new AuditMessageKey(
                        msg.getMessageId(),
                        AuditDirection.INBOUND);
            else
                amk =
                    new AuditMessageKey(
                        msg.getMessageId(),
                        AuditDirection.OUTBOUND);

        } catch (Exception e) {
            Audit.addAuditLogEntry(
                amk,
                AuditLogStatus.ERROR,
                auditStr
                    + "Error while creating basic instances (obj,msg,amk,mp)");
            throw new ModuleException(
                auditStr
                    + "Error while creating basic instances (obj,msg,amk,mp)");
        }

        Audit.addAuditLogEntry(
            amk,
            AuditLogStatus.SUCCESS,
            auditStr + "Process started");

        // See what we want to do in case of mapping failure (default is HANG)
        hangOnError = true;
        String hoe = mpget("hang.on.error");
        if (hoe != null)
            hangOnError =
                !hoe.equalsIgnoreCase("no") || !hoe.equalsIgnoreCase("false");

        // Audit source document if requested
        String auditSource = mpget("audit.source.message");
        if (auditSource != null)
            Audit.addAuditLogEntry(
                amk,
                AuditLogStatus.SUCCESS,
                auditStr
                    + "Source document: "
                    + new String(msg.getDocument().getContent()));

        // SWCV guid generation
        String swvcGuidStr = mpget("swcv.guid");
        if ((swvcGuidStr == null)
            || (swvcGuidStr.length() != 32 && swvcGuidStr.length() != 36))
            return this.handleReturn(
                inputModuleData,
                auditStr
                    + "No SWVC Guid was provided or Guid has wrong length. Exiting!",
                true);

        if (swvcGuidStr.length() == 32)
            // Perform some required (homemade) formatting
            swvcGuidStr =
                swvcGuidStr.substring(0, 8)
                    + "-"
                    + swvcGuidStr.substring(8, 12)
                    + "-"
                    + swvcGuidStr.substring(12, 16)
                    + "-"
                    + swvcGuidStr.substring(16, 20)
                    + "-"
                    + swvcGuidStr.substring(20, 32);

        IGUID swcv = null;
        try {
            IGUIDGeneratorFactory guidGenFac =
                GUIDGeneratorFactory.getInstance();
            IGUIDGenerator guidGen = guidGenFac.createGUIDGenerator();
            swcv = guidGen.parseGUID(swvcGuidStr);
        } catch (GUIDFormatException e2) {
            return this.handleReturn(
                inputModuleData,
                auditStr
                    + "Error while creting SWVC Guid (GUIDFormatException). Exiting!",
                true);
        }

        // Get the mapping type (if none provided assume it's graphical mapping)
        String mappingType = mpget("mapping.type").toUpperCase();
        if (!mappingType.equalsIgnoreCase("GRAPHICAL")
            && !mappingType.equalsIgnoreCase("JAVA")
            && !mappingType.equalsIgnoreCase("XSLT")) {
            return this.handleReturn(
                inputModuleData,
                auditStr
                    + "Wrong mapping type supplied "
                    + "(only GRAPHICAL | JAVA | XSLT are allowed). Exiting!",true);
        } else if (mappingType == null)
            mappingType = "GRAPHICAL";

        // Get mapping namespace (this is not vital, as the mapping search
        // is tolerant enough to look into the whole SWVC)    
        String ns = mpget("namespace");
        if (ns == null)
            ns = new String();

        // Get mapping name. Here I wanna be a good boy and allow people
        // to put it the natural way ;-)
        String mappingName = mpget("mapping.name");
        if (mappingName == null)
            return this.handleReturn(
                inputModuleData,
                auditStr + "No mapping name was provided. Exiting!",
                true);

        if (mappingType.equalsIgnoreCase("GRAPHICAL")) {
            // Let's build the real class name
            mappingName = "com/sap/xi/tf/" + "_" + mappingName + "_";
            mappingType = "JAVA";
        }

        // Check which trace level was requested (default to warning)
        char trLevCh = '1';
        String trLevel = mpget("trace.level");
        if (trLevel != null) {
            if (trLevel.equalsIgnoreCase("INFO"))
                trLevCh = '2';
            else if (trLevel.equalsIgnoreCase("DEBUG"))
                trLevCh = '3';
            else if (trLevel.equalsIgnoreCase("WARNING"))
                trLevCh = '1';
            else if (trLevel.equalsIgnoreCase("OFF"))
                trLevCh = '0';
        }

        // Create the messenger object
        Messenger mes = MappingDataAccess.createMessenger(trLevCh);

        // Instantiate MappingData object
        MappingData md = null;
        try {
            md =
                MappingDataAccess.createMappingData(
                    mappingType,
                    mappingName,
                    ns,
                    swcv,
                    -1,
                    "AFWJavaMap");
        } catch (Exception e) {
            return this.handleReturn(
                inputModuleData,
                auditStr + "Error instantiating MappingData: " + e);
        }

        // Put this MappingData object in an array (this emulates an
        // interface mapping with several mapping steps)   
        MappingData[] mds = new MappingData[1];
        mds[0] = md;

        // Build the map object
        Map map = null;
        try {
            // Here I just feed the messageId but with same tecnique
            // other useful stuff can be put
            // (it strongly depends on which Header Fields you access
            // in your mapping!)
            map = new HashMap();
            map.put("MessageId", msg.getMessageId());

            // Here I need to use reflection to get a valid instance
            // of the trace object
            Object tr = null;
            Class c = mes.getClass();
            Field trf = c.getDeclaredField("trace");
            trf.setAccessible(true);
            tr = trf.get(mes);
            map.put("MappingTrace", tr);
        } catch (Exception e) {
            return this.handleReturn(
                inputModuleData,
                auditStr
                    + "Error during HashMap and MappingTrace creation - "
                    + e
                    + " - Exiting!");
        }

        // Create the MappingHandler object
        MappingHandler mh = null;
        try {
            mh =
                new MappingHandler(
                    msg.getDocument().getContent(),
                    mds,
                    map,
                    mes);
        } catch (Exception e) {
            return this.handleReturn(
                inputModuleData,
                auditStr
                    + "Error during MappingHandler creation - "
                    + e
                    + " - Exiting!");
        }

        // Rock n' Roll: execute the mapping
        boolean mappingFailed = false;
        try {
            out = mh.run();
        } catch (Exception e1) {
            mappingFailed = true;
            // handleReturn is postponed to allow mapping trace
            // to be in the msg audit
        }

        // Trace management
        TraceList tl = mes.getTraceList();
        for (int i = 0; i < tl.size(); i++)
            Audit.addAuditLogEntry(
                amk,
                AuditLogStatus.SUCCESS,
                "Mapping TRACE: "
                    + "("
                    + tl.getItem(i).getLevel().toString()
                    + ") "
                    + tl.getItem(i).getMessage());

        if (mappingFailed)
            return this.handleReturn(
                inputModuleData,
                auditStr + "Error during mapping execution - Exiting!");

        // Audit resulting document if requested
        String auditResult = mpget("audit.result.message");
        if (auditResult != null)
            Audit.addAuditLogEntry(
                amk,
                AuditLogStatus.SUCCESS,
                auditStr + "Output document: " + new String(out));

        // New payload insertion
        try {
            XMLPayload newPayload = msg.getDocument();
            newPayload.setContent(out);
            msg.setDocument(newPayload);
            inputModuleData.setPrincipalData(msg);
        } catch (Exception e) {
            return this.handleReturn(
                inputModuleData,
                auditStr + "Error while inserting new payload.");
        }

        // Stats
        Date dend = new Date();
        Audit.addAuditLogEntry(
            amk,
            AuditLogStatus.SUCCESS,
            auditStr
                + "Process completed - "
                + "(execution "
                + (dend.getTime() - dstart.getTime())
                + " ms)");

        // Return
        return inputModuleData;

    }

    private ModuleData handleReturn(ModuleData md, String strmsg)
        throws ModuleException {
        Audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, auditStr + strmsg);
        if (hangOnError) {
            // Audit anyway source document to let developers understand
            // what went wrong by doing some test/debug
            Audit.addAuditLogEntry(
                amk,
                AuditLogStatus.SUCCESS,
                auditStr
                    + "Source document: "
                    + new String(msg.getDocument().getContent()));
            // TODO: This is a temporary workaround!
            // Put a dummy error XML message as output so that the message
            // won't have any application meaning
            Message lmsg = (Message) md.getPrincipalData();
            XMLPayload dummypl = lmsg.createXMLPayload();
            try {
                dummypl.setContent(
                    new String(
                        "<?xml version=\"1.0\"?>"
                            + "<Error>Mapping failed in module</Error>")
                        .getBytes());
                lmsg.setDocument(dummypl);
            } catch (PayloadFormatException e) {
            } catch (InvalidParamException e) {
            }
        }

        return md;
    }

    private ModuleData handleReturn(
        ModuleData md,
        String strmsg,
        boolean throwEx)
        throws ModuleException {
        throw new ModuleException(strmsg);
    }

    private String mpget(String pname) {
        return (String) mc.getContextData(pname);
    }

    public void ejbRemove() {
    }

    public void ejbActivate() {
    }

    public void ejbPassivate() {
    }

    public void setSessionContext(SessionContext context) {
        myContext = context;
    }

    private SessionContext myContext;
    /**
     * Create Method.
     */
    public void ejbCreate() throws CreateException {

    }

}

 

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

Usage

The module supports these 8 parameters:

Parameter Name

Mandatory

Values

Description

swcv.guid
Yes
Any valid SWCV Guid
The guid of the Software Component Version in which the mapping resides

mapping.name
Yes
Any valid mapping name
The name of the mapping program. In case of graphical mapping it's just the name of the mapping you can see in the repository. In case of Java mapping, the name must be fully qualified with package (e.g. com.yourcompany.mapping), visible in the relevant imported archive. In case of XSLT mapping, the name of the XSL file, visible in the relevant imported archive.

mapping.type
No
GRAPHICAL (default) | JAVA | XSLT
Determine the type of mapping

namespace
No
Any valid namespace
The namespace in which the mapping resides. I know it sounds strange, but this is not mandatory as the mapping lookup function is just so tolerant that searches in the whole SWCV. Of course, giving the right namespace increases performance.

trace.level
No
OFF | WARNING (default) | INFO | DEBUG
Determine the mapping trace level that you want to output in the message audit log.

hang.on.error
No
Yes (default) | No
Determine whether the message will stop if something fails. This needs additional work: currently, upon error, a dummy XML document is put in the message in place of the real one.

audit.source.message
No
Any value
Deactivated by default. If set, the source document will be written to the audit log. Useful for sender channels.

audit.result.message
No
Any value
Deactivated by default. If set, the result document will be written to the audit log. Useful for receiver channels.

I think the above table is eloquent enough to let you guess how powerful this guy is...

image

Just one hint, maybe useless: to find out the Software Component Version Guid, in IR just double click on it in the left tree, let it open in the right pane and then go to the "Software Component Version" menu and choose "Properties..." It's the "Object ID".

Conclusion

I consider this development o' mine a beta version, though perfectly workin', that I will refine with the help of all you SDN'ers out there.
I also expect someone to write a weblog on the implications and use cases that this module can have on XI projects.
Finally, I must be honest: it was a painful one ;-) ... But in the end greatly satisfactory!
Special thanks go to my wife Nunzia for puttin' up with my frustration when I was hardly finalizin' and to the great Pietro Marchesani for his support and for our chat about Java Reflection one drunky night in Athens.

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