Skip to content

Commit

Permalink
Added type auto detect magic; #170
Browse files Browse the repository at this point in the history
  • Loading branch information
phax committed Jul 16, 2024
1 parent 470d704 commit f104b2d
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -569,22 +569,34 @@ 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)
{
// ignore
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,36 @@
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;

import com.helger.commons.lang.GenericReflection;
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;

/**
Expand All @@ -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> 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
Expand All @@ -68,51 +216,51 @@ public static <T> 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 ();
}

@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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xpath2">
<pattern>
<rule context="/xml">
<let name="stringVar" value="'foo'"/>
<assert role="ERROR" test="$stringVar eq 'bar'">error message</assert>
</rule>
<rule context="/xml/owner">
<let name="booleanVar" value="1 eq 1"/>
<assert role="ERROR" test="$booleanVar eq true()">error message</assert>
<let name="stringVar" value="'foo'"/>
<let name="booleanVar" value="1 eq 1"/>
<let name="booleanVar2" value="true()"/>
<let name="numVar" value="123"/>
<let name="doubleVar" value="12.345"/>
<let name="listVar" value="(1,2,3)"/>
<let name="nodeVar" value="."/>
<let name="nodeSetVar" value="//owner"/>
<assert role="ERROR" test="$stringVar = 'bar'">error message</assert>
<assert role="ERROR" test="$booleanVar = false()">error message</assert>
<assert role="ERROR" test="$booleanVar2 = false()">error message</assert>
<assert role="ERROR" test="$numVar = 17">error message</assert>
<assert role="ERROR" test="$doubleVar = 3.14">error message</assert>
<assert role="ERROR" test="$listVar = (1, 2)">error message</assert>
<assert role="ERROR" test="$nodeVar = ..">error message</assert>
<assert role="ERROR" test="$nodeSetVar = ../owner">error message</assert>
</rule>
</pattern>
</schema>

0 comments on commit f104b2d

Please sign in to comment.