com.helger.schematron.pure.bound.xpath.PSXPathBoundSchema Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-schematron Show documentation
Show all versions of ph-schematron Show documentation
Library for validating XML documents with Schematron
/**
* Copyright (C) 2014-2017 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.helger.schematron.pure.bound.xpath;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunctionResolver;
import javax.xml.xpath.XPathVariableResolver;
import org.oclc.purl.dsdl.svrl.SchematronOutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.CommonsHashMap;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.collection.ext.ICommonsMap;
import com.helger.commons.lang.ClassLoaderHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.pure.binding.IPSQueryBinding;
import com.helger.schematron.pure.binding.SchematronBindException;
import com.helger.schematron.pure.binding.xpath.IPSXPathVariables;
import com.helger.schematron.pure.binding.xpath.PSXPathVariables;
import com.helger.schematron.pure.bound.AbstractPSBoundSchema;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.model.IPSElement;
import com.helger.schematron.pure.model.IPSHasMixedContent;
import com.helger.schematron.pure.model.PSAssertReport;
import com.helger.schematron.pure.model.PSDiagnostic;
import com.helger.schematron.pure.model.PSName;
import com.helger.schematron.pure.model.PSPattern;
import com.helger.schematron.pure.model.PSPhase;
import com.helger.schematron.pure.model.PSRule;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.schematron.pure.model.PSValueOf;
import com.helger.schematron.pure.validation.IPSValidationHandler;
import com.helger.schematron.pure.validation.SchematronValidationException;
import com.helger.schematron.pure.validation.xpath.PSXPathValidationHandlerSVRL;
import com.helger.schematron.saxon.SaxonNamespaceContext;
import com.helger.schematron.xslt.util.PSErrorListener;
import com.helger.xml.namespace.MapBasedNamespaceContext;
import com.helger.xml.xpath.XPathHelper;
import net.sf.saxon.lib.FeatureKeys;
import net.sf.saxon.xpath.XPathEvaluator;
/**
* The default XPath binding for the pure Schematron implementation.
*
* @author Philip Helger
*/
@Immutable
public class PSXPathBoundSchema extends AbstractPSBoundSchema
{
private static final Logger s_aLogger = LoggerFactory.getLogger (PSXPathBoundSchema.class);
private final XPathVariableResolver m_aXPathVariableResolver;
private final XPathFunctionResolver m_aXPathFunctionResolver;
private final XPathFactory m_aXPathFactory;
private ICommonsList m_aBoundPatterns;
/**
* Compile an XPath expression string to an {@link XPathExpressionException}
* object. If expression contains any variables, the
* {@link XPathVariableResolver} will be used to resolve them within this
* method!
*
* @param aXPathContext
* Context to use. May not be null
.
* @param sXPathExpression
* The expression to be compiled. May not be null
.
* @return The precompiled {@link XPathExpression}
* @throws XPathExpressionException
* If expression cannot be compiled.
*/
@Nullable
private static XPathExpression _compileXPath (@Nonnull final XPath aXPathContext,
@Nonnull final String sXPathExpression) throws XPathExpressionException
{
XPathExpression ret = null;
try
{
ret = aXPathContext.compile (sXPathExpression);
}
catch (final XPathExpressionException ex)
{
// Do something with it
throw ex;
}
return ret;
}
@Nullable
private ICommonsList _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
@Nonnull final XPath aXPathContext,
@Nonnull final IPSXPathVariables aVariables)
{
final ICommonsList ret = new CommonsArrayList <> ();
boolean bHasAnyError = false;
for (final Object aContentElement : aMixedContent.getAllContentElements ())
{
if (aContentElement instanceof PSName)
{
final PSName aName = (PSName) aContentElement;
if (aName.hasPath ())
{
// Replace all variables
final String sPath = aVariables.getAppliedReplacement (aName.getPath ());
try
{
final XPathExpression aXpathExpression = _compileXPath (aXPathContext, sPath);
ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
}
catch (final XPathExpressionException ex)
{
error (aName, "Failed to compile XPath expression in : '" + sPath + "'", ex);
bHasAnyError = true;
}
}
else
{
// No XPath required
ret.add (new PSXPathBoundElement (aName));
}
}
else
if (aContentElement instanceof PSValueOf)
{
final PSValueOf aValueOf = (PSValueOf) aContentElement;
// Replace variables
final String sSelect = aVariables.getAppliedReplacement (aValueOf.getSelect ());
try
{
final XPathExpression aXPathExpression = _compileXPath (aXPathContext, sSelect);
ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
}
catch (final XPathExpressionException ex)
{
error (aValueOf, "Failed to compile XPath expression in : '" + sSelect + "'", ex);
bHasAnyError = true;
}
}
else
{
// No XPath compilation necessary
if (aContentElement instanceof String)
ret.add (new PSXPathBoundElement ((String) aContentElement));
else
ret.add (new PSXPathBoundElement ((IPSElement) aContentElement));
}
}
if (bHasAnyError)
return null;
return ret;
}
@Nullable
private ICommonsMap _createBoundDiagnostics (@Nonnull final XPath aXPathContext,
@Nonnull final IPSXPathVariables aGlobalVariables)
{
final ICommonsMap ret = new CommonsHashMap <> ();
boolean bHasAnyError = false;
final PSSchema aSchema = getOriginalSchema ();
if (aSchema.hasDiagnostics ())
{
// For all contained diagnostic elements
for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
{
final ICommonsList aBoundElements = _createBoundElements (aDiagnostic,
aXPathContext,
aGlobalVariables);
if (aBoundElements == null)
{
// error already emitted
bHasAnyError = true;
}
else
{
final PSXPathBoundDiagnostic aBoundDiagnostic = new PSXPathBoundDiagnostic (aDiagnostic, aBoundElements);
if (ret.put (aDiagnostic.getID (), aBoundDiagnostic) != null)
{
error (aDiagnostic, "A diagnostic element with ID '" + aDiagnostic.getID () + "' was overwritten!");
bHasAnyError = true;
}
}
}
}
if (bHasAnyError)
return null;
return ret;
}
/**
* Pre-compile all patterns incl. their content
*
* @param aXPathContext
* @param aXPathContext
* Global XPath object to use. May not be null
.
* @param aBoundDiagnostics
* A map from DiagnosticID to its mapped counterpart. May not be
* null
.
* @param aGlobalVariables
* The global Schematron-let variables. May not be null
.
* @return null
if an XPath error is contained
*/
@Nullable
private ICommonsList _createBoundPatterns (@Nonnull final XPath aXPathContext,
@Nonnull final ICommonsMap aBoundDiagnostics,
@Nonnull final IPSXPathVariables aGlobalVariables)
{
final ICommonsList ret = new CommonsArrayList <> ();
boolean bHasAnyError = false;
// For all relevant patterns
for (final PSPattern aPattern : getAllRelevantPatterns ())
{
// Handle pattern specific variables
final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
if (aPattern.hasAnyLet ())
{
// The pattern has special variables, so we need to extend the variable
// map
for (final Map.Entry aEntry : aPattern.getAllLetsAsMap ().entrySet ())
if (aPatternVariables.add (aEntry).isUnchanged ())
error (aPattern, "Duplicate with name '" + aEntry.getKey () + "' in ");
}
// For all rules of the current pattern
final ICommonsList aBoundRules = new CommonsArrayList <> ();
for (final PSRule aRule : aPattern.getAllRules ())
{
// Handle rule specific variables
final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
if (aRule.hasAnyLet ())
{
// The rule has special variables, so we need to extend the
// variable map
for (final Map.Entry aEntry : aRule.getAllLetsAsMap ().entrySet ())
if (aRuleVariables.add (aEntry).isUnchanged ())
error (aRule, "Duplicate with name '" + aEntry.getKey () + "' in ");
}
// For all contained assert and reports within the current rule
final ICommonsList aBoundAssertReports = new CommonsArrayList <> ();
for (final PSAssertReport aAssertReport : aRule.getAllAssertReports ())
{
final String sTest = aRuleVariables.getAppliedReplacement (aAssertReport.getTest ());
try
{
final XPathExpression aTestExpr = _compileXPath (aXPathContext, sTest);
final ICommonsList aBoundElements = _createBoundElements (aAssertReport,
aXPathContext,
aRuleVariables);
if (aBoundElements == null)
{
// Error already emitted
bHasAnyError = true;
}
else
{
final PSXPathBoundAssertReport aBoundAssertReport = new PSXPathBoundAssertReport (aAssertReport,
sTest,
aTestExpr,
aBoundElements,
aBoundDiagnostics);
aBoundAssertReports.add (aBoundAssertReport);
}
}
catch (final Throwable t)
{
error (aAssertReport,
"Failed to compile XPath expression in <" +
(aAssertReport.isAssert () ? "assert" : "report") +
">: '" +
sTest +
"' with the following variables: " +
aRuleVariables.getAll (),
t);
bHasAnyError = true;
}
}
// Evaluate base node set for this rule
final String sRuleContext = aGlobalVariables.getAppliedReplacement (getValidationContext (aRule.getContext ()));
PSXPathBoundRule aBoundRule = null;
try
{
final XPathExpression aRuleContext = _compileXPath (aXPathContext, sRuleContext);
aBoundRule = new PSXPathBoundRule (aRule, sRuleContext, aRuleContext, aBoundAssertReports);
aBoundRules.add (aBoundRule);
}
catch (final XPathExpressionException ex)
{
error (aRule, "Failed to compile XPath expression in : '" + sRuleContext + "'", ex);
bHasAnyError = true;
}
}
// Create the bound pattern
final PSXPathBoundPattern aBoundPattern = new PSXPathBoundPattern (aPattern, aBoundRules);
ret.add (aBoundPattern);
}
if (bHasAnyError)
return null;
return ret;
}
@Nonnull
public static XPathFactory createXPathFactorySaxonFirst () throws SchematronBindException
{
// The XPath object used to compile the expressions
XPathFactory aXPathFactory;
try
{
// First try to use Saxon, using the context class loader
aXPathFactory = XPathFactory.newInstance (XPathFactory.DEFAULT_OBJECT_MODEL_URI,
"net.sf.saxon.xpath.XPathFactoryImpl",
ClassLoaderHelper.getContextClassLoader ());
}
catch (final Throwable t)
{
// Must be Throwable because of e.g. IllegalAccessError (see issue #19)
// Seems like Saxon is not in the class path - fall back to default JAXP
try
{
aXPathFactory = XPathFactory.newInstance (XPathFactory.DEFAULT_OBJECT_MODEL_URI);
}
catch (final Exception ex2)
{
throw new SchematronBindException ("Failed to create JAXP XPathFactory", ex2);
}
}
return aXPathFactory;
}
/**
* Create a new bound schema. All the XPath pre-compilation happens inside
* this constructor, so that the {@link #validate(Node, IPSValidationHandler)}
* method can be called many times without compiling the XPath statements
* again and again.
*
* @param aQueryBinding
* The query binding to be used. May not be null
.
* @param aOrigSchema
* The original schema that should be bound. May not be
* null
.
* @param sPhase
* The selected phase. May be null
indicating that the
* default phase of the schema should be used (if present) or all
* patterns should be evaluated if no default phase is present.
* @param aCustomErrorListener
* A custom error listener to be used. May be null
in
* which case a
* {@link com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler}
* is used internally.
* @param aXPathVariableResolver
* Custom XPath variable resolver. May be null
.
* @param aXPathFunctionResolver
* Custom XPath function resolver. May be null
.
* @throws SchematronBindException
* In case XPath expressions are incorrect and pre-compilation fails
*/
public PSXPathBoundSchema (@Nonnull final IPSQueryBinding aQueryBinding,
@Nonnull final PSSchema aOrigSchema,
@Nullable final String sPhase,
@Nullable final IPSErrorHandler aCustomErrorListener,
@Nullable final XPathVariableResolver aXPathVariableResolver,
@Nullable final XPathFunctionResolver aXPathFunctionResolver) throws SchematronBindException
{
super (aQueryBinding, aOrigSchema, sPhase, aCustomErrorListener);
m_aXPathVariableResolver = aXPathVariableResolver;
m_aXPathFunctionResolver = aXPathFunctionResolver;
m_aXPathFactory = createXPathFactorySaxonFirst ();
}
@Nonnull
private XPath _createXPathContext ()
{
final MapBasedNamespaceContext aNamespaceContext = getNamespaceContext ();
final XPath aXPathContext = XPathHelper.createNewXPath (m_aXPathFactory,
m_aXPathVariableResolver,
m_aXPathFunctionResolver,
aNamespaceContext);
if (aXPathContext instanceof XPathEvaluator)
{
// Saxon implementation special handling
final XPathEvaluator aSaxonXPath = (XPathEvaluator) aXPathContext;
if (false)
{
// Enable this to debug Saxon function resolving
aSaxonXPath.getConfiguration ().setBooleanProperty (FeatureKeys.TRACE_EXTERNAL_FUNCTIONS, true);
}
// Since 9.7.0-4 it must implement NamespaceResolver
aSaxonXPath.setNamespaceContext (new SaxonNamespaceContext (aNamespaceContext));
// Wrap the PSErrorHandler to a ErrorListener
aSaxonXPath.getConfiguration ().setErrorListener (new PSErrorListener (getErrorHandler ()));
}
return aXPathContext;
}
@Nonnull
public PSXPathBoundSchema bind () throws SchematronBindException
{
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("Binding pure Schematron");
if (m_aBoundPatterns != null)
throw new IllegalStateException ("bind must only be called once!");
final PSSchema aSchema = getOriginalSchema ();
final PSPhase aPhase = getPhase ();
// Get all "global" variables that are defined in the schema
final PSXPathVariables aGlobalVariables = new PSXPathVariables ();
if (aSchema.hasAnyLet ())
for (final Map.Entry aEntry : aSchema.getAllLetsAsMap ().entrySet ())
if (aGlobalVariables.add (aEntry).isUnchanged ())
error (aSchema, "Duplicate with name '" + aEntry.getKey () + "' in global ");
if (aPhase != null)
{
// Get all variables that are defined in the specified phase
for (final Map.Entry aEntry : aPhase.getAllLetsAsMap ().entrySet ())
if (aGlobalVariables.add (aEntry).isUnchanged ())
error (aSchema,
"Duplicate with name '" + aEntry.getKey () + "' in with name '" + getPhaseID () + "'");
}
final XPath aXPathContext = _createXPathContext ();
// Pre-compile all diagnostics first
final ICommonsMap aBoundDiagnostics = _createBoundDiagnostics (aXPathContext,
aGlobalVariables);
if (aBoundDiagnostics == null)
throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the " +
(isDefaultErrorHandler () ? "log output" : "error listener") +
" for XPath errors!");
// Perform the pre-compilation of all XPath expressions in the patterns,
// rules, asserts/reports and the content elements
m_aBoundPatterns = _createBoundPatterns (aXPathContext, aBoundDiagnostics, aGlobalVariables);
if (m_aBoundPatterns == null)
throw new SchematronBindException ("Failed to precompile the supplied schema.");
return this;
}
@Nullable
public XPathVariableResolver getXPathVariableResolver ()
{
return m_aXPathVariableResolver;
}
@Nullable
public XPathFunctionResolver getXPathFunctionResolver ()
{
return m_aXPathFunctionResolver;
}
@Nonnull
public String getValidationContext (@Nonnull final String sRuleContext)
{
// Do we already have an absolute XPath?
if (sRuleContext.startsWith ("/"))
return sRuleContext;
// Create an absolute XPath expression!
return "//" + sRuleContext;
}
public void validate (@Nonnull final Node aNode,
@Nonnull final IPSValidationHandler aValidationHandler) throws SchematronValidationException
{
ValueEnforcer.notNull (aNode, "Node");
ValueEnforcer.notNull (aValidationHandler, "ValidationHandler");
if (m_aBoundPatterns == null)
throw new IllegalStateException ("bind was never called!");
final PSSchema aSchema = getOriginalSchema ();
final PSPhase aPhase = getPhase ();
// Call the "start" callback method
aValidationHandler.onStart (aSchema, aPhase);
// For all bound patterns
for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
{
final PSPattern aPattern = aBoundPattern.getPattern ();
aValidationHandler.onPattern (aPattern);
// For all bound rules
rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
{
final PSRule aRule = aBoundRule.getRule ();
// Find all nodes matching the rules
NodeList aRuleMatchingNodes = null;
try
{
aRuleMatchingNodes = (NodeList) aBoundRule.getBoundRuleExpression ().evaluate (aNode, XPathConstants.NODESET);
}
catch (final XPathExpressionException ex)
{
error (aRule,
"Failed to evaluate XPath expression to a nodeset: '" + aBoundRule.getRuleExpression () + "'",
ex);
continue rules;
}
final int nRuleMatchingNodes = aRuleMatchingNodes.getLength ();
if (nRuleMatchingNodes > 0)
{
// For all contained assert and report elements
for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
{
// XSLT does "fired-rule" for each node
aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());
final PSAssertReport aAssertReport = aBoundAssertReport.getAssertReport ();
final boolean bIsAssert = aAssertReport.isAssert ();
final XPathExpression aTestExpression = aBoundAssertReport.getBoundTestExpression ();
// Check each node, if it matches the assert/report
for (int i = 0; i < nRuleMatchingNodes; ++i)
{
final Node aRuleMatchingNode = aRuleMatchingNodes.item (i);
try
{
final boolean bTestResult = ((Boolean) aTestExpression.evaluate (aRuleMatchingNode,
XPathConstants.BOOLEAN)).booleanValue ();
if (bIsAssert)
{
// It's an assert
if (!bTestResult)
{
// Assert failed
if (aValidationHandler.onFailedAssert (aAssertReport,
aBoundAssertReport.getTestExpression (),
aRuleMatchingNode,
i,
aBoundAssertReport)
.isBreak ())
{
return;
}
}
}
else
{
// It's a report
if (bTestResult)
{
// Successful report
if (aValidationHandler.onSuccessfulReport (aAssertReport,
aBoundAssertReport.getTestExpression (),
aRuleMatchingNode,
i,
aBoundAssertReport)
.isBreak ())
{
return;
}
}
}
}
catch (final XPathExpressionException ex)
{
error (aRule,
"Failed to evaluate XPath expression to a boolean: '" +
aBoundAssertReport.getTestExpression () +
"'",
ex);
}
}
}
if (false)
{
// The rule matched at least one node. In this case continue with
// the next pattern
break rules;
}
}
}
}
// Call the "end" callback method
aValidationHandler.onEnd (aSchema, aPhase);
}
@Nonnull
public SchematronOutputType validateComplete (@Nonnull final Node aNode) throws SchematronValidationException
{
final PSXPathValidationHandlerSVRL aValidationHandler = new PSXPathValidationHandlerSVRL (getErrorHandler ());
validate (aNode, aValidationHandler);
return aValidationHandler.getSVRL ();
}
@Override
public String toString ()
{
return ToStringGenerator.getDerived (super.toString ()).append ("boundPatterns", m_aBoundPatterns).getToString ();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy