All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.phloc.schematron.pure.bound.xpath.PSXPathBoundSchema Maven / Gradle / Ivy

There is a newer version: 2.7.1
Show newest version
/**
 * Copyright (C) 2014 phloc systems
 * http://www.phloc.com
 * office[at]phloc[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.phloc.schematron.pure.bound.xpath;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.phloc.commons.ValueEnforcer;
import com.phloc.commons.string.ToStringGenerator;
import com.phloc.commons.xml.xpath.XPathHelper;
import com.phloc.schematron.pure.binding.IPSQueryBinding;
import com.phloc.schematron.pure.binding.SchematronBindException;
import com.phloc.schematron.pure.binding.xpath.PSXPathVariables;
import com.phloc.schematron.pure.bound.AbstractPSBoundSchema;
import com.phloc.schematron.pure.errorhandler.IPSErrorHandler;
import com.phloc.schematron.pure.model.IPSHasMixedContent;
import com.phloc.schematron.pure.model.PSAssertReport;
import com.phloc.schematron.pure.model.PSDiagnostic;
import com.phloc.schematron.pure.model.PSName;
import com.phloc.schematron.pure.model.PSPattern;
import com.phloc.schematron.pure.model.PSPhase;
import com.phloc.schematron.pure.model.PSRule;
import com.phloc.schematron.pure.model.PSSchema;
import com.phloc.schematron.pure.model.PSValueOf;
import com.phloc.schematron.pure.validation.IPSValidationHandler;
import com.phloc.schematron.pure.validation.SchematronValidationException;
import com.phloc.schematron.pure.validation.xpath.PSXPathValidationHandlerSVRL;

/**
 * The default XPath binding for the pure Schematron implementation.
 * 
 * @author Philip Helger
 */
@Immutable
public class PSXPathBoundSchema extends AbstractPSBoundSchema
{
  private final List  m_aBoundPatterns;

  @Nullable
  private List  _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
                                                           @Nonnull final XPath aXPathContext,
                                                           @Nonnull final PSXPathVariables aVariables)
  {
    final List  ret = new ArrayList  ();
    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 = aXPathContext.compile (sPath);
            ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
          }
          catch (final XPathExpressionException ex)
          {
            error (aName, "Failed to compile XPath expression in : '" + sPath + "'", ex);
            return null;
          }
        }
        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 = aXPathContext.compile (sSelect);
            ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
          }
          catch (final XPathExpressionException ex)
          {
            error (aValueOf, "Failed to compile XPath expression in : '" + sSelect + "'", ex);
            return null;
          }
        }
        else
        {
          // No XPath compilation necessary
          ret.add (new PSXPathBoundElement (aContentElement));
        }
    }
    return ret;
  }

  @Nullable
  private Map  _createBoundDiagnostics (@Nonnull final XPath aXPath,
                                                                        @Nonnull final PSXPathVariables aVariables)
  {
    final Map  ret = new HashMap  ();
    final PSSchema aSchema = getOriginalSchema ();
    if (aSchema.hasDiagnostics ())
    {
      // For all contained diagnostic elements
      for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
      {
        final List  aBoundElements = _createBoundElements (aDiagnostic, aXPath, aVariables);
        if (aBoundElements == null)
        {
          // error already emitted
          return null;
        }

        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!");
          return null;
        }
      }
    }
    return ret;
  }

  /**
   * Pre-compile all patterns incl. their content
   * 
   * @param aXPath
   *        Global XPath object to use. May not be null.
   * @param aBoundDiagnostics
   *        A map from DiagnosticID to its mapped counterpart. May not be
   *        null.
   * @param aVariables
   *        The global Schematron-let variables. May not be null.
   * @param aVariables
   * @return null if an XPath error is contained
   */
  @Nullable
  private List  _createBoundPatterns (@Nonnull final XPath aXPath,
                                                           @Nonnull final Map  aBoundDiagnostics,
                                                           @Nonnull final PSXPathVariables aVariables)
  {
    final List  ret = new ArrayList  ();

    // For all relevant patterns
    for (final PSPattern aPattern : getAllRelevantPatterns ())
    {
      // Handle pattern specific variables
      List  aAddedVarsPattern = null;
      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 (aVariables.add (aEntry).isUnchanged ())
          {
            warn (aPattern, "Duplicate let with name '" +
                            aEntry.getKey () +
                            "' in  - second definition is ignored");
          }
          else
          {
            if (aAddedVarsPattern == null)
              aAddedVarsPattern = new ArrayList  ();
            aAddedVarsPattern.add (aEntry.getKey ());
          }
        }
      }

      // For all rules of the current pattern
      final List  aBoundRules = new ArrayList  ();
      for (final PSRule aRule : aPattern.getAllRules ())
      {
        // Handle rule specific variables
        List  aAddedVarsRule = null;
        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 (aVariables.add (aEntry).isUnchanged ())
            {
              warn (aRule, "Duplicate let with name '" +
                           aEntry.getKey () +
                           "' in  - second definition is ignored");
            }
            else
            {
              if (aAddedVarsRule == null)
                aAddedVarsRule = new ArrayList  ();
              aAddedVarsRule.add (aEntry.getKey ());
            }
          }
        }

        // For all contained assert and reports within the current rule
        final List  aBoundAssertReports = new ArrayList  ();
        for (final PSAssertReport aAssertReport : aRule.getAllAssertReports ())
        {
          final String sTest = aVariables.getAppliedReplacement (aAssertReport.getTest ());
          try
          {
            final XPathExpression aTestExpr = aXPath.compile (sTest);
            final List  aBoundElements = _createBoundElements (aAssertReport, aXPath, aVariables);
            if (aBoundElements == null)
            {
              // Error already emitted
              return null;
            }

            final PSXPathBoundAssertReport aBoundAssertReport = new PSXPathBoundAssertReport (aAssertReport,
                                                                                              sTest,
                                                                                              aTestExpr,
                                                                                              aBoundElements,
                                                                                              aBoundDiagnostics);
            aBoundAssertReports.add (aBoundAssertReport);
          }
          catch (final XPathExpressionException ex)
          {
            error (aAssertReport, "Failed to compile XPath expression in <" +
                                  (aAssertReport.isAssert () ? "assert" : "report") +
                                  ">: '" +
                                  sTest +
                                  "' " +
                                  aVariables, ex);
            return null;
          }
        }

        // Evaluate base node set for this rule
        final String sRuleContext = aVariables.getAppliedReplacement (getValidationContext (aRule.getContext ()));
        PSXPathBoundRule aBoundRule = null;
        try
        {
          final XPathExpression aRuleContext = aXPath.compile (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);
          return null;
        }

        // Finally remove all variables added for the rule
        aVariables.removeAll (aAddedVarsRule);
      }

      // Create the bound pattern
      final PSXPathBoundPattern aBoundPattern = new PSXPathBoundPattern (aPattern, aBoundRules);
      ret.add (aBoundPattern);

      // Finally remove all variables added for the pattern
      aVariables.removeAll (aAddedVarsPattern);
    }
    return ret;
  }

  /**
   * 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.phloc.schematron.pure.errorhandler.LoggingPSErrorHandler}
   *        is used internally.
   * @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) throws SchematronBindException
  {
    super (aQueryBinding, aOrigSchema, sPhase, aCustomErrorListener);

    final PSSchema aSchema = getOriginalSchema ();
    final PSPhase aPhase = getPhase ();

    // Get all "global" variables that are defined in the schema and - if
    // defined - in the specified phase
    final PSXPathVariables aVariables = new PSXPathVariables ();
    if (aSchema.hasAnyLet ())
      for (final Map.Entry  aEntry : aSchema.getAllLetsAsMap ().entrySet ())
        aVariables.add (aEntry);
    if (aPhase != null)
      for (final Map.Entry  aEntry : aPhase.getAllLetsAsMap ().entrySet ())
        if (aVariables.add (aEntry).isUnchanged ())
          warn (aSchema, "Duplicate let with name '" +
                         aEntry.getKey () +
                         "' in  with name '" +
                         sPhase +
                         "' - second definition is ignored");

    // The XPath object used to compile the expressions
    XPathFactory aXPathFactory;
    try
    {
      aXPathFactory = XPathFactory.newInstance ();
    }
    catch (final Exception ex)
    {
      throw new SchematronBindException ("Failed to create XPathFactory", ex);
    }
    final XPath aXPath = XPathHelper.createNewXPath (aXPathFactory,
                                                     (XPathVariableResolver) null,
                                                     (XPathFunctionResolver) null,
                                                     getNamespaceContext ());

    // Pre-compile all diagnostics first
    final Map  aBoundDiagnostics = _createBoundDiagnostics (aXPath, aVariables);
    if (aBoundDiagnostics == null)
      throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the log for XPath errors!");

    // Perform the pre-compilation of all XPath expressions in the patterns,
    // rules, asserts/reports and the content elements
    m_aBoundPatterns = _createBoundPatterns (aXPath, aBoundDiagnostics, aVariables);
    if (m_aBoundPatterns == null)
      throw new SchematronBindException ("Failed to precompile the supplied schema. Check the log for XPath errors!");
  }

  @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");

    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 ();
        aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());

        // 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 ())
          {
            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)
                  {
                    if (aValidationHandler.onFailedAssert (aAssertReport,
                                                           aBoundAssertReport.getTestExpression (),
                                                           aRuleMatchingNode,
                                                           i,
                                                           aBoundAssertReport).isBreak ())
                      return;
                  }
                }
                else
                {
                  // It's a report
                  if (bTestResult)
                  {
                    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).toString ();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy