Curious
Curious

Reputation: 401

GET Request XML response parsing using XPath to produce a JS Object

The problem is that I get an "application/xml" response from a GET request to a WMS server which I then use the DOMParser().parseFromString() method to parse using an XPath expression. I tried looking at different solutions like dedicated libraries but I found out about XPath expressions and now I need help writing a XPath expression that will return me the Layer tags' names and titles of an XML response that looks like

<Layer>
<Title>Title Level 1</Title>
<Name>Name Level 1</Name>
    <Layer>
    <Title>Title Level 2</Title>
    <Name>Name Level 2</Name>
        <Layer>
        <Title>Title Level 3-1</Title>
        <Name>Name Level 3-1</Name>
        </Layer>
        <Layer>
        <Title>Title Level 3-2</Title>
        <Name>Name Level 3-2</Name>
        </Layer>
    </Layer>
</Layer>

And would return me a JSON

{
title: 'Title Level 1'
name: 'Name Level 1'
children: [
    {
     title: 'Title Level 2'
     name: 'Name Level 2'
     children: [
         {
          title: 'Title Level 3-1'
          name: 'Name Level 3-1'
         },
         {
          title: 'Title Level 3-1'
          name: 'Name Level 3-1'
         }
     ]
]
}

Upvotes: 0

Views: 563

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167696

You can use XSLT 3 with Saxon-JS 2 (https://www.saxonica.com/download/javascript.xml) in the browser to transform XML to JSON:

const xml = `<Layer>
<Title>Title Level 1</Title>
<Name>Name Level 1</Name>
    <Layer>
    <Title>Title Level 2</Title>
    <Name>Name Level 2</Name>
        <Layer>
        <Title>Title Level 3-1</Title>
        <Name>Name Level 3-1</Name>
        </Layer>
        <Layer>
        <Title>Title Level 3-2</Title>
        <Name>Name Level 3-2</Name>
        </Layer>
    </Layer>
</Layer>`;

const xslt = `<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    xmlns="http://www.w3.org/2005/xpath-functions"
    expand-text="yes"
    version="3.0">

  <xsl:output method="json" build-tree="no"/>
  
  <xsl:template match="/*" priority="5">
    <xsl:variable name="json-xml">
      <map>
        <xsl:apply-templates/>
      </map>
    </xsl:variable>
    <xsl:sequence select="xml-to-json($json-xml) => parse-json()"/>
  </xsl:template>
  
  <xsl:template match="*[not(*)]">
    <string key="{local-name()}">{.}</string>
  </xsl:template>
  
  <xsl:template match="Layer[1]">
    <array key="children">
       <xsl:apply-templates select="../Layer" mode="map"/>
    </array>
  </xsl:template>
  
  <xsl:template match="Layer[position() > 1]"/>
  
  <xsl:template match="Layer" mode="map">
    <map>
      <xsl:apply-templates/>
    </map>
  </xsl:template>
  
</xsl:stylesheet>`;

const jsonResult = SaxonJS.XPath.evaluate(`transform(map { 'source-node' : parse-xml($xml), 'stylesheet-text' : $xslt, 'delivery-format' : 'raw' })?output`, [], { 'params' : { 'xml' : xml, 'xslt' : xslt } });

console.log(jsonResult);
<script src="https://xsltfiddle-beta.liberty-development.net/js/SaxonJS2/SaxonJS2.js"></script>

A slightly different but perhaps more compact XSLT 3 approach would be to directly transform the XML to maps and arrays output as JSON objects and arrays:

const xml = `<Layer>
<Title>Title Level 1</Title>
<Name>Name Level 1</Name>
    <Layer>
    <Title>Title Level 2</Title>
    <Name>Name Level 2</Name>
        <Layer>
        <Title>Title Level 3-1</Title>
        <Name>Name Level 3-1</Name>
        </Layer>
        <Layer>
        <Title>Title Level 3-2</Title>
        <Name>Name Level 3-2</Name>
        </Layer>
    </Layer>
</Layer>`;

const xslt = `<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
   exclude-result-prefixes="#all"
   xmlns="http://www.w3.org/2005/xpath-functions"
   xmlns:mf="http://example.com/mf"
   expand-text="yes"
   version="3.0">
  
  <xsl:strip-space elements="*"/>

  <xsl:output method="json" build-tree="no" indent="yes"/>
  
  <xsl:template match="/Layer" priority="5">
<xsl:map>
  <xsl:apply-templates/>
</xsl:map>
  </xsl:template>
  
  <xsl:template match="*[not(*)]">
<xsl:map-entry key="local-name()" select="data()"/>
  </xsl:template>
  
  <xsl:template match="Layer[1]">
<xsl:map-entry key="'children'">
  <xsl:sequence select="array { ../Layer/mf:apply-templates(.) }"/>
</xsl:map-entry>
  </xsl:template>
  
  <xsl:template match="Layer[position() > 1]"/>
  
  <xsl:function name="mf:apply-templates" as="item()*">
<xsl:param name="elements" as="element(*)*"/>
<xsl:map>
  <xsl:apply-templates select="$elements/*"/>      
</xsl:map>
  </xsl:function>
  
</xsl:stylesheet>`;

const jsonResult = SaxonJS.XPath.evaluate(`
  transform(
    map { 
      'source-node' : parse-xml($xml), 
      'stylesheet-text' : $xslt, 
      'delivery-format' : 'raw' 
    }
  )?output`, 
  [], 
  { 'params' : { 'xml' : xml, 'xslt' : xslt } }
);

console.log(jsonResult);
<script src="https://xsltfiddle-beta.liberty-development.net/js/SaxonJS2/SaxonJS2.js"></script>

In both examples for compactness (and elegance of a executable single StackOverflow code snippet) I used XPath 3.1 transform function to run XSLT 3 code directly from JavaScript; while Saxon-JS 2 supports that flawlessly both in the browser and in Node.js it should be noted that, for performance reasons, the recommended way, if you have the XSLT developed and fixed, is to pre-compile with Saxon EE or with Saxon JS 2's xslt3 command line from XSLT to SEF (i.e. from the XML based XSLT code to the JSON based SEF format) so that the SEF can be fed directly to the transform function in the Saxon JS API https://www.saxonica.com/saxon-js/documentation/index.html#!api/transform. That should perform much better in real world scenarios than using SaxonJS.XPath.evaluate and on Node.js additionally allows you to use async programming.

Upvotes: 1

Related Questions