Skip to content

Commit

Permalink
Implement #3293 "Add simple XPath 3.1 Map functions"
Browse files Browse the repository at this point in the history
- initial functions
  - `map:entry()`
  - `map:merge()`
  - `map:get()`
- PathMap is not yet supported
  • Loading branch information
ebruchez committed Jun 28, 2017
1 parent ead0149 commit f558485
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/main/java/org/orbeon/oxf/xml/XMLConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ public class XMLConstants {
public static final String XHTML_SHORT_PREFIX = "xh";
public static final String XHTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml";

public static final String XPATH_FUNCTIONS_NAMESPACE_URI = "http://www.w3.org/2005/xpath-functions";
public static final String XPATH_FUNCTIONS_NAMESPACE_URI = "http://www.w3.org/2005/xpath-functions";
public static final String XPATH_MAP_FUNCTIONS_NAMESPACE_URI = "http://www.w3.org/2005/xpath-functions/map";

public static final String SAX_LEXICAL_HANDLER = "http://xml.org/sax/properties/lexical-handler";

Expand Down
130 changes: 130 additions & 0 deletions src/main/scala/org/orbeon/oxf/xforms/function/map/functions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Copyright (C) 2017 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.function.map

import org.orbeon.oxf.xforms.function.XFormsFunction
import org.orbeon.oxf.xforms.function.map.MapFunctions.saxonTypeForMap
import org.orbeon.oxf.xml.SaxonUtils.StringValueWithEquals
import org.orbeon.saxon.Configuration
import org.orbeon.saxon.`type`.{ExternalObjectType, ItemType, TypeHierarchy}
import org.orbeon.saxon.expr.{PathMap, XPathContext}
import org.orbeon.saxon.om._
import org.orbeon.saxon.value._
import org.orbeon.scaxon.XML

trait MapFunction extends XFormsFunction {

// override def addToPathMap(pathMap: PathMap, pathMapNodeSet: PathMap.PathMapNodeSet): PathMap.PathMapNodeSet =
// addSubExpressionsToPathMap(pathMap, pathMapNodeSet)
}

//
// `map:entry($key as xs:anyAtomicType, $value as item()*) as map(*)`
//
class MapEntry extends MapFunction {

import MapFunctions._

override def evaluateItem(context: XPathContext): ObjectValue = {

implicit val ctx = context

val config = context.getConfiguration
val mapType = saxonTypeForMap(config)

val key = itemArgument(0)
val value = itemsArgument(1)

new ObjectValue(
Map(fixStringValue(key) new SequenceExtent(value)),
mapType
)
}

override def getItemType(th: TypeHierarchy): ItemType =
saxonTypeForMap(th.getConfiguration)
}

//
// `map:merge($maps as map(*)*) as map(*)`
//
class MapMerge extends MapFunction {

import MapFunctions._

override def evaluateItem(context: XPathContext): ObjectValue = {

implicit val ctx = context

val config = context.getConfiguration
val mapType = saxonTypeForMap(config)

val maps = itemsArgumentOpt(0).iterator flatMap collectMapValues

new ObjectValue(
maps.foldLeft(Map.empty[AtomicValue, ValueRepresentation])(_ ++ _),
mapType
)
}

override def getItemType(th: TypeHierarchy): ItemType =
saxonTypeForMap(th.getConfiguration)
}

//
// `map:get($map as map(*), $key as xs:anyAtomicType) as item()*`
//
class MapGet extends MapFunction {

import MapFunctions._

override def iterate(context: XPathContext): SequenceIterator = {

implicit val ctx = context

val map = itemsArgumentOpt(0).iterator flatMap collectMapValues next()
val key = fixStringValue(itemArgument(1)).asInstanceOf[AtomicValue]

map.getOrElse(key, EmptySequence.getInstance) match {
case v: Value v.iterate()
case v: NodeInfo SingletonIterator.makeIterator(v)
case _ throw new IllegalStateException
}
}
}

private object MapFunctions {

val UnderlyingClass = classOf[Map[AtomicValue, ValueRepresentation]]

def saxonTypeForMap(config: Configuration) = new ExternalObjectType(UnderlyingClass, config)

def collectMapValues(it: SequenceIterator)(implicit xpathContext: XPathContext): Iterator[Map[AtomicValue, ValueRepresentation]] = {

val config = xpathContext.getConfiguration
val mapType = saxonTypeForMap(config)

XML.asScalaIterator(it) collect {
case v: ObjectValue if v.getItemType(config.getTypeHierarchy) == mapType
v.getObject.asInstanceOf[Map[AtomicValue, ValueRepresentation]]
}
}

def fixStringValue(item: Item): Item =
item match {
case v: StringValue new StringValueWithEquals(v.getStringValueCS)
case v v
}

}
44 changes: 44 additions & 0 deletions src/main/scala/org/orbeon/oxf/xforms/library/MapFunctions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (C) 2017 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.library

import org.orbeon.oxf.xforms.function.map.{MapEntry, MapGet, MapMerge}
import org.orbeon.oxf.xml.OrbeonFunctionLibrary
import org.orbeon.saxon.`type`.{BuiltInAtomicType, Type}
import org.orbeon.saxon.expr.StaticProperty.{ALLOWS_ZERO_OR_MORE, EXACTLY_ONE}


trait MapFunctions extends OrbeonFunctionLibrary {

// Define in early definition of subclass
val MapFunctionsNS: Seq[String]

Namespace(MapFunctionsNS) {

Fun("entry", classOf[MapEntry], op = 0, min = 2, BuiltInAtomicType.ANY_ATOMIC, EXACTLY_ONE,
Arg(BuiltInAtomicType.ANY_ATOMIC, EXACTLY_ONE),
Arg(Type.ITEM_TYPE, ALLOWS_ZERO_OR_MORE)
)

Fun("merge", classOf[MapMerge], op = 0, min = 1, BuiltInAtomicType.ANY_ATOMIC, EXACTLY_ONE,
Arg(BuiltInAtomicType.ANY_ATOMIC, ALLOWS_ZERO_OR_MORE)
)

Fun("get", classOf[MapGet], op = 0, min = 2, Type.ITEM_TYPE, ALLOWS_ZERO_OR_MORE,
Arg(BuiltInAtomicType.ANY_ATOMIC, EXACTLY_ONE),
Arg(BuiltInAtomicType.ANY_ATOMIC, EXACTLY_ONE)
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
package org.orbeon.oxf.xforms.library

import org.orbeon.oxf.xml.OrbeonFunctionLibrary
import org.orbeon.oxf.xml.{OrbeonFunctionLibrary, XMLConstants}
import org.orbeon.oxf.xforms.function.Last
import org.orbeon.saxon.functions._
import org.orbeon.saxon.`type`.BuiltInAtomicType._
Expand All @@ -35,6 +35,7 @@ object XFormsFunctionLibrary extends {
val XFormsFunnyFunctionsNS = Seq(NamespaceConstant.FN, XFORMS_NAMESPACE_URI)
val XXFormsIndependentFunctionsNS = Seq(XXFORMS_NAMESPACE_URI)
val XXFormsEnvFunctionsNS = Seq(XXFORMS_NAMESPACE_URI)
val MapFunctionsNS = Seq(XMLConstants.XPATH_MAP_FUNCTIONS_NAMESPACE_URI)
val EXFormsFunctionsNS = Seq(EXFORMS_NAMESPACE_URI)
val XSLTFunctionsNS = Seq(NamespaceConstant.FN)
val tryXFormsDocument = true
Expand All @@ -47,6 +48,7 @@ object XFormsFunctionLibrary extends {
with XFormsDeprecatedFunctions
with XXFormsIndependentFunctions
with XXFormsEnvFunctions
with MapFunctions
with EXFormsFunctions
with XSLTFunctions {

Expand Down
6 changes: 6 additions & 0 deletions src/main/scala/org/orbeon/oxf/xml/FunctionSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,15 @@ abstract class FunctionSupport extends SystemFunction {
def booleanArgumentOpt(i: Int)(implicit xpathContext: XPathContext): Option[Boolean] =
arguments.lift(i) map effectiveBooleanValue

def itemsArgument(i: Int)(implicit xpathContext: XPathContext): SequenceIterator =
arguments(i).iterate(xpathContext)

def itemsArgumentOpt(i: Int)(implicit xpathContext: XPathContext): Option[SequenceIterator] =
arguments.lift(i) map (_.iterate(xpathContext))

def itemArgument(i: Int)(implicit xpathContext: XPathContext): Item =
itemsArgument(i).next()

def itemArgumentOpt(i: Int)(implicit xpathContext: XPathContext): Option[Item] =
itemsArgumentOpt(i) map (_.next())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2922,4 +2922,65 @@
</output>
</test>

<test description="Map functions" name="oxf:pipeline">
<input name="config" href="wrap-xforms-state.xpl"/>
<input name="document">
<xh:html xmlns:map="http://www.w3.org/2005/xpath-functions/map">
<xh:head>
<xf:model id="model">
<xf:instance id="results">
<results>
<number/>
<string/>
<node-name/>
<sequence/>
</results>
</xf:instance>

<xf:var
name="map"
value="
map:merge(
(
map:entry('number', 42),
map:entry('string', 'forty-two'),
map:entry('node', instance()),
map:entry('sequence', 1 to 10)
)
)"/>

<xf:action event="xforms-ready">
<xf:setvalue ref="number" value="map:get($map, 'number')"/>
<xf:setvalue ref="string" value="map:get($map, 'string')"/>
<xf:setvalue ref="node-name" value="name(map:get($map, 'node'))"/>
<xf:setvalue ref="sequence" value="string-join(for $v in map:get($map, 'sequence') return string($v), ' ')"/>
</xf:action>

</xf:model>
</xh:head>
</xh:html>
</input>
<output name="response">
<xxf:event-response xmlns:xxf="http://orbeon.org/oxf/xml/xforms">
<xxf:dynamic-state>
<dynamic-state>
<instances>
<instance id="results" model-id="model">
<results>
<number>42</number>
<string>forty-two</string>
<node-name>results</node-name>
<sequence>1 2 3 4 5 6 7 8 9 10</sequence>
</results>
</instance>
</instances>
</dynamic-state>
</xxf:dynamic-state>
<xxf:action>
<xxf:control-values/>
</xxf:action>
</xxf:event-response>
</output>
</test>

</group>
Original file line number Diff line number Diff line change
Expand Up @@ -7562,4 +7562,48 @@
</output>
</test>

<!-- Map functions don't support PathMap yet so this test is disabled as of 2017-06-28. -->
<test description="#3293: Map functions" name="oxf:java" exclude="true">
<input name="config">
<config sourcepath="oxf:/" class="org.orbeon.oxf.xforms.analysis.XPathAnalysisProcessor"/>
</input>
<input name="form">
<xh:html xmlns:map="http://www.w3.org/2005/xpath-functions/map">
<xh:head>
<xf:model xxf:xpath-analysis="true" id="model">
<xf:instance id="main">
<form>
<number>42</number>
<string>forty-two</string>
<node>text</node>
<sequence/>
</form>
</xf:instance>
</xf:model>
</xh:head>
<xh:body>
<xf:var
name="map"
value="
map:merge(
(
map:entry('number', number/xs:integer(.)),
map:entry('string', string/string()),
map:entry('node', node),
map:entry('sequence', 1 to 10)
)
)"/>

<xf:var name="number" value="map:get($map, 'number')"/>
<xf:var name="string" value="map:get($map, 'string')"/>
<xf:var name="node-text" value="map:get($map, 'node')/string()"/>
<xf:var name="sequence" value="string-join(for $v in map:get($map, 'sequence') return string($v), ' ')"/>
</xh:body>
</xh:html>
</input>
<output name="analysis">
<root/>
</output>
</test>

</group>

0 comments on commit f558485

Please sign in to comment.