diff --git a/src/main/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResource.scala b/src/main/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResource.scala index c7ec81ecfb..8b0b32831f 100644 --- a/src/main/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResource.scala +++ b/src/main/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResource.scala @@ -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 @@ -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 } @@ -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 @@ -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) + } +} \ No newline at end of file diff --git a/src/main/scala/org/orbeon/scaxon/XML.scala b/src/main/scala/org/orbeon/scaxon/XML.scala index af27f4dcfe..60ecb8343c 100644 --- a/src/main/scala/org/orbeon/scaxon/XML.scala +++ b/src/main/scala/org/orbeon/scaxon/XML.scala @@ -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} @@ -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 @@ -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 diff --git a/src/test/resources/org/orbeon/oxf/xforms/tests-xforms-xpath-analysis.xml b/src/test/resources/org/orbeon/oxf/xforms/tests-xforms-xpath-analysis.xml index 2fa0bf3055..e846c297a1 100644 --- a/src/test/resources/org/orbeon/oxf/xforms/tests-xforms-xpath-analysis.xml +++ b/src/test/resources/org/orbeon/oxf/xforms/tests-xforms-xpath-analysis.xml @@ -7311,8 +7311,8 @@ - Download - Télécharger + + @@ -7322,20 +7322,22 @@ - Hello! - Bonjour! + + - + + - + + @@ -7361,12 +7363,29 @@ - + - + instance('fr-language-instance') - instance('orbeon-resources')/resource/buttons/download + instance('orbeon-resources')/resource/bar/message + + + model + + + fr-language-instance + orbeon-resources + + + + + + + + + instance('fr-language-instance') + instance('orbeon-resources')/resource/bar/alert model @@ -7383,12 +7402,12 @@ diff --git a/xforms/jvm/src/test/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResourceTest.scala b/xforms/jvm/src/test/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResourceTest.scala new file mode 100644 index 0000000000..ee8822d578 --- /dev/null +++ b/xforms/jvm/src/test/scala/org/orbeon/oxf/xforms/function/xxforms/XXFormsResourceTest.scala @@ -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 = + + + + 42 + + + + + Default Alert + Invalid Length + + + + 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)) + } + } + +}