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

com.helger.phive.engine.schematron.ValidationExecutorSchematron Maven / Gradle / Ivy

/**
 * Copyright (C) 2014-2021 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.phive.engine.schematron;

import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.transform.dom.DOMSource;
import javax.xml.xpath.XPath;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.error.IError;
import com.helger.commons.error.SingleError;
import com.helger.commons.error.level.EErrorLevel;
import com.helger.commons.error.level.IErrorLevel;
import com.helger.commons.error.list.ErrorList;
import com.helger.commons.hashcode.HashCodeGenerator;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.location.SimpleLocation;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.commons.wrapper.Wrapper;
import com.helger.phive.api.EValidationType;
import com.helger.phive.api.IValidationType;
import com.helger.phive.api.artefact.IValidationArtefact;
import com.helger.phive.api.artefact.ValidationArtefact;
import com.helger.phive.api.execute.AbstractValidationExecutor;
import com.helger.phive.api.execute.IValidationExecutor;
import com.helger.phive.api.result.ValidationResult;
import com.helger.phive.engine.source.IValidationSourceXML;
import com.helger.schematron.AbstractSchematronResource;
import com.helger.schematron.SchematronResourceHelper;
import com.helger.schematron.pure.SchematronResourcePure;
import com.helger.schematron.pure.errorhandler.WrappedCollectingPSErrorHandler;
import com.helger.schematron.sch.SchematronResourceSCH;
import com.helger.schematron.schxslt.xslt2.SchematronResourceSchXslt_XSLT2;
import com.helger.schematron.svrl.SVRLFailedAssert;
import com.helger.schematron.svrl.SVRLHelper;
import com.helger.schematron.svrl.SVRLMarshaller;
import com.helger.schematron.svrl.SVRLResourceError.SVRLErrorBuilder;
import com.helger.schematron.svrl.SVRLSuccessfulReport;
import com.helger.schematron.svrl.jaxb.SchematronOutputType;
import com.helger.schematron.xslt.SchematronResourceXSLT;
import com.helger.xml.EXMLParserFeature;
import com.helger.xml.XMLHelper;
import com.helger.xml.namespace.IIterableNamespaceContext;
import com.helger.xml.namespace.MapBasedNamespaceContext;
import com.helger.xml.serialize.read.DOMReaderSettings;
import com.helger.xml.serialize.write.XMLWriter;
import com.helger.xml.transform.WrappedCollectingTransformErrorListener;
import com.helger.xml.xpath.XPathExpressionHelper;
import com.helger.xml.xpath.XPathHelper;

/**
 * Implementation of {@link IValidationExecutor} for Schematron validation.
 *
 * @author Philip Helger
 */
public class ValidationExecutorSchematron extends AbstractValidationExecutor  implements
                                          IValidationExecutor.ICacheSupport
{
  private enum ESchematronOutput
  {
    SVRL,
    OIOUBL
  }

  public static final String IN_MEMORY_RESOURCE_NAME = "in-memory-data";

  private static final Logger LOGGER = LoggerFactory.getLogger (ValidationExecutorSchematron.class);

  private final String m_sPrerequisiteXPath;
  private final MapBasedNamespaceContext m_aNamespaceContext;
  private boolean m_bCacheSchematron = ICacheSupport.DEFAULT_CACHE;
  private ICommonsMap  m_aCustomErrorLevels;

  public ValidationExecutorSchematron (@Nonnull final IValidationArtefact aValidationArtefact,
                                       @Nullable final String sPrerequisiteXPath,
                                       @Nullable final IIterableNamespaceContext aNamespaceContext)
  {
    super (aValidationArtefact);
    ValueEnforcer.isTrue (aValidationArtefact.getValidationArtefactType ().isSchematron (), "Artifact is not a Schematron");
    m_sPrerequisiteXPath = sPrerequisiteXPath;
    m_aNamespaceContext = aNamespaceContext == null ? null : new MapBasedNamespaceContext (aNamespaceContext);
  }

  @Nullable
  public final String getPrerequisiteXPath ()
  {
    return m_sPrerequisiteXPath;
  }

  @Nullable
  @ReturnsMutableObject
  public final MapBasedNamespaceContext getNamespaceContext ()
  {
    return m_aNamespaceContext == null ? null : m_aNamespaceContext.getClone ();
  }

  public final boolean isCacheArtefact ()
  {
    return m_bCacheSchematron;
  }

  @Nonnull
  public final ValidationExecutorSchematron setCacheArtefact (final boolean bCacheArtefact)
  {
    m_bCacheSchematron = bCacheArtefact;
    return this;
  }

  public void ensureItemIsInCache ()
  {
    if (m_bCacheSchematron)
    {
      final AbstractSchematronResource aRes = _createSchematronResource (null, new ErrorList (), x -> {});
      aRes.setUseCache (true);
      aRes.isValidSchematron ();
      LOGGER.debug ("ValidationExecutorSchematron " + getValidationArtefact ().getRuleResourcePath () + " is now in the cache");
    }
  }

  @Nonnull
  public final ValidationExecutorSchematron addCustomErrorLevel (@Nonnull @Nonempty final String sErrorID,
                                                                 @Nonnull final EErrorLevel eErrorLevel)
  {
    ValueEnforcer.notEmpty (sErrorID, "ErrorID");
    ValueEnforcer.notNull (eErrorLevel, "ErrorLevel");
    if (m_aCustomErrorLevels == null)
      m_aCustomErrorLevels = new CommonsHashMap <> ();
    m_aCustomErrorLevels.put (sErrorID, eErrorLevel);
    return this;
  }

  @Nonnull
  public final ValidationExecutorSchematron addCustomErrorLevels (@Nullable final Map  aCustomErrorLevels)
  {
    if (aCustomErrorLevels != null && !aCustomErrorLevels.isEmpty ())
    {
      if (m_aCustomErrorLevels == null)
        m_aCustomErrorLevels = new CommonsHashMap <> ();
      m_aCustomErrorLevels.putAll (aCustomErrorLevels);
    }
    return this;
  }

  @Nonnull
  private AbstractSchematronResource _createSchematronResource (@Nullable final Locale aLocale,
                                                                @Nonnull final ErrorList aErrorList,
                                                                @Nonnull final Consumer  aSpecialOutputHdl)
  {
    final IValidationArtefact aArtefact = getValidationArtefact ();

    // get the Schematron resource to be used for this validation artefact
    final IReadableResource aSCHRes = aArtefact.getRuleResource ();

    final IValidationType aVT = aArtefact.getValidationArtefactType ();
    if (aVT == EValidationType.SCHEMATRON_PURE)
    {
      final SchematronResourcePure aPureSCH = new SchematronResourcePure (aSCHRes);
      aPureSCH.setErrorHandler (new WrappedCollectingPSErrorHandler (aErrorList));
      // Don't cache to avoid that errors in the Schematron are hidden on
      // consecutive calls!
      return aPureSCH;
    }

    if (aVT == EValidationType.SCHEMATRON_SCH)
    {
      final SchematronResourceSCH aSCHSCH = new SchematronResourceSCH (aSCHRes);
      aSCHSCH.setErrorListener (new WrappedCollectingTransformErrorListener (aErrorList));
      if (aLocale != null && StringHelper.hasText (aLocale.getLanguage ()))
        aSCHSCH.setLanguageCode (aLocale.getLanguage ());
      return aSCHSCH;
    }

    if (aVT == EValidationType.SCHEMATRON_SCHXSLT)
    {
      final SchematronResourceSchXslt_XSLT2 aSCHSCH = new SchematronResourceSchXslt_XSLT2 (aSCHRes);
      aSCHSCH.setErrorListener (new WrappedCollectingTransformErrorListener (aErrorList));
      if (aLocale != null && StringHelper.hasText (aLocale.getLanguage ()))
        aSCHSCH.setLanguageCode (aLocale.getLanguage ());
      return aSCHSCH;
    }

    if (aVT == EValidationType.SCHEMATRON_XSLT)
    {
      final SchematronResourceXSLT aSCHXSLT = new SchematronResourceXSLT (aSCHRes);
      aSCHXSLT.setErrorListener (new WrappedCollectingTransformErrorListener (aErrorList));
      return aSCHXSLT;
    }

    if (aVT == EValidationType.SCHEMATRON_OIOUBL)
    {
      final SchematronResourceXSLT aSCHXSLT = new SchematronResourceXSLT (aSCHRes);
      aSCHXSLT.setErrorListener (new WrappedCollectingTransformErrorListener (aErrorList));
      // Special output layout
      aSpecialOutputHdl.accept (ESchematronOutput.OIOUBL);
      return aSCHXSLT;
    }

    throw new IllegalStateException ("Unsupported Schematron validation type: " + aVT);
  }

  @Nonnull
  public ValidationResult applyValidation (@Nonnull final IValidationSourceXML aSource, @Nullable final Locale aLocale)
  {
    ValueEnforcer.notNull (aSource, "Source");

    final IValidationArtefact aArtefact = getValidationArtefact ();

    // Get source as XML DOM Node
    Node aNode = null;
    try
    {
      aNode = SchematronResourceHelper.getNodeOfSource (aSource.getAsTransformSource (),
                                                        new DOMReaderSettings ().setFeatureValues (EXMLParserFeature.AVOID_XML_ATTACKS));
    }
    catch (final Exception ex)
    {
      throw new IllegalStateException ("For Schematron validation to work, the source must be valid XML which it is not.", ex);
    }

    if (StringHelper.hasText (m_sPrerequisiteXPath))
    {
      // Check if the artefact can be applied on the given document by
      // checking the prerequisite XPath
      final XPath aXPathContext = XPathHelper.createNewXPath ();
      if (m_aNamespaceContext != null)
        aXPathContext.setNamespaceContext (m_aNamespaceContext);

      try
      {
        final Boolean aResult = XPathExpressionHelper.evalXPathToBoolean (aXPathContext,
                                                                          m_sPrerequisiteXPath,
                                                                          XMLHelper.getOwnerDocument (aNode));
        if (aResult != null && !aResult.booleanValue ())
        {
          if (LOGGER.isInfoEnabled ())
            LOGGER.info ("Ignoring validation artefact " +
                         aArtefact.getRuleResourcePath () +
                         " because the prerequisite XPath expression '" +
                         m_sPrerequisiteXPath +
                         "' is not fulfilled.");
          return ValidationResult.createIgnoredResult (aArtefact);
        }
      }
      catch (final IllegalArgumentException ex)
      {
        // Catch errors in prerequisite XPaths - most likely because of
        // missing namespace prefixes...
        final String sErrorMsg = "Failed to verify if validation artefact " +
                                 aArtefact.getRuleResourcePath () +
                                 " matches the prerequisite XPath expression '" +
                                 m_sPrerequisiteXPath +
                                 "' - ignoring validation artefact.";
        LOGGER.error (sErrorMsg, ex);
        return new ValidationResult (aArtefact,
                                     new ErrorList (SingleError.builderError ().setErrorText (sErrorMsg).setLinkedException (ex).build ()));
      }
    }

    // No prerequisite or prerequisite matched
    final ErrorList aErrorList = new ErrorList ();
    final Wrapper  aOutput = new Wrapper <> (ESchematronOutput.SVRL);
    final AbstractSchematronResource aSCH = _createSchematronResource (aLocale, aErrorList, aOutput::set);

    // Don't cache to avoid that errors in the Schematron are hidden on
    // consecutive calls!
    aSCH.setUseCache (m_bCacheSchematron);

    try
    {
      // Main application of Schematron
      final Document aDoc = aSCH.applySchematronValidation (new DOMSource (aNode));

      if (LOGGER.isDebugEnabled ())
        LOGGER.debug ("SVRL: " + XMLWriter.getNodeAsString (aDoc));

      switch (aOutput.get ())
      {
        case SVRL:
        {
          final SchematronOutputType aSVRL = aDoc == null || aDoc.getDocumentElement () == null ? null : new SVRLMarshaller ().read (aDoc);
          if (aSVRL != null)
          {
            // Valid Schematron - interpret result

            // Convert failed asserts and successful reports to error objects
            for (final SVRLFailedAssert aFailedAssert : SVRLHelper.getAllFailedAssertions (aSVRL))
              aErrorList.add (aFailedAssert.getAsResourceError (aSource.getSystemID ()));
            for (final SVRLSuccessfulReport aSuccessfulReport : SVRLHelper.getAllSuccessfulReports (aSVRL))
              aErrorList.add (aSuccessfulReport.getAsResourceError (aSource.getSystemID ()));
          }
          else
          {
            // Schematron does not create SVRL!
            LOGGER.warn ("Failed to read the result as SVRL:" +
                         (aDoc != null ? "\n" + XMLWriter.getNodeAsString (aDoc) : " no XML Document created"));
            aErrorList.add (SingleError.builderError ()
                                       .setErrorLocation (aArtefact.getRuleResourcePath ())
                                       .setErrorText ("Internal error interpreting Schematron result")
                                       .setErrorFieldName (aDoc != null ? XMLWriter.getNodeAsString (aDoc) : null)
                                       .build ());
          }
          break;
        }
        case OIOUBL:
        {
          if (aDoc != null && aDoc.getDocumentElement () != null)
          {
            // interpret result
            /**
             * 
             * 
             *   Checking OIOUBL-2.02 Invoice, 2017-09-15, Version 1.9.0.34429
             *   
             *     cbc:UBLVersionID = '2.0'
             *     [F-LIB001] Invalid UBLVersionID. Must be '2.0'
             *     /Invoice[1]
             *   
             * 
             * 
*/ for (final Element eError : XMLHelper.getChildElementIterator (aDoc.getDocumentElement (), "Error")) { // final String sContext = eError.getAttribute ("context"); final String sPattern = XMLHelper.getFirstChildElementOfName (eError, "Pattern").getTextContent (); final String sDescription = XMLHelper.getFirstChildElementOfName (eError, "Description").getTextContent (); final String sXPath = XMLHelper.getFirstChildElementOfName (eError, "Xpath").getTextContent (); aErrorList.add (new SVRLErrorBuilder (sPattern).setErrorLocation (new SimpleLocation (aSource.getSystemID ())) .setErrorText (sDescription) .setErrorFieldName (sXPath) .build ()); } } else { // Schematron does not create SVRL! LOGGER.warn ("Failed to read the result as OIOUBL result:" + (aDoc != null ? "\n" + XMLWriter.getNodeAsString (aDoc) : " no XML Document created")); aErrorList.add (SingleError.builderError () .setErrorLocation (aArtefact.getRuleResourcePath ()) .setErrorText ("Internal error - no Schematron output created for OIOUBL") .build ()); } break; } default: throw new IllegalStateException ("Unsupported output type"); } } catch (final Exception ex) { // Usually an error in the Schematron aErrorList.add (SingleError.builderError () .setErrorLocation (aArtefact.getRuleResourcePath ()) .setErrorText (ex.getMessage ()) .setLinkedException (ex) .build ()); } // Apply custom levels if (m_aCustomErrorLevels != null && aErrorList.isNotEmpty ()) { final ErrorList aOldErrorList = aErrorList.getClone (); aErrorList.clear (); for (final IError aCurError : aOldErrorList) { final String sErrorID = aCurError.getErrorID (); final IErrorLevel aCustomLevel = m_aCustomErrorLevels.get (sErrorID); if (aCustomLevel != null) { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("Changing error level of '" + sErrorID + "' from " + aCurError.getErrorLevel ().getNumericLevel () + " to " + aCustomLevel + " (" + aCustomLevel.getNumericLevel () + ")"); aErrorList.add (SingleError.builder (aCurError).setErrorLevel (aCustomLevel).build ()); } else { // No change aErrorList.add (aCurError); } } } return new ValidationResult (aArtefact, aErrorList); } @Override public boolean equals (final Object o) { if (o == this) return true; if (!super.equals (o)) return false; final ValidationExecutorSchematron rhs = (ValidationExecutorSchematron) o; return m_bCacheSchematron == rhs.m_bCacheSchematron && EqualsHelper.equals (m_sPrerequisiteXPath, rhs.m_sPrerequisiteXPath) && EqualsHelper.equals (m_aNamespaceContext, rhs.m_aNamespaceContext); } @Override public int hashCode () { return HashCodeGenerator.getDerived (super.hashCode ()) .append (m_bCacheSchematron) .append (m_sPrerequisiteXPath) .append (m_aNamespaceContext) .getHashCode (); } @Override public String toString () { return ToStringGenerator.getDerived (super.toString ()) .append ("CacheSchematron", m_bCacheSchematron) .appendIfNotNull ("PrerequisiteXPath", m_sPrerequisiteXPath) .appendIfNotNull ("NamespaceContext", m_aNamespaceContext) .getToString (); } /** * Create a new instance for a single resource that uses Pure Schematron * validation. * * @param aRes * The resource pointing to the Schematron rules. May not be * null. * @param aNamespaceContext * An optional namespace context for nice error messages. May be * null. * @return A new instance and never null. * @since 6.0.4 */ @Nonnull public static ValidationExecutorSchematron createPure (@Nonnull final IReadableResource aRes, @Nullable final IIterableNamespaceContext aNamespaceContext) { return new ValidationExecutorSchematron (new ValidationArtefact (EValidationType.SCHEMATRON_PURE, aRes), null, aNamespaceContext); } /** * Create a new instance for a single resource that uses the simple Schematron * validation. This is discouraged for speed reasons. It is recommended to * precompile the SCH to XSLT at build time and than use the * {@link #createXSLT(IReadableResource, IIterableNamespaceContext)} instead. * * @param aRes * The resource pointing to the Schematron rules. May not be * null. * @param aNamespaceContext * An optional namespace context for nice error messages. May be * null. * @return A new instance and never null. * @since 6.0.4 */ @Nonnull public static ValidationExecutorSchematron createSCH (@Nonnull final IReadableResource aRes, @Nullable final IIterableNamespaceContext aNamespaceContext) { return new ValidationExecutorSchematron (new ValidationArtefact (EValidationType.SCHEMATRON_SCH, aRes), null, aNamespaceContext); } /** * Create a new instance for a single resource that uses the precompiled XSLT * Schematron validation. * * @param aRes * The resource pointing to the XSLT rules. May not be * null. * @param aNamespaceContext * An optional namespace context for nice error messages. May be * null. * @return A new instance and never null. * @since 6.0.4 */ @Nonnull public static ValidationExecutorSchematron createXSLT (@Nonnull final IReadableResource aRes, @Nullable final IIterableNamespaceContext aNamespaceContext) { return createXSLT (aRes, null, aNamespaceContext); } /** * Create a new instance for a single resource that uses the precompiled XSLT * Schematron validation. * * @param aRes * The resource pointing to the XSLT rules. May not be * null. * @param sPrerequisiteXPath * An optional XPath expression that needs to be fulfilled in the * source document to run this validation rules. This can increase the * execution speed. May be null. * @param aNamespaceContext * An optional namespace context for nice error messages. May be * null. * @return A new instance and never null. * @since 6.0.4 */ @Nonnull public static ValidationExecutorSchematron createXSLT (@Nonnull final IReadableResource aRes, @Nullable final String sPrerequisiteXPath, @Nullable final IIterableNamespaceContext aNamespaceContext) { return new ValidationExecutorSchematron (new ValidationArtefact (EValidationType.SCHEMATRON_XSLT, aRes), sPrerequisiteXPath, aNamespaceContext); } /** * Create a new instance for a single resource that uses the special OIOUBL * Schematron validation. * * @param aRes * The resource pointing to the OIOUBL rules. May not be * null. * @param aNamespaceContext * An optional namespace context for nice error messages. May be * null. * @return A new instance and never null. * @since 6.0.4 */ @Nonnull public static ValidationExecutorSchematron createOIOUBL (@Nonnull final IReadableResource aRes, @Nullable final IIterableNamespaceContext aNamespaceContext) { return new ValidationExecutorSchematron (new ValidationArtefact (EValidationType.SCHEMATRON_OIOUBL, aRes), null, aNamespaceContext); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy