Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/**
* 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);
}
}