Saturday, August 10, 2019

Xpath Evaluation On XML where there are multiple Namespace Declaration and there's XMLNS Declaration Without Prefix

The XML contains multiple or more than one XMLNS namespace declaration. One of them is with declaration without prefix (http://www.ilog.com/rules/param) which causes issues where xpath can't read the Xml element.

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ds="http://www.ilog.com/rules/DecisionService" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<DecisionServiceResponse >
<DecisionID>RLOS001PL62000205DRAFT_2019-07-05-13:35:57:48957</DecisionID>
<underwritingRequest xmlns="http://www.ilog.com/rules/param">
<ns0:underwritingApprovalRequest xmlns:ns0="http://www.tmbbank.com/enterprise/model">
<application xmlns="">
<projectCode/>

I have a Java class that implements javax.xml.namespace.NamespaceContext which later will be instantiated and set in the setNamespaceContext of the class javax.xml.xpath.XPath. Below showing how I override the 3 methods from the NamespaceContext interface. 
    /**
     * This method is called by XPath. It returns the default namespace, if the
     * prefix is null or "".
     *
     * @param prefix to search for
     * @return uri
     */
    public String getNamespaceURI(String prefix) {
        if (prefix == null || prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
            return prefix2Uri.get(DEFAULT_NS);
        } else {
            return prefix2Uri.get(prefix);
        }
    }
    //This method is not needed in this context, but can be implemented in a similar way.
    public String getPrefix(String namespaceURI) {
        return uri2Prefix.get(namespaceURI);
    }
    public Iterator<String> getPrefixes(String namespaceURI) {
        // Not implemented
        return null;
    } 
Apart from the above, there is another method as below: 
    private void putInCache(String prefix, String uri) {
            prefix2Uri.put(prefix, uri);
            uri2Prefix.put(uri, prefix);
    }
    private void storeAttribute(Attr attribute) {
        // examine the attributes in namespace xmlns
        if (attribute.getNamespaceURI() != null
                && attribute.getNamespaceURI().equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI)) {
            // Default namespace xmlns="uri goes here"
            if (attribute.getNodeName().equals(XMLConstants.XMLNS_ATTRIBUTE)) {
                putInCache(DEFAULT_NS, attribute.getNodeValue());
            } else {
                // Here are the defined prefixes stored
                putInCache(attribute.getLocalName(), attribute.getNodeValue());
            }
        }
    }
The above method would not work because are multiple xmlns declarations, and prefix2Uri is a HashMap which does not allow duplicates, hence the prefix DEFAULT will be set to the most latest xmlns="" declaration than the one highlighted in yellow as above. 

Let me print something below for your clarity: 
ns0, http://www.tmbbank.com/enterprise/model
DEFAULT, http://www.ilog.com/rules/param
DEFAULT, ""
As you can see, eventually the prefix2Uri HashMap contains only 2 entries: 
ns0, http://www.tmbbank.com/enterprise/model
DEFAULT, ""
In order to fix this, just need to add one line in the putInCache method that looks like below, highlighted in light blue: 
    private void putInCache(String prefix, String uri) {
        if (prefix2Uri.get(prefix) == null || prefix2Uri.get(prefix).equals("")) {
            prefix2Uri.put(prefix, uri);
            uri2Prefix.put(uri, prefix);
        }
    }
Besides that, the XPath query that is used to query the XML, also needs to add DEFAULT as the prefix. 

Initially, the method that builds the XPath is as below: 
    public static String getPath(Node n) {
        StringBuilder path = new StringBuilder();
        do {           
            path.insert(0, n.getNodeName());
            path.insert(0, "/");
        } while ((n = n.getParentNode()) != null && n.getNodeType() == Node.ELEMENT_NODE);
        return path.toString();
    }

Now, it should look like the below , added lines highlighted in purple:
    public static String getPath(Node n) {
        StringBuilder path = new StringBuilder();
        do {           
            if(n.getNamespaceURI() != null && !n.getNamespaceURI().equals("") && (n.getPrefix() == null || n.getPrefix().equals(""))){                path.insert(0, UniversalNamespaceCache.DEFAULT_NS + ":" + n.getNodeName());
            } else {
                path.insert(0, n.getNodeName());
            }
            path.insert(0, "/");
        } while ((n = n.getParentNode()) != null && n.getNodeType() == Node.ELEMENT_NODE);
        return path.toString();
    }




Thursday, August 8, 2019

Apache Camel DSL Sample That Does A Few Things

Below piece does a few things. It's useful if you are using Camel DSL like me. The explanation is in the form of comment below. 

<!-- Bean instantiation of Sql Server driver which will be referenced by below DSL -->
    <bean id="sqlServerDS"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${mssql.driverClassName}" />
        <property name="url" value="${mssql.url}" />
        <property name="username" value="${mssql.username}" />
        <property name="password" value="${mssql.password}" />
    </bean>

<!-- Setting up camel endpoint for XSLT which will be used to transform the received XML into another XML -->
<camel:endpoint id="someXSLT" uri="xslt:file://${xslt.location}/someXSLT.xslt"/>

<!-- Setting up File endpoint to output the generated output flat file -->
<camel:endpoint id="InformationFile" uri="file://?fileName=${output.location}/$simple{file:name}&amp;fileExist=Append"/>

<!-- Camel Route starts -->
<camel:route id="LalaLand">

                        <!-- Listening to someQueue which has been setup in ActiveMq -->
                        <camel:from uri="tcpActivemq:queue:someQueue"/>

                        <!-- transform the Xml picked up from someQueue using someXSLT -->
<camel:to ref="someXSLT"/>
                        <!-- define file name for the output file and stores it in Header -->
                        <camel:setHeader headerName="CamelFileName">
<camel:xpath resultType="java.lang.String">concat('somefile_', //code, '.txt')     </camel:xpath>
</camel:setHeader>
                        
                        <!-- logging -->
<camel:log message="SPLIT BEGINS" loggingLevel="DEBUG" logName="lalaland" />
                       <!-- Split the Xml transformed by someXSLT into lines -->
<camel:split>
<camel:xpath>//FileRecord</camel:xpath>
<camel:setHeader headerName="ctcRef">
<camel:xpath resultType="java.lang.String">//contactRef</camel:xpath>
</camel:setHeader>
                                <!- The payload will lose in the next section so needs to store the original message -->
<camel:setProperty propertyName="oriMessage">
<camel:simple>${body}</camel:simple>
</camel:setProperty>

                                <!-- if header variable ctcRef is not empty, Camel DSL will call 3 SQL to finally get the Date Of Birth, which is the requirement of this. Why 3 Sql? Because over here it does not support JOIN query -->
<camel:choice>
<camel:when>
<camel:simple>${header.ctcRef} != ''</camel:simple>
<camel:to uri="sql:SELECT ID FROM CONTACTS WHERE CONTACTREF = :#ctcRef;?dataSource=sqlServerDS" />
<camel:log message="body: ${body[0].get('ID')} " loggingLevel="DEBUG" logName="com.experian" />
<camel:setHeader headerName="contactId">
<camel:simple>${body[0].get('ID')}</camel:simple>
</camel:setHeader>
<camel:log message="contactId: ${header.contactId} " loggingLevel="DEBUG" logName="com.experian" />
<camel:to uri="sql:SELECT ACCOUNTID FROM ACCOUNTCONTACTS WHERE CONTACTTYPEID = 102 AND CONTACTID = :#contactId;?dataSource=sqlServerDS" />
<camel:setHeader headerName="accountId">
<camel:simple>${body[0].get('ACCOUNTID')}</camel:simple>
</camel:setHeader>
<camel:log message="accountId: ${header.accountId} " loggingLevel="DEBUG" logName="com.experian" />
<camel:to uri="sql:SELECT CONVERT(varchar,DATE_OF_BIRTH,23) AS DOB FROM CS_PERSON WHERE ACCOUNTS1 = :#accountId;?dataSource=sqlServerDS" />
                                                <!-- Store Date Of Birth in Header -->
<camel:setProperty propertyName="dob">
<camel:simple>${body[0].get('DOB')}</camel:simple>
</camel:setProperty>
<camel:log message="dob: ${header.dob} " loggingLevel="DEBUG" logName="com.experian" />
</camel:when>
</camel:choice>
                               <!-- Set the Body back with the original payload stored above -->
<camel:setBody>
<camel:simple>${property.oriMessage}</camel:simple>
</camel:setBody>
                                
                                <!-- Retrieves field value using xpath, add the dob from Header, and print in one line -->
<camel:transform>
<camel:xpath resultType="java.lang.String">concat(//accountRef, '|', //contactRef, '|', //title, '|', //givenName, '|', //middleName, '|', //familyName, '|', ${header.dob}, '&#xD;&#xA;')</camel:xpath>
</camel:transform>
                                
                               <!- The below will be executed after last line by checking based on CamelSplitComplete from Header -->
<camel:when>
<camel:simple>${header.CamelSplitComplete} == true</camel:simple>
                                        <!-- Below will add below wordings and number of records based on CamelSplitSize from Header to the output flat file -->
<camel:transform>
<camel:simple>adding new line here ${header.CamelSplitSize} </camel:simple>
</camel:transform>
</camel:when>
                                <!- content will be output to a physical file -->
<camel:to ref="someFile"/>
</camel:split>
</camel:route>