Wednesday, February 10, 2010

A Simple(rudimentary) implementation of XPath node search (SelectSingleNode) for OpenEdge 10.2A

- DOES NOT implement Node value filtering (yet)
- Implements Positional referencing/filtering
- DOES NOT implement Attribute searching (yet)
- DOES NOT implement Multiple node result selection like SelectNodes (new function coming)

NOTE: This is a work in progress.

NOTE: You will need to declare the following global variable:
DEFINE VARIABLE recurseXPath AS LOGICAL NO-UNDO.

FUNCTION SelectSingleNode RETURNS LOGICAL (INPUT hXBaseNode AS HANDLE,INPUT xPath AS CHARACTER, OUTPUT hXNode AS HANDLE):
DEFINE VARIABLE hXCurrentNode AS HANDLE.
DEFINE VARIABLE hXChildNode AS HANDLE.
DEFINE VARIABLE childCount AS INTEGER.
DEFINE VARIABLE currentPosition AS INTEGER.
DEFINE VARIABLE startPosition AS INTEGER.
DEFINE VARIABLE endPosition AS INTEGER.
DEFINE VARIABLE iterationDone AS LOGICAL.
DEFINE VARIABLE nodeName AS CHARACTER.
DEFINE VARIABLE pathSegment AS CHARACTER.
DEFINE VARIABLE nodeFilter AS CHARACTER.
DEFINE VARIABLE fqNodeName AS CHARACTER.
DEFINE VARIABLE subPath AS CHARACTER.

CREATE X-NODEREF hXCurrentNode.
CREATE X-NODEREF hXChildNode.

IF (SUBSTRING(xPath, 1, 2) = "//") THEN DO:
recurseXPath = TRUE.
subPath = SUBSTRING(xPath, 3).
hXCurrentNode = hXBaseNode:OWNER-DOCUMENT.
END.
ELSE IF (SUBSTRING(xPath,1,2) = "..") THEN DO:
subPath = SUBSTRING(xPath, 3).
hxBaseNode:GET-PARENT(hXCurrentNode).
END.
ELSE IF (SUBSTRING(xPath,1,2) = "./") THEN DO:
subPath = SUBSTRING(xPath, 3).
hXCurrentNode = hxBaseNode.
END.
ELSE IF (SUBSTRING(xPath, 1, 1) = "/") THEN DO:
subPath = SUBSTRING(xPath, 2).
hXCurrentNode = hxBaseNode.
END.
ELSE DO:
subPath = xPath.
hXCurrentNode = hxBaseNode.
END.

iterationDone = FALSE.
pathSegment = ENTRY(1, subPath, "/").
subPath = SUBSTRING(subPath,LENGTH(pathSegment) + 1).

/* need to validate prefixes used. */
DO ON ERROR UNDO, LEAVE:
fqNodeName = ENTRY(1, pathSegment, "[").
nodeFilter = ENTRY(2, pathSegment, "[").
nodeFilter = SUBSTRING(nodeFilter, 1, LENGTH(nodeFilter) - 1).

CATCH err AS Progress.Lang.SysError:
nodeFilter = "".
fqNodeName = pathSegment.
/* LOG ERROR */
END CATCH.
END.

/* need to validate prefixes used. */
DO ON ERROR UNDO, LEAVE:
nodeName = ENTRY(2, fqNodeName, ":").
CATCH err AS Progress.Lang.SysError:
nodeName = fqNodeName.
/* LOG ERROR */
END CATCH.
END.

DO ON ERROR UNDO, LEAVE:

startPosition = 1.
endPosition = hXCurrentNode:NUM-CHILDREN.

DO currentPosition = startPosition TO endPosition:
hXCurrentNode:GET-CHILD(hXChildNode, currentPosition).
IF (hXChildNode:SUBTYPE = "ELEMENT") THEN DO:
IF (hXChildNode:NAME = fqNodeName) THEN DO:
/* apply filter */
IF (nodeFilter = "" OR (hXChildNode:CHILD-NUM = INTEGER(nodeFilter))) THEN DO:
hXCurrentNode = hXChildNode.
hxNode = hXCurrentNode.
iterationDone = TRUE.
recurseXPath = FALSE.
LEAVE.
END.
END.
IF (recurseXPath = TRUE AND hXChildNode:NUM-CHILDREN > 0) THEN DO:
DEFINE VARIABLE hxFirstChild AS HANDLE.
CREATE X-NODEREF hxFirstChild.
hXChildNode:GET-CHILD(hxFirstChild,1).

IF (hxFirstChild:SUBTYPE = "ELEMENT") THEN DO:
iterationDone = SelectSingleNode(hXChildNode, pathSegment, hxNode).
IF (hxNode:NAME = fqNodeName) THEN DO:
hXCurrentNode = hxNode.
iterationDone = TRUE.
recurseXPath = FALSE.
END.
END.
END.
END.
END.

IF (iterationDone = TRUE) THEN
DO WHILE (ENTRY(2, subPath, "/") <> ""):
RETURN SelectSingleNode(hXCurrentNode, subPath, hxNode).
END.

RETURN iterationDone.

CATCH err AS PROGRESS.Lang.SysError:
RETURN iterationDone.
END CATCH.
END.
RETURN iterationDone.
END FUNCTION.
A simple pair of Get and Set Inner Text functions for OpenEdge Xml:

FUNCTION SetInnerText RETURNS CHARACTER (INPUT hxDoc AS HANDLE, INPUT-OUTPUT hXBaseNode AS HANDLE, INPUT nodeValue AS CHARACTER):
DEFINE VARIABLE hxTextNode AS HANDLE NO-UNDO.
CREATE X-NODEREF hxTextNode.

DO ON ERROR UNDO, LEAVE:
IF(hxBaseNode:NUM-CHILDREN > 0) THEN DO:
hxBaseNode:GET-CHILD(hxTextNode, 1).
IF (hxTextNode:SUBTYPE = "TEXT") THEN DO:
hxTextNode:NODE-VALUE = nodeValue.
RETURN hxTextNode:NODE-VALUE.
END.
ELSE
RETURN "".
END.
ELSE DO:
hxDoc:CREATE-NODE(hxTextNode, "", "TEXT").
hxTextNode:NODE-VALUE = nodeValue.
hXBaseNode:APPEND-CHILD(hxTextNode).
RETURN hxTextNode:NODE-VALUE.
END.
CATCH err AS PROGRESS.Lang.SysError:
RETURN "".
END CATCH.
END.
END FUNCTION.

FUNCTION GetInnerText RETURNS CHARACTER (INPUT hXBaseNode AS HANDLE):
DEFINE VARIABLE hxTextNode AS HANDLE NO-UNDO.
CREATE X-NODEREF hxTextNode.

DO ON ERROR UNDO, LEAVE:
hxBaseNode:GET-CHILD(hxTextNode, 1).
IF (hxTextNode:SUBTYPE = "TEXT") THEN DO:
RETURN hxTextNode:NODE-VALUE.
END.
ELSE
RETURN "".
CATCH err AS PROGRESS.Lang.SysError:
RETURN "".
END CATCH.
END.
END FUNCTION.
SOAP Sniffing in OpenEdge 10.2.A

From the OpenEdge development environment prompt run the following:

proenv>prosoapview PORTNUM

E.G.

proenv>prosoapview 4444

Thursday, February 04, 2010

Creating a SOAP header with AuthHead tags in Progress OpenEdge 10.2.A


DEFINE VARIABLE hWebService AS HANDLE NO-UNDO.
DEFINE VARIABLE hPortType AS HANDLE NO-UNDO.



PROCEDURE CreateMessageAuthHeader :
    DEFINE OUTPUT PARAMETER hSOAPHeader AS HANDLE .
    DEFINE INPUT PARAMETER cOperationNamespace AS CHARACTER .
    DEFINE INPUT PARAMETER cOperationLocalName AS CHARACTER .
    DEFINE OUTPUT PARAMETER lDeleteOnDone AS LOGICAL .

    DEFINE VARIABLE hTmpSoapHeader AS HANDLE NO-UNDO.
    DEFINE VARIABLE hSoapHeaderRef AS HANDLE NO-UNDO.
    DEFINE VARIABLE hNodeAuthHead AS HANDLE NO-UNDO.
  
    CREATE SOAP-HEADER hTmpSoapHeader.
    CREATE SOAP-HEADER-ENTRYREF hSoapHeaderRef.

    CREATE X-DOCUMENT hXDocAuthHeader.
    CREATE X-NODEREF hNodeAuthHead.

    hTmpSoapHeader:ADD-HEADER-ENTRY(hSoapHeaderRef).

    hXDocAuthHeader:CREATE-NODE-NAMESPACE(hNodeAuthHead, cNsUri, cNsPrefix + ":" + "AuthHeader", "ELEMENT").
    AddMessageElement(hNodeAuthHead, hXDocAuthHeader, "username", "username", cNsUri, cNsPrefix).
    AddMessageElement(hNodeAuthHead, hXDocAuthHeader, "password", "password", cNsUri, cNsPrefix).

    hSoapHeaderRef:SET-NODE(hNodeAuthHead).
    hSoapHeader = hTmpSoapHeader.

    lDeleteOnDone = TRUE.

END PROCEDURE .


FUNCTION ConnectWebService RETURNS INT ().
        /*Connect to Webservice*/
        CREATE SERVER hWebService.
                                
        /* replace connection string with config val */
        hWebService:CONNECT("-WSDL 'http://www.someservicesite.com/webservice.asmx?wsdl'").
        RUN PortType SET hPortType ON hWebService.

        /* Set up webservice call/callback */
        hPortType:SET-CALLBACK-PROCEDURE("REQUEST-HEADER", "CreateMessageAuthHeader").
END FUNCTION.


/***********************Web Methods*********************************/

FUNCTION GetStatus RETURNS INT
  (INPUT IdNumber AS CHARACTER)
  IN hPortType.

/***********************Business Methods*********************************/
FUNCTION GetMyStatus RETURNS INT (INPUT IdNumber  AS CHARACTER).
    ConnectWebService().
    statusResult = GetStatus(IdNumber).
END FUNCTION.

Using OpenEdge 10.2.A from Progress to consume a webservice:

When passing an EXTENT LONGCHAR containing an array of XML messages as a parameter to a web method, the xml message(s) are being partially HTML-ENCODED. Which the receiving web method does not appreciate in the slightest.

When I say the XML is being partially HTML-ENCODED, I mean that the '<' and '"'s are being replaced with '<' and '"' respectively. The '>'s are not being replaced. I have no access to the code block where the web call is being made and the encoding occurs as the WSDL is downloaded by Progress OpenEdge on each call. The WSDL is processed and the methods + signatures determined dynamically.

The Schema used defined a custom element type of "anyType". This was the source of the problem. Progress would not interpret the usage/implementation of this node correctly - where the anyType node was meant to be replaced by any other element in the schema defined as type anyType.

Taking the WSDL and manually modifying the schema portion of it to build the message without the anyType did the trick. The encoding also ceased.