diff --git a/ph-schematron-pure/src/main/java/com/helger/schematron/pure/bound/xpath/PSXPathBoundSchema.java b/ph-schematron-pure/src/main/java/com/helger/schematron/pure/bound/xpath/PSXPathBoundSchema.java index f749f0ab7..07a60b2e1 100644 --- a/ph-schematron-pure/src/main/java/com/helger/schematron/pure/bound/xpath/PSXPathBoundSchema.java +++ b/ph-schematron-pure/src/main/java/com/helger/schematron/pure/bound/xpath/PSXPathBoundSchema.java @@ -569,15 +569,11 @@ private void _evaluateVariables (@Nonnull final PSXPathVariables aVariables, Object aEvalResult; - // We don't know the type returned by the XPath expression, so we try - // them one after another - // Based on some common cases, the chances for "NodeList" results and - // "String" results are highest, so they are tried first - + // First we try type auto detection magic, that only works if Saxon is on + // the classpath. See #170 for the original intention try { - aEvalResult = XPathEvaluationHelper.evaluateAsNodeList (aXPathExpression, aNode, sBaseURI); - m_nVarForNodeLists++; + aEvalResult = XPathEvaluationHelper.evaluateWithTypeAutodetect (aXPathExpression, aNode, sBaseURI); } catch (final XPathExpressionException ex) { @@ -585,6 +581,22 @@ private void _evaluateVariables (@Nonnull final PSXPathVariables aVariables, aEvalResult = null; } + // We don't know the type returned by the XPath expression, so we try + // them one after another + // Based on some common cases, the chances for "NodeList" results and + // "String" results are highest, so they are tried first + + if (aEvalResult == null) + try + { + aEvalResult = XPathEvaluationHelper.evaluateAsNodeList (aXPathExpression, aNode, sBaseURI); + m_nVarForNodeLists++; + } + catch (final XPathExpressionException ex) + { + // ignore + } + // Number first, so that e.g. "sum()" is evaluated as a Number // Numbers can be converted to boolean and to string if (aEvalResult == null) diff --git a/ph-schematron-pure/src/main/java/com/helger/schematron/pure/xpath/XPathEvaluationHelper.java b/ph-schematron-pure/src/main/java/com/helger/schematron/pure/xpath/XPathEvaluationHelper.java index 90120a6b6..ff3dac7aa 100644 --- a/ph-schematron-pure/src/main/java/com/helger/schematron/pure/xpath/XPathEvaluationHelper.java +++ b/ph-schematron-pure/src/main/java/com/helger/schematron/pure/xpath/XPathEvaluationHelper.java @@ -24,6 +24,8 @@ import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -31,6 +33,27 @@ import com.helger.xml.XMLHelper; import net.sf.saxon.dom.DocumentWrapper; +import net.sf.saxon.expr.ArithmeticExpression; +import net.sf.saxon.expr.CastExpression; +import net.sf.saxon.expr.ContextItemExpression; +import net.sf.saxon.expr.Expression; +import net.sf.saxon.expr.FirstItemExpression; +import net.sf.saxon.expr.GeneralComparison; +import net.sf.saxon.expr.Literal; +import net.sf.saxon.expr.SlashExpression; +import net.sf.saxon.expr.StaticFunctionCall; +import net.sf.saxon.expr.StringLiteral; +import net.sf.saxon.expr.ValueComparison; +import net.sf.saxon.expr.sort.DocumentSorter; +import net.sf.saxon.om.FunctionItem; +import net.sf.saxon.type.AtomicType; +import net.sf.saxon.type.BuiltInAtomicType; +import net.sf.saxon.type.ItemType; +import net.sf.saxon.value.BigDecimalValue; +import net.sf.saxon.value.BooleanValue; +import net.sf.saxon.value.Int64Value; +import net.sf.saxon.value.SequenceExtent; +import net.sf.saxon.xpath.JAXPVariableReference; import net.sf.saxon.xpath.XPathExpressionImpl; /** @@ -42,23 +65,148 @@ @Immutable public final class XPathEvaluationHelper { + // Separate class that only gets loaded if Saxon is on the classpath + @Immutable + private static final class SaxonEvaluator + { + private static final Logger LOGGER = LoggerFactory.getLogger (SaxonEvaluator.class); + + private SaxonEvaluator () + {} + + @Nonnull + static Object getBaseUriFixed (@Nonnull final XPathExpression aXPath, + @Nonnull final Node aNode, + @Nonnull final String sBaseURI) + { + // Saxon specific handling + // This is trick needed for #47 - "base-uri()" + final XPathExpressionImpl aImpl = (XPathExpressionImpl) aXPath; + return new DocumentWrapper (XMLHelper.getOwnerDocument (aNode), sBaseURI, aImpl.getConfiguration ()).wrap (aNode); + } + + @Nullable + private static QName _findReturnType (@Nonnull final ItemType type) + { + if (type instanceof BuiltInAtomicType) + { + final BuiltInAtomicType biat = (BuiltInAtomicType) type; + if (biat.equals (BuiltInAtomicType.BOOLEAN)) + return XPathConstants.BOOLEAN; + if (biat.isNumericType ()) + return XPathConstants.NUMBER; + if (BuiltInAtomicType.isStringLike (biat)) + return XPathConstants.STRING; + // Fall back to node set + return XPathConstants.NODESET; + } + LOGGER.warn ("Unknown Saxon type: " + (type == null ? "null" : type.getClass ().getName ())); + return null; + } + + @Nullable + private static QName _findReturnType (@Nonnull final Expression expr) + { + if (expr instanceof ValueComparison || expr instanceof GeneralComparison) + return XPathConstants.BOOLEAN; + if (expr instanceof StringLiteral) + return XPathConstants.STRING; + if (expr instanceof ArithmeticExpression) + return XPathConstants.NUMBER; + if (expr instanceof ContextItemExpression || expr instanceof FirstItemExpression) + return XPathConstants.NODE; + if (expr instanceof SlashExpression || expr instanceof DocumentSorter) + return XPathConstants.NODESET; + if (expr instanceof Literal) + { + final Literal lit = (Literal) expr; + final var litValue = lit.getGroundedValue (); + if (litValue instanceof BooleanValue) + return XPathConstants.BOOLEAN; + if (litValue instanceof BigDecimalValue || litValue instanceof Int64Value) + return XPathConstants.NUMBER; + if (litValue instanceof SequenceExtent.Of) + return XPathConstants.NODESET; + LOGGER.warn ("Unknown Saxon grounded value type: " + + (litValue == null ? "null" : litValue.getClass ().getName ())); + return null; + } + if (expr instanceof CastExpression) + { + final AtomicType aTargetType = ((CastExpression) expr).getTargetType (); + return _findReturnType (aTargetType); + } + if (expr instanceof StaticFunctionCall) + { + final FunctionItem aTargetFun = ((StaticFunctionCall) expr).getTargetFunction (); + return _findReturnType (aTargetFun.getFunctionItemType ().getResultType ().getPrimaryType ()); + } + if (expr instanceof JAXPVariableReference) + { + // As it is unclear, if the variable can be resolved at all, we just + // skip this + return null; + } + + LOGGER.warn ("Unknown Saxon expression type: " + expr.getClass ().getName ()); + return null; + } + + @Nullable + static QName findReturnType (@Nonnull final XPathExpression aXPath) + { + final XPathExpressionImpl aImpl = (XPathExpressionImpl) aXPath; + try + { + // tested with Saxon 12.5 + return _findReturnType (aImpl.getInternalExpression ()); + } + catch (final Throwable ex) + { + // E.g. method not found, type not found etc. Just a catch all + LOGGER.error ("Failed to evaluate Saxon internal type: " + + ex.getClass ().getName () + + " - " + + ex.getMessage ()); + return null; + } + } + } + private XPathEvaluationHelper () {} + private static boolean isSaxonImplementation (@Nonnull final XPathExpression aXPath) + { + return "net.sf.saxon.xpath.XPathExpressionImpl".equals (aXPath.getClass ().getName ()); + } + + @Nullable + public static Object evaluateWithTypeAutodetect (@Nonnull final XPathExpression aXPath, + @Nonnull final Node aNode, + @Nullable final String sBaseURI) throws XPathExpressionException + { + if (!isSaxonImplementation (aXPath)) + return null; + + // Saxon specific magic + final QName aReturnType = SaxonEvaluator.findReturnType (aXPath); + if (aReturnType == null) + return null; + + return evaluate (aXPath, aNode, aReturnType, sBaseURI); + } + @Nullable public static T evaluate (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nonnull final QName aReturnType, @Nullable final String sBaseURI) throws XPathExpressionException { - Object aRealItem = aItem; - if (sBaseURI != null && "net.sf.saxon.xpath.XPathExpressionImpl".equals (aXPath.getClass ().getName ())) + Object aRealItem = aNode; + if (sBaseURI != null && isSaxonImplementation (aXPath)) { - // Saxon specific handling - // This is trick needed for #47 - "base-uri()" - final XPathExpressionImpl aImpl = (XPathExpressionImpl) aXPath; - aRealItem = new DocumentWrapper (XMLHelper.getOwnerDocument (aItem), sBaseURI, aImpl.getConfiguration ()).wrap ( - aItem); + aRealItem = SaxonEvaluator.getBaseUriFixed (aXPath, aNode, sBaseURI); } // Unfortunately there is no "any" type @@ -68,17 +216,17 @@ public static T evaluate (@Nonnull final XPathExpression aXPath, @Nullable public static Boolean evaluateAsBooleanObj (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - return evaluate (aXPath, aItem, XPathConstants.BOOLEAN, sBaseURI); + return evaluate (aXPath, aNode, XPathConstants.BOOLEAN, sBaseURI); } public static boolean evaluateAsBoolean (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - final Boolean aVal = evaluateAsBooleanObj (aXPath, aItem, sBaseURI); + final Boolean aVal = evaluateAsBooleanObj (aXPath, aNode, sBaseURI); if (aVal == null) throw new XPathExpressionException ("Failed to evaluate the XPath expression as boolean"); return aVal.booleanValue (); @@ -86,33 +234,33 @@ public static boolean evaluateAsBoolean (@Nonnull final XPathExpression aXPath, @Nullable public static Node evaluateAsNode (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - return evaluate (aXPath, aItem, XPathConstants.NODE, sBaseURI); + return evaluate (aXPath, aNode, XPathConstants.NODE, sBaseURI); } @Nullable public static NodeList evaluateAsNodeList (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - return evaluate (aXPath, aItem, XPathConstants.NODESET, sBaseURI); + return evaluate (aXPath, aNode, XPathConstants.NODESET, sBaseURI); } @Nullable public static Double evaluateAsNumber (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - return evaluate (aXPath, aItem, XPathConstants.NUMBER, sBaseURI); + return evaluate (aXPath, aNode, XPathConstants.NUMBER, sBaseURI); } @Nullable public static String evaluateAsString (@Nonnull final XPathExpression aXPath, - @Nonnull final Node aItem, + @Nonnull final Node aNode, @Nullable final String sBaseURI) throws XPathExpressionException { - return evaluate (aXPath, aItem, XPathConstants.STRING, sBaseURI); + return evaluate (aXPath, aNode, XPathConstants.STRING, sBaseURI); } } diff --git a/ph-schematron-pure/src/test/java/com/helger/schematron/pure/supplementary/Issue170Test.java b/ph-schematron-pure/src/test/java/com/helger/schematron/pure/supplementary/Issue170Test.java index 59cb64518..4dcb35482 100644 --- a/ph-schematron-pure/src/test/java/com/helger/schematron/pure/supplementary/Issue170Test.java +++ b/ph-schematron-pure/src/test/java/com/helger/schematron/pure/supplementary/Issue170Test.java @@ -46,10 +46,10 @@ public static void validateAndProduceSVRL (@Nonnull final File aSchematron, fina // Perform validation final SchematronOutputType aSVRL = aSCH.applySchematronValidationToSVRL (new FileSystemResource (aXML)); assertNotNull (aSVRL); - if (false) + if (true) LOGGER.info (new SVRLMarshaller ().getAsString (aSVRL)); - assertEquals (3, SVRLHelper.getAllFailedAssertionsAndSuccessfulReports (aSVRL).size ()); + assertEquals (12, SVRLHelper.getAllFailedAssertionsAndSuccessfulReports (aSVRL).size ()); } @Test diff --git a/ph-schematron-pure/src/test/resources/external/issues/github170/schematron.sch b/ph-schematron-pure/src/test/resources/external/issues/github170/schematron.sch index 5bfcc5e95..ffe13ad89 100644 --- a/ph-schematron-pure/src/test/resources/external/issues/github170/schematron.sch +++ b/ph-schematron-pure/src/test/resources/external/issues/github170/schematron.sch @@ -1,12 +1,22 @@ - - - error message - - - error message + + + + + + + + + error message + error message + error message + error message + error message + error message + error message + error message