Tuesday, May 7, 2013

The XML Logger

In my last post, I promised to try to bring us up to date on the current implementation of the logging framework, and specifically the XML Logger component of it.  The framework is initialized on the Application Start event making all these methods available from script.  From an XML Logging perspective, it can be initiated in one of two ways:

  • As a standard business service from within the Integration WF (or business service).  In this approach, just before the outbound WS or HTTP call, you would call the logRequest or logResponse methods of the framework business service, passing in at a minimum the property set about to be interfaced.  There are many other attributes of the payload record which can be optionally used which I won't go into detail over.  You can always add attributes to meet your needs and you don't need to populate any of them really.
  • As a Filter Service.  This is used for Web Services and is useful in that it can be turned on or off without modifying any existing Integration WFs.  On the Web Service admin views, for each web service operation that you want to log, just specify the Request/Response Filter Service as the Framework business service and the Request/Response Filter Method as logRequest/logResponse respectively.
  • Can be implemented to capture other payloads as needed, for instance before and after messages of the EAI Data Transformation Service
Ok, now for the nitty gritty.  What do the logRequest/logResponse methods do?  Both are similar and different only in that all interface logging records have a placeholder for a request and a response payload, which the two methods are differentiated to populate.  The main input to these methods is an XML payload.  At a high level, here is the algorithm:
  1. Navigate the property set until the first 'ListOf*' tag is found which is assumed to be the beginning of the Integration Object data.
  2. Call a method to Parse the remaining child data to correctly name and categorize the interface based on the IO Type and iddentify the record id and unique identifier attributes.  This allows for optional scripting to tailor the logging service to your client's unique needs
  3. Call the logInterface method which:
    1. Checks if in an EAI Transaction.  If so, add the payload to an array, otherwise continue (This is currently only implemented to support outbound interfaces when using the Filter service implementation)
    2. Creates a session record if one does not already exist (Inbound interfaces executed by an EAI OM typically)
    3. Deal with anonymous logins (when used on an inbound interface the request method will be executed under the Anonymous login but the response method with be performed by the interface user id)
    4. Creates a payload record to store the attributes extracted from the payload parsing
    5. Split the payload into chunks no larger than the BLOB length and create detail records for each chunk
First the PreInvoke method.  This mostly speaks for itself but since we may want to save processing overhead the calling of the parseInterface method is parameterized and controlled by which method is actually invoked.

function Service_PreInvokeMethod (MethodName, Inputs, Outputs){
  var retValue      = CancelOperation;

  switch(MethodName) {
  case "logInterface":     
    var key = logInterface(Inputs, Outputs);
    Outputs.SetProperty("key", key)
    break;
  case "logParseRequest":     
    logRequest(Inputs, Outputs, true, "");
    break;
  case "logParseResponse":     
    logResponse(Inputs, Outputs, true, "");
    break;
  case "logRequest":     
    logRequest(Inputs, Outputs, false, "");
    break;
  case "logResponse":     
    logResponse(Inputs, Outputs, false, "");
    break;
  case "logTransformRequest":     
    logRequest(Inputs, Outputs, false, "Transform");
    break;
  case "logTransformResponse":     
    logResponse(Inputs, Outputs, false, "Transform");
    break;
  }
  return (retValue);
}
Next the logRequest and logResponse Methods.  Like I said they are very similar except for which field the payload is passed to.  Also the logResponse method has some additional logic for parsing SOAP faults.

function logRequest(Inputs, Outputs, parse, mode) {
/* ***********************************
Purpose: Log interface request from/to an external interface in the session log
Usage: In Web Service definition, operations applet, set the Request Filter BS to 'PPT Utilities'
  and the method to logRequest.  Clear the cache
Arguments: 1 - SoapMessage will implicily be passed as a child property set
**************************************** */
try {
  var soapEnv, soapBody, divePS, direction, progress;
  var bodyType="";
  var msgType="";
  var parseResults = new Object();
  var key = TimeStamp("DateTimeMilli");
  var max = 3;
  var dives = 0;

  if(Inputs.GetChildCount() > 0) {
    // Get the SOAP envelope from the SOAP hierarchy.  If payload is passed as an input property set, skip down an extra level
    soapEnv = Inputs.GetChild(0);        //Like env:Envelope

    //Minimize processing if payloads/logging information will not be stored
    if (gHoldBufferDump == true || gsTraceIntfaceReqResp == "TRUE") {
      //if called from EAI Data Transformation Engine and user logging level is 5 capture passing specific props
      if (mode=="Transform" && (gHoldBufferDump == true || gCurrentLogLvl >= 5)) {
        parseResults.recId = soapEnv.GetChild(0).GetChild(0).GetProperty("Id");
        parseResults.recBC = soapEnv.GetChild(0).GetChild(0).GetType();
        msgType = Inputs.GetProperty("MapName");
        parseResults.funcName = Inputs.GetProperty("MapName");
        direction = "EAI Transform";
      } else {
        try { // Try to process the message to get functional data 
          if (soapEnv.GetType().toUpperCase().indexOf("ENVELOPE") <0 ) soapEnv = soapEnv.GetChild(0);
          direction = Inputs.GetProperty("WebServiceType")+" "+Inputs.GetProperty("Direction");
          for (var i=0; i < soapEnv.GetChildCount(); i++) {   
            bodyType = soapEnv.GetChild(i).GetType();    //Like env:Body
            if (bodyType.toUpperCase() == "BODY" || bodyType.substr(bodyType.indexOf(":")+1).toUpperCase() == "BODY") {
              soapBody = soapEnv.GetChild(i);
              for (var j=0; j < soapBody.GetChildCount(); j++) {   
                msgType = soapBody.GetChild(j).GetType();  //Full Port name of the WS 
  
                //Parse to check for faults and create a text string to be used in the key
                if (msgType.indexOf(":") >= 0)  msgType = msgType.substr(msgType.indexOf(":")+1); //strip namespace
                if (msgType.indexOf(" ") >= 0)  msgType = msgType.substr(0, msgType.indexOf(" ")); //strip namespace declaration
                if (msgType.indexOf("_") >= 0)  msgType = msgType.substr(0, msgType.lastIndexOf("_"));//strip port operation
  
                //if true, attempt to find Row Id in payload to stamp on log record so log can be linked to Siebel record  
                if (parse == true) {
                  divePS = soapBody.GetChild(j); //.GetChild(0)
                  while (divePS.GetType().indexOf("ListOf") < 0 && dives <= max) {
                    if (divePS.GetChildCount() > 0) {
                      divePS = divePS.GetChild(0);
                      dives++;
                    } else dives = max + 1;
                  }
   
                  //If a ListOf... container is found, this is a SiebelMessage generated by Siebel. Otherwise parse the SOAP Body
                  if (divePS.GetType().indexOf("ListOf") >= 0) parseInterface(divePS, parseResults);
                  else parseInterface(soapBody, parseResults);
                }
              } 
        
              break;
            }
   }         
 } catch(e) {
          //If an error occurs while parsing, just try to write the message whole
        }
            
      } //SOAP Message scenario

      //If msgType is identified then insert a log for this payload
      if (msgType != "") {
        msgType = msgType.replace(/_spc/g, "").replace(/\s/g, "");
        key = msgType+"_"+key;

        TheApplication().SetProfileAttr("InterfaceKeyInbound", key);
        progress = logInterface(key, soapEnv, null, direction, parseResults.recId, parseResults.recBC, "Pending", msgType, parseResults.funcName, null, null, null, parseResults.ref1, null, null, parseResults.refField);
      }
    } else if (gsTraceIntfaceReqResp == "FALSE") {
      //Do Nothing
    } else { //Store payloads in case an error occurs
      //var holdPayload = ["Request", Inputs, parse, mode];
      gHoldReqResp.push(["Request", Inputs, parse, mode]);
      if (gHoldBufferMax > 0 && gHoldReqResp.length > gHoldBufferMax) gHoldReqResp.shift();
    }
  } // Inputs.GetChildCount()
  Outputs.InsertChildAt(soapEnv,0);
} catch(e) {
  RaiseError(e);
} finally {
  divePS = null;
  soapBody = null;
  soapEnv = null;
  parseResults =  null;
}

}

function logResponse(Inputs, Outputs, parse, mode) {
/* ***********************************
Purpose: Log interface response from/to an external interface in the session log
Usage: In Web Service definition, operations applet, set the Response Filter BS to 'PPT Utilities'
  and the method to logResponse.  Clear the cache
Arguments: 1 - SoapMessage will implicily be passed as a child property set
**************************************** */
try {
  var soapEnv, soapBody, divePS, direction, progress;
  var bodyType="";
  var msgType="";
  var parseResults = new Object();
  var fault=null;
  var key = TheApplication().GetProfileAttr("InterfaceKeyInbound");
  var max = 3;
  var dives = 0;
  var dump = false;

  if(Inputs.GetChildCount() > 0) {
    // Get the SOAP envelope from the SOAP hierarchy
    soapEnv = Inputs.GetChild(0);

    //Minimize processing if payloads/logging information will not be stored
    if (gHoldBufferDump == true || gsTraceIntfaceReqResp == "TRUE") {
      if (mode=="Transform" && (gHoldBufferDump == true || gCurrentLogLvl >= 5)) {
        dump = true;
        direction = "EAI Transform";
        if (soapEnv.GetChild(0).GetChild(0).PropertyExists("Id")) {
          parseResults.recId = soapEnv.GetChild(0).GetChild(0).GetProperty("Id");
          parseResults.recBC = soapEnv.GetChild(0).GetChild(0).GetType();
        }
      } else if (mode=="") {
        dump = true;
        try { // Try to process the message to get functional data
          direction = Inputs.GetProperty("WebServiceType")+" "+Inputs.GetProperty("Direction");
          //Soap Envelope Request may have a Header and a Body so loop to the Body
          for (var i=0; i < soapEnv.GetChildCount(); i++) {
            bodyType = soapEnv.GetChild(i).GetType();
            if (bodyType.toUpperCase() == "BODY" || bodyType.substr(bodyType.indexOf(":")+1).toUpperCase() == "BODY") {
              soapBody = soapEnv.GetChild(i);
  
              //Soap Body typically has a container for the Message
              for (var j=0; j < soapBody.GetChildCount(); j++) {   
                msgType = soapBody.GetChild(j).GetType();
   
                //Parse to check for faults and create a text string to be used in the key
                if (msgType.indexOf(":") >= 0)  msgType = msgType.substr(msgType.indexOf(":")+1)
                if (msgType.indexOf(" ") >= 0)  msgType = msgType.substr(0, msgType.indexOf(" "))
                if (msgType.indexOf("_") >= 0)  msgType = msgType.substr(0, msgType.lastIndexOf("_"))
  
                if (msgType.toUpperCase() == "FAULT") fault = soapBody.GetChild(j).GetProperty("faultstring");
                else if (parse == true) { 
                  //if true, attempt to find Row Id in payload to stamp on log record so log can be linked to Siebel record  
                  divePS = soapBody.GetChild(j); //focus on the Message level
                  while (divePS.GetType().indexOf("ListOf") < 0 && dives <= max) {
                    if (divePS.GetChildCount() > 0) {
                      divePS = divePS.GetChild(0);
                      dives++;
                    } else dives = max + 1;
                  }
   
                  //If a ListOf... container is found, this is a SiebelMessage generated by Siebel. Otherwise parse the child of the Body
                  if (divePS.GetType().indexOf("ListOf") >= 0) parseInterface(divePS, parseResults);
                  else parseInterface(soapBody, parseResults);
                }
              }
              break;
            }
          }
        } catch(e) {
          //If an error occurs while parsing, just try to write the message whole
        }
      }
 
      if (key == "") {
        key = TimeStamp("DateTimeMilli");
        if (msgType != "") {
          msgType = msgType.replace(/_spc/g, "");
          key = msgType+"_"+key;
        }
      }
         
      if (dump == true) {
        if (fault != null) {
          logInterface(key, null, soapEnv, null, parseResults.recId, parseResults.recBC, "error", null, null, fault);
        } else {
          var recId = (parseResults.recId != "" ? parseResults.recId : null);
          var recBC = (parseResults.recBC != "" ? parseResults.recBC : null);
          var ref1 = (parseResults.ref1 != "" ? parseResults.ref1 : null);
          var funcName = (parseResults.funcName != "" ? parseResults.funcName : null);
          progress = logInterface(key, null, soapEnv, direction, recId, recBC, "Complete", msgType, funcName, null, null, null, ref1);
        }
      }
    } else if (gsTraceIntfaceReqResp == "FALSE") {
      //Do Nothing
    } else { //Store payloads in case an error occurs
      gHoldReqResp.push(["Response", Inputs, parse, mode]);
      if (gHoldBufferMax > 0 && gHoldReqResp.length > gHoldBufferMax) gHoldReqResp.shift();
    }
  } // Inputs.GetChildCount()
  Outputs.InsertChildAt(soapEnv,0);
} catch(e) {
  RaiseError(e);
} finally {
  divePS = null;
  soapBody = null;
  soapEnv = null;
  parseResults =  null;
  TheApplication().SetProfileAttr("InterfaceKeyInbound", "");
}
}

The parseInterface method passes the ListOf container of the Integration Object to a switch statement where each BC type can be evaluated.  This is useful only when using the Filter Service triggering mechanism otherwise the Record Id and Interface Name attributes can just be explicitly passed as parameters.  A case section should be created for each Integration Object being processed. This function really needs to be manually manipulated for every implementation and integration point to explicitly specify how to find the record id for a particular integration.

function parseInterface(ListOfIC, oReturn) {    //Input ListOfIC is a ListOf...
//Called from logRequest and logResponse to parse a message and get the row id or reference ids of different
//objects
try {
  if (ListOfIC.GetChildCount() > 0) {
    var IC = ListOfIC.GetChild(0);   //Integration Component Instance
    var icNameSpace = "";
    var intCompType = IC.GetType();
    var propName;
    var stop = false;
    var childIC;
    var childFlds;
      
    if (intCompType.indexOf(":") >= 0) {
      icNameSpace = intCompType.substr(0, intCompType.indexOf(":")+1);
      intCompType = intCompType.substr(intCompType.indexOf(":")+1);
    }

    //For these types, dive an additional level
    switch(intCompType) {
    case "ATPCheckInterfaceRequestOrders":
      IC = IC.GetChild(0);
      intCompType = IC.GetType();
      if (intCompType.indexOf(":") >= 0) {
        icNameSpace = intCompType.substr(0, intCompType.indexOf(":")+1);
        intCompType = intCompType.substr(intCompType.indexOf(":")+1);
      }
      break;
    }
 
    for (var flds = 0; flds < IC.GetChildCount(); flds++) { //Loop through Fields
      propName = IC.GetChild(flds).GetType();
      switch (intCompType) {
      case "Quote":
      case "SWIQuote":
        oReturn.recBC = "Quote";
        if (propName == icNameSpace+"Id") {
          oReturn.recId = IC.GetChild(flds).GetValue();
          stop = true;
        }
        break;

      case "ProductIntegration":
        oReturn.recBC = "Internal Product";
        if (propName == icNameSpace+"ListOfProductDefinition") {
          childIC = IC.GetChild(flds).GetChild(0);
          for (childFlds = 0; childFlds < childIC.GetChildCount(); childFlds++) { //Loop through Fields
            propName = childIC.GetChild(childFlds).GetType();
            if (propName == icNameSpace+"Id" || propName == icNameSpace+"ProductId") {
              oReturn.recId = childIC.GetChild(childFlds).GetValue();
              stop = true;
            }
            if (stop) break;                              
          }
        }
        break;
     
      default:
        if (propName.indexOf("Id") >= 0) {
          oReturn.recId = IC.GetChild(flds).GetValue();
          stop = true;
        }
        oReturn.recBC = intCompType;
        stop = true;
      }
      if (stop) break;                              
    }
  }
} catch(e) {
  RaiseError(e);
} finally {
  childIC = null;
  IC = null;
}
}

The logInterface method is called by both logRequest and logResponse to manage the session records for inbound interfaces and create the actual payload record.  I will have to go into more detail about the Anonymous login processing at some other time.  Suffice to say this works under a variety of web service setups.

function logInterface() {
if (gHoldBufferDump == true || gsTraceIntfaceReqResp == "TRUE") {
  try {
    var key, dir, recId, recBC, status, srcObj, name, logText, userText, retCode, ref1, ref2, ref3, refField, findResponseMethod;
    var request:PropertySet, response:PropertySet;
    var progress = "";

    //if attributes passed as an input property set, set variables from them
    if (typeof(arguments[0]) == "object") {
      progress = progress+"\n"+"REF1: typeof(arguments[0]) == object";
      var Inputs:PropertySet = arguments[0];
      name = Inputs.GetProperty("FunctionalName");
      key = Inputs.GetProperty("Key");
   
      for (var i=0;i < Inputs.GetChildCount();i++) {
        if (Inputs.GetChild(i).GetType() == "Request") request = Inputs.GetChild(0);
        if (Inputs.GetChild(i).GetType() == "Response") response = Inputs.GetChild(0);
      }
      dir = Inputs.GetProperty("Direction");
      recId = Inputs.GetProperty("LinkId");
      recBC = Inputs.GetProperty("LinkBC");
      status = Inputs.GetProperty("Status");
      srcObj = Inputs.GetProperty("SourceObject");
      logText = Inputs.GetProperty("LogText");
      userText = Inputs.GetProperty("UserText");
      retCode = Inputs.GetProperty("ReturnCode");
      ref1 = Inputs.GetProperty("Reference1");
      ref2 = Inputs.GetProperty("Reference2");
      ref3 = Inputs.GetProperty("Reference3");
      refField = Inputs.GetProperty("RefField");
    } else {
      progress = progress+"\n"+"REF1: else typeof(arguments[0])";
      key = arguments[0];
      request = arguments[1];
      response = arguments[2];
      dir = arguments[3];
      recId = arguments[4];
      recBC = arguments[5];
      status = arguments[6];
      srcObj = arguments[7];
      name = arguments[8];
      logText = arguments[9];
      userText = arguments[10];
      retCode = arguments[11];
      ref1 = arguments[12];
      ref2 = arguments[13];
      ref3 = arguments[14];
      refField = arguments[15];
    }
  
    //When called though WF as Payload logger, generate the key if not provided
    if (key == "" || key == undefined || key == null) {
      if (name == "" || name == undefined || name == null) name = "None";
      key = name.replace(/_spc/g, "").replace(/\s/g, "")+TimeStamp();
    }

    var found:Boolean = false;
    var sessionId:String = "";
    var guestSessionId:String = "";
    var guestSessionFound = false;
    var createSession = false;
    var firstMessage = false;
    var boSessionFlat;
    var bcSessionXMLFlat;
    var useGuestSession = (response != null && gsMergeGuestSessions=="TRUE" && gsGuest != "" ? true : false);
    var boSession:BusObject = TheApplication().GetBusObject("PPT User Session");
    var bcSession:BusComp;
 
    if (gHoldBuffer == true) {
      progress = progress+"\n"+"REF2: gHoldBuffer == true";
      //If in a EAI Txn, store all payloads in an array so they can be written after the commit or rollback
      gHoldPayloads[gHoldPayloads.length] = arguments;
    } else {
      progress = progress+"\n"+"REF2: gsTraceIntfaceReqResp == TRUE & key == "+key;
      //If an interface is being logged for the first time, need to instantiate the session
      if (gSessionId == "") {
        progress = progress+"\n"+"REF3: gSessionId == ''";
        //If Guest connections are not used, a simplified session management can be used 
        if (gsGuest == "" && key != "") {
          progress = progress+"\n"+"REF4: gsGuest == '' & key == "+key;
          bcSession = boSession.GetBusComp("PPT User Session");
          with (bcSession) {
            NewRecord(NewBefore);
            SetFieldValue("Employee Id",TheApplication().LoginId());
            SetFieldValue("Session Stamp",gsLogSession);
            WriteRecord();
            gSessionId = GetFieldValue("Id");
          }
          createSession = false; //skip logic below to create/merge a guest session
          firstMessage = true; //will always insert the input message rather than querying to update
          //Reset the variable to check whether to merge the guest session.  This allows a log buffer dump
          gsMergeGuestSessions = "FALSE";
        } else {
          progress = progress+"\n"+"REF4: else: gsGuest == '' & key == "+key;
          createSession = true;
  
          //confirm that current session has not been created yet
          bcSession = boSession.GetBusComp("PPT User Session");
          with (bcSession) {
            ClearToQuery();
            SetSearchSpec("Session Stamp", gsLogSession);
            ExecuteQuery(ForwardOnly);
            found = FirstRecord();
 
            if (found == true) {
              gSessionId = GetFieldValue("Id");
              createSession = false;
            } else {
              firstMessage = true;
            }
          }
        }
      }
 
      if (createSession == true || useGuestSession == true) {
        progress = progress+"\n"+"REF5: createSession == true || useGuestSession == true";
        bcSession = boSession.GetBusComp("PPT User Session");
 
        //Because EAI logins can trigger logging from the anonymous login, the response logging will trigger
        //from a different session. Query for the most recent corresponding request log and update it
        if (useGuestSession) {
          progress = progress+"\n"+"REF6: useGuestSession";
          boSessionFlat = TheApplication().GetBusObject("PPT User Session Flat");
          bcSessionXMLFlat = boSessionFlat.GetBusComp("PPT User Session XML");
          bcSessionXMLFlat.ActivateField("Parent Id");
          bcSessionXMLFlat.ClearToQuery();
          if (typeof(srcObj) != "undefined" && srcObj != null) bcSessionXMLFlat.SetSearchSpec("Source Object", srcObj);
          if (typeof(ref1) != "undefined" && ref1 != null) bcSessionXMLFlat.SetSearchSpec("Reference Id 1", ref1);
          bcSessionXMLFlat.SetSearchSpec("Response", "IS NULL");
          bcSessionXMLFlat.SetSearchSpec("Employee Login", gsGuest);
          bcSessionXMLFlat.SetSortSpec("Created (DESC)");
          bcSessionXMLFlat.ExecuteQuery(ForwardBackward);
          guestSessionFound = bcSessionXMLFlat.FirstRecord();
    
          if (guestSessionFound == true) guestSessionId = bcSessionXMLFlat.GetFieldValue("Parent Id");
        }
    
        if (guestSessionFound == false && createSession == true) {
          progress = progress+"\n"+"REF7: guestSessionFound == false & createSession == true";
          //Anonymous login session not found and there is no current session.  Create a new one
          with (bcSession) {
            NewRecord(NewBefore);
            SetFieldValue("Employee Id",TheApplication().LoginId());
            SetFieldValue("Session Stamp",gsLogSession);
            WriteRecord();
            gSessionId = GetFieldValue("Id");
          }
        } else if (guestSessionFound == true && createSession == false) {
          progress = progress+"\n"+"REF7: guestSessionFound == true & createSession == false";
          //Anonymous login session found and there is a current session. 
          //Link child records to the parent session for the Interface User and delete the guest session (faster than a merge)
          while (guestSessionFound) {
            bcSessionXMLFlat.SetFieldValue("Parent Id",gSessionId);
            bcSessionXMLFlat.WriteRecord();
            guestSessionFound = bcSessionXMLFlat.NextRecord();
          }
          with (bcSession) {
            ClearToQuery();
            SetSearchSpec("Id", guestSessionId);
            SetSortSpec("");
            ExecuteQuery(ForwardOnly);
            guestSessionFound = FirstRecord();
     
            if (guestSessionFound == true) DeleteRecord();
            ClearToQuery();
            SetSortSpec("");
            SetSearchSpec("Id", gSessionId);
            ExecuteQuery(ForwardOnly);
          }
        } else if (guestSessionFound == true && createSession == true) {
          progress = progress+"\n"+"REF7: guestSessionFound == true & createSession == true";
          //Anonymous login session found and there is no current session.  Update the guest session to EAI values
          with (bcSession) {
            ActivateField("Employee Id");
            ActivateField("Session Stamp");
            ClearToQuery();
            SetSearchSpec("Id", guestSessionId);
            SetSortSpec("");
            ExecuteQuery(ForwardBackward);
            found = FirstRecord();
     
            if (found == true) {
              SetFieldValue("Employee Id",TheApplication().LoginId());
              SetFieldValue("Session Stamp",gsLogSession);
              WriteRecord();
            }
          }
        } else {
          progress = progress+"\n"+"REF7: Anonymous login session not found and there is a current session.  Do Nothing";
          //Anonymous login session not found and there is a current session.  Do Nothing
        }
 
        //Reset the variable to check whether to merge the guest session.  This allows a log buffer dump
        gsMergeGuestSessions = "FALSE";
      }
      var bcSessionXML = boSession.GetBusComp("PPT User Session XML");
      var bcSessionXMLDtl = boSession.GetBusComp("PPT User Session XML Detail");
 
      with (bcSessionXML) {
        if (firstMessage) {
          progress = progress+"\n"+"REF8: firstMessage";
          //This is an insert so no query needed to update an existing record
          found = false;
          findResponseMethod = "Do Not Search";
        } else if (useGuestSession && guestSessionFound) {
          progress = progress+"\n"+"REF8: useGuestSession & guestSessionFound";
          //If this is the first update after a guest session is used, the key will not match but there should only be one XML record
          ClearToQuery();
          SetSearchSpec("Created By Login", gsGuest);
          SetSearchSpec("Parent Id", gSessionId);
          SetSearchSpec("Request", "IS NOT NULL");
          SetSearchSpec("Response", "IS NULL");
          ExecuteQuery(ForwardBackward);
          found = FirstRecord();
          findResponseMethod = "Session/Guest Login: "+gSessionId+"/"+gsGuest;
        } else if ((typeof(response) != "undefined" && response != null) ||
          (typeof(ref1) != "undefined" && ref1 != null && typeof(request) != "undefined" && request != null && gsReplaceSyncResponseWAsyncRequest == "TRUE")) {
          progress = progress+"\n"+"REF8: normal response update OR inbound request to a previously sent asynchronous request";
          //This is a normal response update to an existing message record or 
          //it is an inbound request to a previously sent asynchronous request with a matching Reference Id AND we want to log the Async request as the response
          ClearToQuery();
          SetSearchSpec("Parent Id", gSessionId);
 
          //If this is an Inbound request and Ref1 is provided, lookup by Ref1 
          if (typeof(ref1) != "undefined" && ref1 != null && typeof(request) != "undefined" && request != null) {
            SetSearchSpec("Reference Id 1", ref1.substring(0, 100));
            findResponseMethod = "Reference 1: "+ref1.substring(0, 100);
          } else {
            SetSearchSpec("Name", key);
            findResponseMethod = "Key: "+key;
          }
          SetSortSpec("");
          ExecuteQuery(ForwardBackward);
          found = FirstRecord();
        }
    
        //This is a normal request or an existing record could not be found for some reason
        if (found == false) {
          progress = progress+"\n"+"REF9: This is a normal request or an existing record could not be found for some reason ("+findResponseMethod+")";
          NewRecord(NewBefore);
          SetFieldValue("Name",key);
          SetFieldValue("Parent Id",gSessionId);
        }
    
        if (useGuestSession == true) {
          progress = progress+"\n"+"REF10: Set Name Key: "+key.substring(0, 100);
          SetFieldValue("Name", key.substring(0, 100));
        }
 
        //Since payloads can be any size, very large ones greater than CLOB size 131k need to be split with the text being 
        //put into multiple records
        if (typeof(request) != "undefined" && request != null) {
          progress = progress+"\n"+"REF11: Set Request";
          SetFieldValue("Request Time", TimeStamp("DateTimeFormatted"));
          splitPayload(bcSessionXML, bcSessionXMLDtl, "Request", request, 131072)
        }
        if (typeof(response) != "undefined" && response != null) {
          progress = progress+"\n"+"REF12: Set Response";
          SetFieldValue("Response Time", TimeStamp("DateTimeFormatted"));
          splitPayload(bcSessionXML, bcSessionXMLDtl, "Response", response, 131072)
        }
  
        if (typeof(dir) != "undefined" && dir != null && dir != "")   SetFieldValue("Direction", dir.substring(0, 30));
        if (typeof(recId) != "undefined" && recId != null && recId != "") SetFieldValue("Record Id", recId.substring(0, 15));
        if (typeof(recBC) != "undefined" && recBC != null && recBC != "") SetFieldValue("Record BC", recBC.substring(0, 50));
        if (typeof(status) != "undefined" && status != null && status !="") SetFieldValue("Status", status.substring(0, 30));
        if (typeof(srcObj)!="undefined" && srcObj != null && srcObj !="") SetFieldValue("Source Object", srcObj.substring(0, 50));
        if (typeof(name) != "undefined" && name != null && name != "")  SetFieldValue("Functional Name", name.substring(0, 50));
        if (typeof(logText)!= "undefined" && logText != null && logText !="") SetFieldValue("Log Text", logText.substring(0, 131072));
        if (typeof(userText)!= "undefined" && userText != null && userText !="") SetFieldValue("User Text", userText.substring(0, 255));
        if (typeof(retCode) != "undefined" && retCode != null && retCode != "")  SetFieldValue("Return Code", retCode.substring(0, 30));
        if (typeof(refField) != "undefined" && refField != null && refField!="") {
          SetFieldValue(refField, ref1);
          SetFieldValue("Reference Id 2", refField);
        } else {
          if (typeof(ref1) != "undefined" && ref1 != null)   SetFieldValue("Reference Id 1", ref1.substring(0, 100));
          if (typeof(ref2) != "undefined" && ref2 != null)   SetFieldValue("Reference Id 2", ref2.substring(0, 100));
        }
        if (typeof(ref3) != "undefined" && ref3 != null)   SetFieldValue("Reference Id 3", ref3.substring(0, 100));
        progress = progress+"\n"+"REF13: PreWrite";
        WriteRecord();
      }
      if (gsLogMode == "FILE") {
        progress = progress+"\n"+"REF14: gsLogMode == FILE";
        if (typeof(request) != "undefined" && request != null)  PropSetToFile(key+"_Request", request);
        if (typeof(response) != "undefined" && response != null) PropSetToFile(key+"_Response", response);
      }
    }
  } catch(e) {
    RaiseError(e, progress);
  } finally {
    request = null;
    response = null;
    Inputs = null;
    bcSessionXML = null;
    bcSessionXMLFlat = null;
    boSessionFlat = null;
    bcSession = null;
    boSession = null;
    TheApplication().SetProfileAttr("C1 Session Id", gSessionId);
  }
  return(progress);
}
}

The trimPS method is actually deprecated in my service but I include it in case it is useful to anyone.  I basically just takes a property set, converts it to text, then sets a specified field on an instantiated BC passed as an input to the converted text of the property set.  The assumption in using this method is that there is a limit to how large the stored payload can be.

function trimPS(ps, fieldName, bc, maxLength){
try {
  var psIn      = TheApplication().NewPropertySet();
  var psOut     = TheApplication().NewPropertySet();
  var bsService = TheApplication().GetService ("XML Converter (Data in Child PropSet)");
  psIn.SetProperty("EscapeNames", "False");
  var text:String = "";

  psIn.AddChild(ps);
  bsService.InvokeMethod("PropSetToXML", psIn, psOut);
  bc.SetFieldValue(fieldName, ToString(psOut).substring(0, maxLength));
  bc.SetFieldValue(fieldName+" Time", TimeStamp("DateTimeFormatted"));
  text = bc.GetFieldValue(fieldName);
  text = text.lTrim();
  if (text.substring(0, 14) == "PropertySet [ ") text = text.substring(14);
  text = text.rTrim("]");
  bc.SetFieldValue(fieldName,text);
} catch (e) {
  throw(e);
} finally { 
  psOut = null;
  psIn = null;
  bsService = null;
}
}

The splitPayload method is what replace the trimPS method.  It is no longer limited in being able store only a certain character length of payload as this method splits the payload into chunks of a specified size and inserts records into the instantiated BC passed as an input.

function splitPayload(parentbc, detailbc, fieldName, ps, maxLength) {
try {
  var psIn      = TheApplication().NewPropertySet();
  var psOut     = TheApplication().NewPropertySet();
  var bsService = TheApplication().GetService ("XML Converter (Data in Child PropSet)");
  psIn.SetProperty("EscapeNames", "False");
  var text:String = "";
  var stripPS = false;
  var textPS = "";
  
  psIn.AddChild(ps);
  bsService.InvokeMethod("PropSetToXML", psIn, psOut);
  textPS = ToString(psOut);

  if (textPS.length > maxLength) {
    while (textPS.length > 0) {
      detailbc.NewRecord(NewAfter);
      detailbc.SetFieldValue("Field", fieldName);
      detailbc.SetFieldValue("Log Text", textPS.substring(0, maxLength));
 
      //Service adds a Prefix that needs to be removed
      text = detailbc.GetFieldValue("Log Text");
      text = text.lTrim();
      if (text.substring(0, 14) == "PropertySet [ ") {
        stripPS = true;
        text = text.substring(14);
      }
 
      textPS = textPS.substring(maxLength, textPS.length); 
 
      //If the Text is broken up across multiple records, need to remove the trailing ] from the last record
      if (stripPS && textPS.length < maxLength) {
        text = text.rTrim("]");
      }
      detailbc.SetFieldValue("Log Text",text);
      detailbc.WriteRecord();
    }
  } else {
    parentbc.SetFieldValue(fieldName, textPS);
    text = parentbc.GetFieldValue(fieldName);
    text = text.lTrim();
    if (text.substring(0, 14) == "PropertySet [ ") {
      stripPS = true;
      text = text.substring(14).rTrim("]");
      parentbc.SetFieldValue(fieldName, text);
    }
//  parentbc.WriteRecord();
  }
} catch (e) {
  RaiseError(e);
} finally { 
  psOut = null;
  psIn = null;
  bsService = null;
}
}

Next: Viewing the Payload

No comments:

Post a Comment