Skip to Content

Background:

Migrate from old B2B integration platform to SAP Process Orchestration using B2B add-on for all Ansi x12 EDI transmissions.

 

Problem Statement:

In old platform, the system allowed for the maintenance of n number of IDs for any possibly qualifier, e.g. ZZ.  When we were initially migrating the new system also allowed this, but would overwrite any data saved to the party in the integration directory with only the last entry saved in the Trading Partner Management.  What we discovered was that any 997 transmissions would only work for the last ZZ qualifier ID that was saved which usually meant only one of our many external partners.  Since our old system allowed for n number of IDs we generally had one unique ID for each separate trading partner.  How do we limit ourselves to one ID for qualifier ZZ without asking our external partners to modify their configurations?

 

Workaround:

Can I add two IDs with the same qualifier into the system for our self-party?

Adjust the text for the agency scheme as seen below so all the values would save in the integration directory for the self-party – i.e. put some unique number at the end.

The result in Integration Directory after adjusting the scheme text.

Do I get any side effects in the system by adjusting this text?  I don’t really want to find out the hard way.

 

Alternative: A custom adapter module which will target specific segments of the XML payload and overwrite the value needed for TPMContentAccessModule with the value required by our external partner.

 

Code:

/*
 * Created: August 3rd, 2015
 * @author Ryan Crosby
 */

package com.yourorganization.utils.xml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Enumeration;

import javax.ejb.Stateless;
import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.sap.aii.af.lib.mp.module.Module;
import com.sap.aii.af.lib.mp.module.ModuleContext;
import com.sap.aii.af.lib.mp.module.ModuleData;
import com.sap.aii.af.lib.mp.module.ModuleException;
import com.sap.engine.interfaces.messaging.api.Message;
import com.sap.engine.interfaces.messaging.api.MessageKey;
import com.sap.engine.interfaces.messaging.api.MessagePropertyKey;
import com.sap.engine.interfaces.messaging.api.PublicAPIAccess;
import com.sap.engine.interfaces.messaging.api.PublicAPIAccessFactory;
import com.sap.engine.interfaces.messaging.api.XMLPayload;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditAccess;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditLogStatus;
import com.sap.engine.interfaces.messaging.api.exception.InvalidParamException;
import com.sap.engine.interfaces.messaging.api.exception.MessagingException;

@Stateless
public class XPathReplacementBean implements SessionBean, Module{
	/**
	 * 
	 */
	private static final long serialVersionUID = -2801247541078166093L;
	private MessageKey amk;
	private AuditAccess audit;
	private Message msg;

	@Override
	public ModuleData process(ModuleContext mc, ModuleData md) 
			throws ModuleException {
	  // Get message data and audit access
	  msg = (Message) md.getPrincipalData();
	  XMLPayload payload = msg.getDocument();
	  amk = new MessageKey(msg.getMessageId(), msg.getMessageDirection());
	  PublicAPIAccess pa;
	  try {
	  pa = PublicAPIAccessFactory.getPublicAPIAccess();
	  } catch (MessagingException e) {
	    throw new ModuleException(e.getMessage());
	  }
	  audit = pa.getAuditAccess();
	  
	  // Get full list of keys 
	  Enumeration e = mc.getContextDataKeys();
	  ArrayList<String> al = new ArrayList<String>();
	  while(e.hasMoreElements())
	  {
		String element = e.nextElement().toString();
		if(!element.equals("module.key"))
		  al.add(element);
	  }
	  
	  // Setup xml parsing to replace values based on XPath expressions
	  String xPathExpression = "", xPathReplacementValue = "";
	  try {
		// Perform XPath replacement in iterative fashion
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		Document doc = db.parse(payload.getInputStream());
		XPath xpath = XPathFactory.newInstance().newXPath();
		for(int i = 0; i < al.size(); i++)
		{
		  // Do 1:n replacements in case XPath has several matches
		  xPathExpression = al.get(i);

		  // Remove all whitespace before parsing expression
		  xPathExpression = xPathExpression.replaceAll(" ", "");
		  xPathReplacementValue = parse(mc.getContextData(xPathExpression));
		  audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, 
		    "Replacing values at " + xPathExpression + " with " + xPathReplacementValue);
		  NodeList nl = (NodeList) xpath.evaluate(xPathExpression, doc, XPathConstants.NODESET);
          int length = nl.getLength();
          for(int j = 0; j < length; j++)
          {
        	Node node = nl.item(j);
        	node.setTextContent(xPathReplacementValue);
          }
		}
		
		// pass modified XML to output stream
		DOMSource domSource = new DOMSource(doc);
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.transform(domSource, new StreamResult(baos));
		payload.setContent(baos.toByteArray());
	  } catch (ParserConfigurationException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (SAXException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (IOException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (XPathExpressionException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (TransformerConfigurationException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (TransformerFactoryConfigurationError e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (TransformerException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  } catch (InvalidParamException e1) {
		  audit.addAuditLogEntry(amk, AuditLogStatus.ERROR, e1.getMessage());
		  throw new ModuleException(e1);
	  }
	
	  // Pass updated module data back
	  return md;
	}
	
	// Parse String to determine if value is passed directly or a formatting function is applied
	private String parse(String value)
	{
		// Adjust for recursion so methods can be passed as parameters to other methods
		String comma = ",", openParen = "(", closeParen = ")";
		Object[] paramsInput;
		int index = value.lastIndexOf(openParen);
		try {
		  // Check for method call - if no method then return value back directly
		  if(index == -1)
		    return value;
		    
		  // Invoke method using reflection to retrieve value for replacement
		  // Find closest comma or open parentheses so method name can be determined
		  // appropriately
		  int closeParenIndex = value.substring(index).indexOf(closeParen) + index;
		  int startIndex = value.substring(0, index).lastIndexOf(comma);
		  if(startIndex == -1)
		    startIndex = value.substring(0, index).lastIndexOf(openParen);
		  String paramString = value.substring(index + 1, closeParenIndex);
		  paramsInput = paramString.split(comma);
		  Class[] params = new Class[paramsInput.length];
		  for(int i = 0; i < params.length; i++)
			params[i] = String.class;

		  // Handle parsing of inner most method call each time using recursion
		  Method theMethod = this.getClass().getDeclaredMethod(value.substring(startIndex + 1, index), params);
		  String tmpVal = (String) theMethod.invoke(this, paramsInput);
		  return parse(value.substring(0, startIndex + 1) + tmpVal + value.substring(closeParenIndex + 1));
      } catch (NoSuchMethodException e) {
    	audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, 
    			"Using direct value because of following exception" + e.getMessage());
		return value;
	  } catch (IllegalArgumentException e) {
	    audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, 
	    		"Using direct value because of following exception" + e.getMessage());
	    return value;
	  } catch (IllegalAccessException e) {
		audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, 
		   		"Using direct value because of following exception" + e.getMessage());
		return value;
	  } catch (InvocationTargetException e) {
		audit.addAuditLogEntry(amk, AuditLogStatus.SUCCESS, 
		   		"Using direct value because of following exception" + e.getMessage());
		return value;
	  }
	}
	
	// Add trailing spaces to value based on number specified
	private String padTrailingSpaces(String value, String max)
	{
	  int pad = Integer.parseInt(max) - value.length();
	  for(int i = 0; i < pad; i++)
	  {
	    value += " ";
	  }
	  return value;
	}
	
	// Lookup value of dynamic configuration property
	private String getDynamicConfigurationProperty(String nameSpace, String propertyName)
	{
	  return msg.getMessageProperty(new MessagePropertyKey(propertyName, nameSpace));
	}

	@Override
	public void ejbActivate() throws EJBException, RemoteException {
	}

	@Override
	public void ejbPassivate() throws EJBException, RemoteException {
	}

	@Override
	public void ejbRemove() throws EJBException, RemoteException {
	}

	@Override
	public void setSessionContext(SessionContext arg0) throws EJBException, 
			RemoteException{
	}
}

 

Notes & Final Thoughts:

  • If you have ever used the SeeBurger adapter modules before you will notice I built it with the intention that new functions could be added as required since it uses reflection.
  • For the purposes of simplicity in the use of reflection I have set it up to expect only String types for parameters and return values with any type casting handled internally within the methods.
  • I wanted to share this alternative in the event that it may help others when they are facing a situation where there hands may be tied and it would help to avoid creating additional flows and other workaround solutions – specifically when you require something at the adapter level and it is something that cannot wait until receiver determination or within the mapping runtime.
To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply