Skip to content

Commit

Permalink
Implement #1328 "xxf:r() must support indexes"
Browse files Browse the repository at this point in the history
  • Loading branch information
ebruchez committed Jun 28, 2017
1 parent f558485 commit 283d2fd
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
*/
package org.orbeon.oxf.xforms.function.xxforms

import org.apache.commons.lang3.StringUtils
import org.orbeon.oxf.util.StringUtils._
import org.orbeon.oxf.xforms.function.{Instance, XFormsFunction}
import org.orbeon.oxf.xforms.model.XFormsInstance
import org.orbeon.saxon.`type`.Type
import org.orbeon.saxon.expr.{AxisExpression, PathMap, StringLiteral, XPathContext}
import org.orbeon.saxon.om.{Axis, NamespaceConstant, NodeInfo, StructuredQName}
import org.orbeon.saxon.om._
import org.orbeon.saxon.pattern.NameTest
import org.orbeon.saxon.value.StringValue
import org.orbeon.scaxon.XML._

import scala.annotation.tailrec

class XXFormsResource extends XFormsFunction {

import XXFormsResource._

override def evaluateItem(xpathContext: XPathContext): StringValue = {

implicit val ctx = xpathContext
Expand All @@ -48,9 +51,9 @@ class XXFormsResource extends XFormsFunction {
resources findResourcesElement
requestedLang XXFormsLang.resolveXMLangHandleAVTs(getContainingDocument, elementAnalysis)
resourceRoot findResourceElementForLang(resources, requestedLang)
leaf path(resourceRoot, StringUtils.replace(stringArgument(0), ".", "/"))
leaves pathFromTokens(resourceRoot, splitResourceName(stringArgument(0))).headOption
} yield
stringToStringValue(leaf.stringValue)
stringToStringValue(leaves.stringValue)

resultOpt.orNull
}
Expand All @@ -64,7 +67,7 @@ class XXFormsResource extends XFormsFunction {
// function must work in either case, readonly and readwrite.
val resourcePath = arguments.head match {
case s: StringLiteral
s.getStringValue.splitTo[List]("/.")
flattenResourceName(s.getStringValue) // this removes the indexes if any
case _
pathMap.setInvalidated(true)
return null
Expand Down Expand Up @@ -112,3 +115,45 @@ class XXFormsResource extends XFormsFunction {
null
}
}

object XXFormsResource {

private val IndexRegex = """(\d+)""".r

def splitResourceName(s: String): List[String] =
s.splitTo[List](".")

def flattenResourceName(s: String): List[String] =
splitResourceName(s) filter Name10Checker.getInstance.isValidNCName

// Hand-made simple path search
//
// - path *must* have the form `foo.bar.2.baz` (names with optional index parts)
// - each path element must be a NCName (non-qualified) except for indexes
// - as in XPath, non-qualified names mean "in no namespace"
//
// NOTE: Ideally should check if this is faster than using a pre-compiled Saxon XPath expression!
def pathFromTokens(context: NodeInfo, tokens: List[String]): List[NodeInfo] = {

@tailrec
def findChild(parents: List[NodeInfo], tokens: List[String]): List[NodeInfo] =
tokens match {
case Nil parents
case token :: restTokens
parents match {
case Nil Nil
case parents
token match {
case IndexRegex(index)
findChild(List(parents(index.toInt)), restTokens)
case path if Name10Checker.getInstance.isValidNCName(token)
findChild(parents / token toList, restTokens)
case _
throw new IllegalArgumentException(s"invalid resource path `${tokens mkString "."}`")
}
}
}

findChild(List(context), tokens)
}
}
23 changes: 1 addition & 22 deletions src/main/scala/org/orbeon/scaxon/XML.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import org.orbeon.oxf.util.XPath
import org.orbeon.oxf.util.XPath._
import org.orbeon.oxf.util.XPathCache._
import org.orbeon.oxf.xforms.XFormsStaticStateImpl.BASIC_NAMESPACE_MAPPING
import org.orbeon.oxf.xforms.action.XFormsAPI._
import org.orbeon.oxf.xforms.XFormsUtils
import org.orbeon.oxf.xforms.action.XFormsAPI._
import org.orbeon.oxf.xforms.model.XFormsInstance
import org.orbeon.oxf.xml.dom4j.Dom4jUtils
import org.orbeon.oxf.xml.{NamespaceMapping, TransformerUtils, XMLParsing, XMLReceiver}
Expand All @@ -34,7 +34,6 @@ import org.orbeon.saxon.tinytree.TinyTree
import org.orbeon.saxon.value.StringValue
import org.orbeon.saxon.xqj.{SaxonXQDataFactory, StandardObjectConverter}

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection._
import scala.xml.Elem
Expand Down Expand Up @@ -557,26 +556,6 @@ object XML {
}
}

// Hand-made simple path search
// - path *must* have the form "foo/bar/baz"
// - each path element must be a NCName (non-qualified)
// - as in XPath, non-qualified names mean "in no namespace"
def path(context: NodeInfo, path: String) = {

@tailrec def findChild(parent: Option[NodeInfo], tokens: List[String]): Option[NodeInfo] =
if (tokens.isEmpty)
parent
else
parent match {
case Some(p) findChild(p child ("" tokens.head) headOption, tokens.tail)
case None None
}

val tokens = path.splitTo[List]("/")

findChild(Some(context), tokens)
}

// Convert a Java object to a Saxon Item using the Saxon API
val anyToItem = new StandardObjectConverter(new SaxonXQDataFactory {
def getConfiguration = XPath.GlobalConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7311,8 +7311,8 @@
</xf:instance>
<xf:instance id="orbeon-resources" xxf:readonly="true">
<resources>
<resource xml:lang="en"><buttons><download>Download</download></buttons></resource>
<resource xml:lang="fr"><buttons><download>Télécharger</download></buttons></resource>
<resource xml:lang="en"/>
<resource xml:lang="fr"/>
</resources>
</xf:instance>
</xf:model>
Expand All @@ -7322,20 +7322,22 @@
<xf:model id="model">
<xf:instance id="fr-form-resources" xxf:readonly="true">
<resources>
<resource xml:lang="en"><message>Hello!</message></resource>
<resource xml:lang="fr"><message>Bonjour!</message></resource>
<resource xml:lang="en"/>
<resource xml:lang="fr"/>
</resources>
</xf:instance>
</xf:model>
</xbl:implementation>
<xbl:template>
<xf:output id="my-output" value="xxf:r('message')"/>
<xf:output id="my-output-1" value="xxf:r('foo.message')"/>
<xf:output id="my-output-2" value="xxf:r('foo.42.alert.0')"/>
</xbl:template>
</xbl:binding>
</xbl:xbl>
</xh:head>
<xh:body>
<xf:output id="my-output" value="xxf:r('buttons.download')"/>
<xf:output id="my-output-1" value="xxf:r('bar.message')"/>
<xf:output id="my-output-2" value="xxf:r('bar.10.alert.0')"/>
<fr:foo id="my-foo"/>
</xh:body>
</xh:html>
Expand All @@ -7361,12 +7363,29 @@
<instance scope="" prefixed-id="fr-language-instance" model-prefixed-id="model" binding="false" value="false"/>
<instance scope="" prefixed-id="orbeon-resources" model-prefixed-id="model" binding="false" value="false"/>
</model>
<output scope="" prefixed-id="my-output" model-prefixed-id="model" binding="false" value="true">
<output scope="" prefixed-id="my-output-1" model-prefixed-id="model" binding="false" value="true">
<value>
<analysis expression="string((xxf:r('buttons.download'))[1])" analyzed="true">
<analysis expression="string((xxf:r('bar.message'))[1])" analyzed="true">
<value-dependent>
<path>instance('fr-language-instance')</path>
<path>instance('orbeon-resources')/resource/buttons/download</path>
<path>instance('orbeon-resources')/resource/bar/message</path>
</value-dependent>
<dependent-models>
<model>model</model>
</dependent-models>
<dependent-instances>
<instance>fr-language-instance</instance>
<instance>orbeon-resources</instance>
</dependent-instances>
</analysis>
</value>
</output>
<output scope="" prefixed-id="my-output-2" model-prefixed-id="model" binding="false" value="true">
<value>
<analysis expression="string((xxf:r('bar.10.alert.0'))[1])" analyzed="true">
<value-dependent>
<path>instance('fr-language-instance')</path>
<path>instance('orbeon-resources')/resource/bar/alert</path>
</value-dependent>
<dependent-models>
<model>model</model>
Expand All @@ -7383,12 +7402,12 @@
<instance scope="my-foo" prefixed-id="my-foo≡fr-form-resources" model-prefixed-id="my-foo≡model" binding="false" value="false"/>
</model>
<template scope="my-foo" prefixed-id="my-foo≡xf-4" model-prefixed-id="my-foo≡model" binding="false" value="false">
<output scope="my-foo" prefixed-id="my-foo≡my-output" model-prefixed-id="my-foo≡model" binding="false" value="true">
<output scope="my-foo" prefixed-id="my-foo≡my-output-1" model-prefixed-id="my-foo≡model" binding="false" value="true">
<value>
<analysis expression="string((xxf:r('message'))[1])" analyzed="true">
<analysis expression="string((xxf:r('foo.message'))[1])" analyzed="true">
<value-dependent>
<path>instance('fr-language-instance')</path>
<path>instance('my-foo≡fr-form-resources')/resource/message</path>
<path>instance('my-foo≡fr-form-resources')/resource/foo/message</path>
</value-dependent>
<dependent-models>
<model>model</model>
Expand All @@ -7401,6 +7420,25 @@
</analysis>
</value>
</output>
<output scope="my-foo" prefixed-id="my-foo≡my-output-2" model-prefixed-id="my-foo≡model" binding="false" value="true">
<value>
<analysis expression="string((xxf:r('foo.42.alert.0'))[1])" analyzed="true">
<value-dependent>
<path>instance('fr-language-instance')</path>
<path>instance('my-foo≡fr-form-resources')/resource/foo/alert</path>
</value-dependent>
<dependent-models>
<model>model</model>
<model>my-foo≡model</model>
</dependent-models>
<dependent-instances>
<instance>fr-language-instance</instance>
<instance>my-foo≡fr-form-resources</instance>
</dependent-instances>
</analysis>
</value>
</output>

</template>
</foo>
</root>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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.xxforms

import org.orbeon.saxon.om.NodeInfo
import org.scalatest.FunSpec

import org.orbeon.scaxon.XML._

class XXFormsResourceTest extends FunSpec {

describe("The `pathFromTokens()` function") {

val xml1: NodeInfo =
<resources>
<components>
<grid>
<insert-above>42</insert-above>
</grid>
</components>
<first-name>
<label>First Name</label>
<alert>Default Alert</alert>
<alert>Invalid Length</alert>
</first-name>
</resources>

val Expected = List(
"components.grid.insert-above" "42",
"first-name.alert.0" "Default Alert",
"first-name.alert.1" "Invalid Length"
)

for ((path, expected) Expected)
it(s"must find the node with path `$path`") {

val result = XXFormsResource.pathFromTokens(xml1.rootElement, XXFormsResource.splitResourceName(path))

assert(1 === result.size)
assert(expected === result.stringValue)
}
}

describe("The `flattenResourceName()` function") {

val Expected = List(
"components.grid.insert-above" List("components", "grid", "insert-above"),
"first-name.alert.0" List("first-name", "alert"),
"first-name.alert.1" List("first-name", "alert")
)

for ((path, expected) Expected)
it(s"must flatten path `$path`") {
assert(expected === XXFormsResource.flattenResourceName(path))
}
}

}

0 comments on commit 283d2fd

Please sign in to comment.