Project

Profile

Help

Question about passing a Javascript object or XDM map as the context item to the XPath.evaluate function and then using ?prop to access a property

Added by Martin Honnen about 7 years ago

I am having some problems to understand how Saxon-JS with the XPath API handles a Javascript object or XDM map passed to the @evaluate@ method as the @contextItem@ where the query then tries to use the @?@ lookup operator to access a property in the map. It seems there is a difference on how this is treated with Saxon 9 Java's XPath API and the Saxon-JS API, in Saxon Java if I have a map @map { "cities" : [ map { "name": "Milano", "pop" : 5, "country" : "Italia" } ] }@ and pass that as a context item to the @evaluate@ method then I can use e.g. @?cities??country@ to access the @country@ properties of all (@@) the array items in the @cities@ property.

So a simple Java program compiled against Saxon 9 EE Java



        Processor proc = new Processor(true);
        XPathCompiler xpath = proc.newXPathCompiler();
        xpath.setLanguageVersion("3.1");
        xpath.declareNamespace("map", "http://www.w3.org/2005/xpath-functions/map");
        
        XdmItem contextItem = xpath.evaluateSingle("map { \"cities\" : [ map { \"name\": \"Milano\", \"pop\" : 5, \"country\" : \"Italia\" } ] }", null);
        
        XdmValue value = xpath.evaluate("let $distinct-countries := distinct-values(?cities?*?country)\n" +
"return\n" +
"  map:merge(\n" +
"    for $country in $distinct-countries\n" +
"    return\n" +
"      let $group := ?cities?*[?country = $country]\n" +
"      return\n" +
"        map { \n" +
"          $country : map { \n" +
"            \"cities\" : $group?name,\n" +
"            \"population\" : sum($group?pop) \n" +
"          }\n" +
"        }\n" +
"   )", contextItem);
        
        System.out.println(value);

runs fine and outputs @map{"Italia":map{"cities":"Milano","population":5}}@.

However, when I try the same with the Saxon-JS XPath API in the form of



			  var contextItem = SaxonJS.XPath.evaluate('map { "cities" : [ map { "name": "Milano", "pop" : 5, "country" : "Italia" } ] }');
			  
			  document.getElementById('input1').textContent = JSON.stringify(contextItem, null, '\t'); 
			  
			  var groups = SaxonJS.XPath.evaluate(document.getElementById('xpath').textContent, contextItem);

I get an error @Required item type of value in 'treat as' expression is function(); supplied value is "Milano"@ so it seems the lookup is done one level deeper in the map.

Is that intended or is that a bug?

The Javascript test giving the exception is online at https://martin-honnen.github.io/js/2017/grouping-json-with-xpath31-maps4.html.

Originally I wanted to write the more complex sample https://martin-honnen.github.io/js/2017/grouping-json-with-xpath31-maps1.html which gives the same error, I am able to fix that in https://martin-honnen.github.io/js/2017/grouping-json-with-xpath31-maps2.html by changing the lookups to use @?@ instead of @?cities?@ but I am wondering why that is necessary, if the difference between the Java and the Javascript API is intended.


Replies (5)

Please register to reply

RE: Question about passing a Javascript object or XDM map as the context item to the XPath.evaluate function and then using ?prop to access a property - Added by John Lumley about 7 years ago

The issue arises from the strategy of conversion from JavaScript to XDM as described in http://www.saxonica.com/saxon-js/documentation/index.html#!ixsl-extension/conversions.

In your example, the @contextItem@ JavaScript value is fine - an @Object@ whose @cities@ property is an array of (one) Object with properties @name@, @pop@ etc. That is its structure is @ { [ {} ] }@.

When @contextItem@ is backconverted into XDM for the subsequent evaluation, the array is converted into a sequence, as described in the conversion documentation. This can be shown by doubling the array brackets in the expression:

'map { "cities" : [[ map { "name": "Milano", "pop" : 5, "country" : "Italia" }, map{ "name": "London", "country": "GB"} ]] }'

where @?cities?*?country@ works as expected as, according to the conversion strategy, arrays nested inside arrays are kept as arrays, so the value of @cities@ now become a sequence of (one) array.

The code for this conversion JavaScript=>XDM currently performs this array=>sequence conversion at every level save when an array is a direct member of an array.

One could argue that with the similarities between @array()@ and @map()@ types, arrays that are 'children' of maps should also not be expanded. We'll have to consider this and your case is fairly strong evidence.

Thank you

RE: Question about passing a Javascript object or XDM map as the context item to the XPath.evaluate function and then using ?prop to access a property - Added by John Lumley about 7 years ago

Further thoughts suggest it's not going to be quite that simple. The nub of the issue is that in XDM we have three sorts of non-functional, non-node 'structures' - the flat @sequence@ (which cannot include @sequences@ as they have been concatenated), the @array@ and the @map@. The values in arrays and maps can be sequences. In the parallel JavaScript model we use in Saxon-JS we have just two employed at present - @Array@ and @Object@.

Obviously sequence is a critical and central 'type' of the XPath world and expressions return zero, one or many-member sequences. When this is converted into the JavaScript world, what representation should we use? The choice was to coerce multi-member sequences into a JS @Array@, for which usual JavaScript features (@[i], .length@ etc.) work just fine and since most results are sequences this hasn't been an issue. For @map()@ we use a loaded @Object@, with keys mapping to properties.

But what about @array()@? The choice was that in JavaScript=>XDM conversion @Array@ effectively defaults to representing a sequence: @Array@ => @sequence()@ but @Array/Array@ => @array()@. Similarly when converting XDM=>JavaScript @sequence()@ => @Array@, but currently @array()@ => @Array@.

The issue is crystalised by how would we convert the XPath

let $m := map{ 'a': (1,[2,3],4), 'b':[1,[2,3],4] }

into a JavaScript representation. In XPath @deep-equal($m?a,$m.b)@ is most certainly false, but using the current conversion to JavaScript (@n = convertToJS($m)@)

n.a == [1,[2,3],4] && n.b == [1,[2,3],4] 

and they are now equal in JavaScript eyes.

Somewhat worse, on reconverting this result back into XDM (@p = convertFromJS(n)@) we would have

map{ 'a': (1,[2,3],4), 'b':(1,[2,3],4) }

so now in XPath @deep-equal($p?a,$p?b)@ is true.

So now we have a roundtrip mismatch. If we altered the output mapping to @array()@ => @Array/Array@, then we might overcome this, albeit at the cost of the JavaScript representation containing an additional wrapping @Array@ around every true @array()@, but I'm very unsure what happens in deeper arrays and array singletons.

This needs some more thought, and probably a recursive drink or two.

RE: Question about passing a Javascript object or XDM map as the context item to the XPath.evaluate function and then using ?prop to access a property - Added by Michael Kay about 7 years ago

Round-tripping conversions between two data models that aren't isomorphic is never going to work, so we can give up on that as an objective.

It might be simplest if, in the JS->XDM direction, we always mapped JS objects to XDM maps and JS arrays to XDM arrays. The only real disadvantage in this is that XDM arrays are (a) unfamiliar to XPath users, and (b) not as convenient to manipulate as sequences (for example, filtering and mapping operations on arrays are tricky, especially in the absence of higher-order functions). Perhaps we could get round that by having a function sequence() which explicitly converts a JS array to an XDM sequence.

Note that the Java example given above is quite different, because it is explicitly using XDM types (XdmItem, XdmValue) throughout, which means there is no conversion to or from native Java datatypes such as java.util.Map or Java arrays. I think I would be a bit reluctant to introduce similar machinery for handling XDM values in Javascript (though my proposed sequence() function is a move in that direction...)

Although it's early in the life of Saxon-JS and (as they say) the future is longer than the past, I think we should respect the fact that we now have a 1.0 release in the field and users are entitled to expect their applications to continue working unless we have very good reasons for making changes.

RE: Question about passing a Javascript object or XDM map as the context item to the XPath.evaluate function and then using ?prop to access a property - Added by John Lumley about 7 years ago

To conclude, we're deciding to keep the status quo....

  • on conversion JS->XDM, an @Array@ is converted as a sequence, save that @Array/Array@ is cast to @array()@ as already documented.
  • on conversion XDM->JS, a sequence becomes @Array@; @array()@ also becomes @Array@.

This means that if you intend to evaluate an XPath expression through the JavaScript API to produce an @array()@ for later consumption in another XPath expression through an intermediate JavaScript variable, you will have to 'double' the array creation, as given in my first reply.

We will add notes about this to the documentation.

    (1-5/5)

    Please register to reply