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

com.helger.schematron.ant.Schematron Maven / Gradle / Ivy

There is a newer version: 5.6.5
Show newest version
/**
 * Copyright (C) 2017-2019 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.ant;

import java.io.File;
import java.io.Serializable;
import java.util.Locale;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.transform.URIResolver;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.types.resources.FileProvider;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.util.ResourceUtils;
import org.oclc.purl.dsdl.svrl.SchematronOutputType;
import org.xml.sax.EntityResolver;

import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.annotation.UsedViaReflection;
import com.helger.commons.collection.attr.IStringMap;
import com.helger.commons.collection.attr.StringMap;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.error.ErrorTextProvider;
import com.helger.commons.error.IError;
import com.helger.commons.error.level.EErrorLevel;
import com.helger.commons.error.level.IErrorLevel;
import com.helger.commons.error.list.IErrorList;
import com.helger.commons.io.file.FileOperations;
import com.helger.commons.io.resource.FileSystemResource;
import com.helger.commons.string.StringHelper;
import com.helger.schematron.ESchematronMode;
import com.helger.schematron.ISchematronResource;
import com.helger.schematron.pure.SchematronResourcePure;
import com.helger.schematron.pure.errorhandler.CollectingPSErrorHandler;
import com.helger.schematron.svrl.AbstractSVRLMessage;
import com.helger.schematron.svrl.DefaultSVRLErrorLevelDeterminator;
import com.helger.schematron.svrl.SVRLHelper;
import com.helger.schematron.svrl.SVRLMarshaller;
import com.helger.schematron.svrl.SVRLResourceError;
import com.helger.schematron.xslt.SchematronResourceSCH;
import com.helger.schematron.xslt.SchematronResourceXSLT;
import com.helger.xml.transform.CollectingTransformErrorListener;
import com.helger.xml.transform.TransformSourceFactory;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * ANT task to perform Schematron validation.
 *
 * @author Philip Helger
 * @since 4.2.3
 */
@SuppressFBWarnings ("DMI_HARDCODED_ABSOLUTE_FILENAME")
public class Schematron extends Task
{
  /**
   * Custom role value that triggers an error.
   *
   * @author Philip Helger
   * @since 5.0.2
   */
  public static class ErrorRole implements Serializable
  {
    private String m_sRole;

    public ErrorRole ()
    {}

    @UsedViaReflection
    public void setRole (@Nullable final String sRole)
    {
      m_sRole = sRole;
    }

    @Nullable
    public String getRole ()
    {
      return m_sRole;
    }

    public boolean equalsIgnoreCase (@Nonnull final String sValue)
    {
      return sValue.equalsIgnoreCase (m_sRole);
    }
  }

  /**
   * Custom parameter for SCH/XSLT transformations only.
   *
   * @author Philip Helger
   * @since 5.0.6
   */
  public static class Parameter implements Serializable
  {
    private String m_sName;
    private String m_sValue;

    public Parameter ()
    {}

    @UsedViaReflection
    public void setName (@Nullable final String sName)
    {
      m_sName = sName;
    }

    @Nullable
    public String getName ()
    {
      return m_sName;
    }

    @UsedViaReflection
    public void setValue (@Nullable final String sValue)
    {
      m_sValue = sValue;
    }

    @Nullable
    public String getValue ()
    {
      return m_sValue;
    }

    void addToMap (@Nonnull final Map  aMap)
    {
      // Only add parameters that have a name
      // If the value is null it becomes ""
      if (StringHelper.hasText (m_sName))
        aMap.put (m_sName, StringHelper.getNotNull (m_sValue));
    }
  }

  /**
   * The Schematron file. This may also be an XSLT file if it is precompiled.
   */
  private File m_aSchematronFile;

  /**
   * The processing engine to use. Can be one of the following:
   * 
    *
  • pure - for SCH files
  • *
  • schematron - for SCH files that will be converted to XSLT and applied * from there.
  • *
  • xslt - apply pre-build XSLT files
  • *
*/ private ESchematronMode m_eSchematronProcessingEngine = ESchematronMode.SCHEMATRON; /** * The collection for resources (like FileSets etc.) which are to be * validated. */ private final ICommonsList m_aResCollections = new CommonsArrayList <> (); /** * The SVRL path to write to. The filenames are based on the source XML * filenames. */ private File m_aSvrlDirectory; /** * Define the phase to be used for Schematron validation. By default the * defaultPhase attribute of the Schematron file is used. This * phase name is only used if the processing engine pure or * schematron are used. */ private String m_sPhaseName; /** * Define the language code to be used for Schematron validation. Default is * English. Supported language codes are: cs, de, en, fr, nl. */ private String m_sLanguageCode; /** * true if the XMLs are supposed to be valid, false * otherwise. Defaults to true. */ private boolean m_bExpectSuccess = true; /** * List of "role" attribute values that will trigger an error. If combined * with "failOnError" it will break the build. */ private final ICommonsList m_aErrorRoles = new CommonsArrayList <> (); /** * true if the build should fail if any error occurs. Defaults to * true. Since v5.0.0. */ private boolean m_bFailOnError = true; /** * true if the build should fail if any validation "error" * occurs. Defaults to false. Since v5.0.11. */ private boolean m_bFailOnValidationError = false; /** * true if the build should fail if any validation "warnings" * occurs. Defaults to false. Since v5.0.11. */ private boolean m_bFailOnValidationWarn = false; /** * true if the build should fail if any validation "information" * occurs. Defaults to false. Since v5.0.11. */ private boolean m_bFailOnValidationInfo = false; /** * For resolving entities such as DTDs. This is used both for the Schematron * file as well as for the XML files to be validated. */ private final XMLCatalog m_aXmlCatalog = new XMLCatalog (); /** * Custom parameters for SCH/XSLT version. */ private final ICommonsList m_aParameters = new CommonsArrayList <> (); private void _debug (@Nonnull final String sMsg) { log (sMsg, Project.MSG_DEBUG); } private void _info (@Nonnull final String sMsg) { log (sMsg, Project.MSG_INFO); } private void _warn (@Nonnull final String sMsg) { _warn (sMsg, null); } private void _warn (@Nonnull final String sMsg, @Nullable final Throwable t) { log (sMsg, t, Project.MSG_WARN); } private void _error (@Nonnull final String sMsg) { _error (sMsg, null); } private void _error (@Nonnull final String sMsg, @Nullable final Throwable t) { log (sMsg, t, Project.MSG_ERR); } private void _errorOrFail (@Nonnull final String sMsg) { _errorOrFail (sMsg, null); } private void _errorOrFail (@Nonnull final String sMsg, @Nullable final Throwable t) { if (m_bFailOnError) throw new BuildException (sMsg, t); _error (sMsg, t); } public Schematron () {} public void setSchematronFile (@Nonnull final File aFile) { m_aSchematronFile = aFile; if (!m_aSchematronFile.isAbsolute ()) m_aSchematronFile = new File (getProject ().getBaseDir (), aFile.getPath ()); _debug ("Using Schematron file '" + m_aSchematronFile + "'"); } public void setSchematronProcessingEngine (@Nullable final String sEngine) { m_eSchematronProcessingEngine = ESchematronMode.getFromIDOrNull (sEngine); _debug ("Schematron processing mode set to '" + m_eSchematronProcessingEngine + "'"); } /** * Add a collection of files to copy. * * @param aResCollection * a resource collection to copy. * @since Ant 1.7 */ public void add (final ResourceCollection aResCollection) { m_aResCollections.add (aResCollection); } public void setSvrlDirectory (@Nonnull final File aDir) { m_aSvrlDirectory = aDir; if (!m_aSvrlDirectory.isAbsolute ()) m_aSvrlDirectory = new File (getProject ().getBaseDir (), aDir.getPath ()); _debug ("Writing SVRL files to directory '" + m_aSvrlDirectory + "'"); } public void setPhaseName (@Nullable final String sPhaseName) { m_sPhaseName = sPhaseName; if (m_sPhaseName == null) _debug ("Using default phase"); else _debug ("Using the phase '" + m_sPhaseName + "'"); } public void setLanguageCode (@Nullable final String sLanguageCode) { m_sLanguageCode = sLanguageCode; if (m_sLanguageCode == null) _debug ("Using default language code"); else _debug ("Using the language code '" + m_sLanguageCode + "'"); } public void setExpectSuccess (final boolean bExpectSuccess) { m_bExpectSuccess = bExpectSuccess; _debug ("Expecting that XML files " + (bExpectSuccess ? "conform" : "do not conform") + " to the provided Schematron file"); } @Nonnull public Schematron.ErrorRole createErrorRole () { final Schematron.ErrorRole aErrorRole = new Schematron.ErrorRole (); m_aErrorRoles.add (aErrorRole); return aErrorRole; } public void setFailOnError (final boolean bFailOnError) { m_bFailOnError = bFailOnError; _debug (bFailOnError ? "Will fail on error" : "Will not fail on error"); } public void setFailOnValidationError (final boolean bFail) { m_bFailOnValidationError = bFail; _debug (bFail ? "Will fail on validation error" : "Will not fail on validation error"); } public void setFailOnValidationWarn (final boolean bFail) { m_bFailOnValidationWarn = bFail; _debug (bFail ? "Will fail on validation warning" : "Will not fail on validation warning"); } public void setFailOnValidationInfo (final boolean bFail) { m_bFailOnValidationInfo = bFail; _debug (bFail ? "Will fail on validation information" : "Will not fail on validation information"); } /** * Add the catalog to our internal catalog * * @param aXmlCatalog * the XMLCatalog instance to use to look up DTDs */ public void addConfiguredXMLCatalog (@Nonnull final XMLCatalog aXmlCatalog) { m_aXmlCatalog.addConfiguredXMLCatalog (aXmlCatalog); _debug ("Added XMLCatalog " + aXmlCatalog); } @Nonnull public Schematron.Parameter createParameter () { final Schematron.Parameter aParameter = new Schematron.Parameter (); m_aParameters.add (aParameter); return aParameter; } /** * Get the {@link EntityResolver} to be used. * * @return Never null. */ @Nonnull @OverrideOnDemand protected EntityResolver getEntityResolver () { return m_aXmlCatalog; } /** * Get the {@link URIResolver} to be used. * * @return Never null. */ @Nonnull @OverrideOnDemand protected URIResolver getURIResolver () { return m_aXmlCatalog; } @Override public void init () throws BuildException { super.init (); m_aXmlCatalog.setProject (getProject ()); } private static final File NULL_FILE_PLACEHOLDER = new File ("/dummy_NULL"); @Nonnull private static File _getKeyFile (@Nullable final File f) { return f != null ? f : NULL_FILE_PLACEHOLDER; } private void _performValidation (@Nonnull final ISchematronResource aSch, @Nonnull final ICommonsList aResCollections, @Nullable final File aSVRLDirectory, final boolean bExpectSuccess) throws BuildException { // Resolve resourceCollections - pain in the ass final ICommonsMap aFiles = new CommonsHashMap <> (); for (final ResourceCollection aResCollection : aResCollections) { if (!aResCollection.isFilesystemOnly ()) _errorOrFail ("Only FileSystem resources are supported."); else for (final Resource aRes : aResCollection) { if (!aRes.isExists ()) { _errorOrFail ("Could not find resource " + aRes.toLongString () + " to copy."); continue; } File aBaseDir = NULL_FILE_PLACEHOLDER; String sName = aRes.getName (); final FileProvider aFP = aRes.as (FileProvider.class); if (aFP != null) { final FileResource aFR = ResourceUtils.asFileResource (aFP); aBaseDir = _getKeyFile (aFR.getBaseDir ()); if (aBaseDir == NULL_FILE_PLACEHOLDER) sName = aFR.getFile ().getAbsolutePath (); } if ((aRes.isDirectory () || aFP != null) && sName != null) { final DirectoryData aBaseDirData = aFiles.computeIfAbsent (_getKeyFile (aBaseDir), DirectoryData::new); if (aRes.isDirectory ()) aBaseDirData.addDir (sName); else aBaseDirData.addFile (sName); } else _errorOrFail ("Could not resolve resource " + aRes.toLongString () + " to a file."); } } for (final DirectoryData aBaseDirData : aFiles.values ()) { _debug ("Scanning directory " + aBaseDirData.getBaseDir () + " for XMLs to be Schematron validated"); final ICommonsList aIncludes = new CommonsArrayList <> (); aIncludes.addAll (aBaseDirData.getFiles ()); for (final String sFile : aBaseDirData.getDirs ()) aIncludes.add (sFile + "/**"); final DirectoryScanner aScanner = new DirectoryScanner (); aScanner.setBasedir (aBaseDirData.getBaseDir ()); if (aIncludes.isNotEmpty ()) aScanner.setIncludes (aIncludes.toArray (new String [0])); aScanner.setCaseSensitive (true); aScanner.scan (); final String [] aXMLFilenames = aScanner.getIncludedFiles (); if (aXMLFilenames != null) { for (final String sXMLFilename : aXMLFilenames) { final File aXMLFile = new File (aBaseDirData.getBaseDir (), sXMLFilename); // Validate XML file _info ("Validating XML file '" + aXMLFile.getPath () + "' against Schematron rules from '" + m_aSchematronFile.getName () + "' expecting " + (bExpectSuccess ? "success" : "failure")); try { // This is performing the validation final SchematronOutputType aSOT = aSch.applySchematronValidationToSVRL (TransformSourceFactory.create (aXMLFile)); // If aSOT == null a different error should be present if (aSVRLDirectory != null && aSOT != null) { // Save SVRL final File aSVRLFile = new File (aSVRLDirectory, sXMLFilename + ".svrl"); if (FileOperations.createDirIfNotExisting (aSVRLFile.getParentFile ()).isFailure ()) _error ("Failed to create parent directory of '" + aSVRLFile.getAbsolutePath () + "'!"); if (new SVRLMarshaller ().write (aSOT, aSVRLFile).isSuccess ()) _info ("Successfully saved SVRL file '" + aSVRLFile.getPath () + "'"); else _error ("Error saving SVRL file '" + aSVRLFile.getPath () + "'"); } _debug ("Created SVRL:\n" + new SVRLMarshaller ().getAsString (aSOT)); final ICommonsList aMessages = SVRLHelper.getAllFailedAssertionsAndSuccessfulReports (aSOT); final int nErrorMessages = aMessages.getCount (x -> x.getFlag ().isGT (EErrorLevel.WARN)); final int nWarningMessages = aMessages.getCount (x -> x.getFlag ().isEQ (EErrorLevel.WARN)); final int nInfoMessages = aMessages.getCount (x -> x.getFlag ().isLT (EErrorLevel.WARN)); final String sErrors = nErrorMessages + " Schematron error" + (nErrorMessages == 1 ? "" : "s"); final String sWarnings = nWarningMessages + " Schematron warning" + (nWarningMessages == 1 ? "" : "s"); // No plural - haha final String sInfos = nInfoMessages + " Schematron information"; final boolean bExpectationFulfilled; if (bExpectSuccess) { // No failed assertions expected bExpectationFulfilled = nErrorMessages == 0; if (bExpectationFulfilled) { // Success as expected _info ("XML file '" + aXMLFile.getPath () + "' was validated against Schematron '" + aSch.getResource ().getPath () + "' and matches the rules" + (nWarningMessages > 0 ? " (" + sWarnings + (nWarningMessages == 1 ? " is" : " are") + " contained)" : "") + (nInfoMessages > 0 ? " (" + sInfos + (nInfoMessages == 1 ? " is" : " are") + " contained)" : "")); } else { _error (sErrors + (nWarningMessages > 0 ? " and " + sWarnings : "") + (nInfoMessages > 0 ? " and " + sInfos : "") + " for XML file '" + aXMLFile.getPath () + "'"); } } else { // At least one failed assertions expected bExpectationFulfilled = nErrorMessages > 0; if (bExpectationFulfilled) { // Errors as expected _info ("XML file '" + aXMLFile.getPath () + "' was validated against Schematron '" + aSch.getResource ().getPath () + "' - " + sErrors + (nWarningMessages > 0 ? " and " + sWarnings : "") + (nInfoMessages > 0 ? " and " + sInfos : "") + (nErrorMessages == 1 && (nWarningMessages + nInfoMessages) == 0 ? " was" : " were") + " found (as expected)"); } else { _error ("No Schematron errors for erroneous XML file '" + aXMLFile.getPath () + "'" + (nWarningMessages > 0 ? " (" + sWarnings + (nWarningMessages == 1 ? " is" : " are") + " contained)" : "") + (nInfoMessages > 0 ? " (" + sInfos + (nInfoMessages == 1 ? " is" : " are") + " contained)" : "")); } } // List details for (final AbstractSVRLMessage aMessage : aMessages) { final SVRLResourceError aResError = aMessage.getAsResourceError (aXMLFile.getPath ()); final String sText = ErrorTextProvider.DEFAULT.getErrorText (aResError, Locale.US); if (aMessage.getFlag ().isGE (EErrorLevel.ERROR)) _error (sText); else if (aMessage.getFlag ().isGE (EErrorLevel.WARN)) _warn (sText); else _info (sText); } if (!bExpectationFulfilled) _errorOrFail ("The expectations were not fullfilled, therefore the overall result is negative"); if (nErrorMessages > 0 && m_bFailOnValidationError) throw new BuildException ("Validation errors are present."); if (nWarningMessages > 0 && m_bFailOnValidationWarn) throw new BuildException ("Validation warnings are present."); if (nInfoMessages > 0 && m_bFailOnValidationInfo) throw new BuildException ("Validation information are present."); } catch (final BuildException up) { throw up; } catch (final Exception ex) { final String sMessage = "Exception validating XML '" + aXMLFile.getPath () + "' against Schematron rules from '" + m_aSchematronFile.getName () + "'. Technical details: " + ex.getClass ().getSimpleName () + " - " + ex.getMessage (); _errorOrFail (sMessage, ex); } } } } } @Override public void execute () throws BuildException { boolean bCanRun = false; if (m_aSchematronFile == null) _errorOrFail ("No Schematron file specified!"); else if (m_aSchematronFile.exists () && !m_aSchematronFile.isFile ()) _errorOrFail ("The specified Schematron file " + m_aSchematronFile + " is not a file!"); else if (m_eSchematronProcessingEngine == null) _errorOrFail ("An invalid Schematron processing instance is specified! Only one of the following values is allowed: " + StringHelper.getImplodedMapped (", ", ESchematronMode.values (), x -> "'" + x.getID () + "'")); else if (m_aResCollections.isEmpty ()) _errorOrFail ("No XML resources to be validated specified! Add e.g. a element."); else if (m_aSvrlDirectory != null && !m_aSvrlDirectory.exists () && !m_aSvrlDirectory.mkdirs ()) _errorOrFail ("Failed to create the SVRL directory " + m_aSvrlDirectory); else bCanRun = true; if (bCanRun) { // Set error level if (m_aErrorRoles.isNotEmpty ()) { // Set global default error level determinator SVRLHelper.setErrorLevelDeterminator (new DefaultSVRLErrorLevelDeterminator () { @Override @Nonnull public IErrorLevel getErrorLevelFromString (@Nullable final String sFlag) { if (sFlag != null) { // Check custom error roles; #66 for (final Schematron.ErrorRole aCustomRole : m_aErrorRoles) if (aCustomRole.equalsIgnoreCase (sFlag)) return EErrorLevel.ERROR; } // Fall back to default return super.getErrorLevelFromString (sFlag); } }); } // 1. Parse Schematron file final Locale aDisplayLocale = Locale.US; ISchematronResource aSch = null; IErrorList aSCHErrors = null; switch (m_eSchematronProcessingEngine) { case PURE: { // pure final CollectingPSErrorHandler aErrorHdl = new CollectingPSErrorHandler (); final SchematronResourcePure aRealSCH = new SchematronResourcePure (new FileSystemResource (m_aSchematronFile)); aRealSCH.setPhase (m_sPhaseName); aRealSCH.setErrorHandler (aErrorHdl); aRealSCH.setEntityResolver (getEntityResolver ()); aRealSCH.validateCompletely (); aSch = aRealSCH; aSCHErrors = aErrorHdl.getAllErrors (); break; } case SCHEMATRON: { // SCH final IStringMap aParams = new StringMap (); m_aParameters.forEach (x -> x.addToMap (aParams)); if (aParams.isNotEmpty ()) _info ("Using the following custom parameters: " + aParams); final CollectingTransformErrorListener aErrorHdl = new CollectingTransformErrorListener (); final SchematronResourceSCH aRealSCH = new SchematronResourceSCH (new FileSystemResource (m_aSchematronFile)); aRealSCH.setPhase (m_sPhaseName); aRealSCH.setLanguageCode (m_sLanguageCode); aRealSCH.setErrorListener (aErrorHdl); aRealSCH.setURIResolver (getURIResolver ()); aRealSCH.setEntityResolver (getEntityResolver ()); aRealSCH.parameters ().setAll (aParams); aRealSCH.isValidSchematron (); aSch = aRealSCH; aSCHErrors = aErrorHdl.getErrorList (); break; } case XSLT: { // XSLT final IStringMap aParams = new StringMap (); m_aParameters.forEach (x -> x.addToMap (aParams)); if (aParams.isNotEmpty ()) _info ("Using the following custom parameters: " + aParams); final CollectingTransformErrorListener aErrorHdl = new CollectingTransformErrorListener (); final SchematronResourceXSLT aRealSCH = new SchematronResourceXSLT (new FileSystemResource (m_aSchematronFile)); // phase and language are ignored because this was decided when the // XSLT was created aRealSCH.setErrorListener (aErrorHdl); aRealSCH.setURIResolver (getURIResolver ()); aRealSCH.setEntityResolver (getEntityResolver ()); aRealSCH.parameters ().setAll (aParams); aRealSCH.isValidSchematron (); aSch = aRealSCH; aSCHErrors = aErrorHdl.getErrorList (); break; } default: _errorOrFail ("No handler for processing engine '" + m_eSchematronProcessingEngine + "'"); break; } if (aSCHErrors != null) { // Error validating the Schematrons!! boolean bAnyParsingError = false; for (final IError aError : aSCHErrors) if (aError.getErrorLevel ().isGE (EErrorLevel.ERROR)) { _error ("Error in Schematron: " + aError.getAsString (aDisplayLocale)); bAnyParsingError = true; } else if (aError.getErrorLevel ().isGE (EErrorLevel.WARN)) _warn ("Warning in Schematron: " + aError.getAsString (aDisplayLocale)); else _info ("Information in Schematron: " + aError.getAsString (aDisplayLocale)); if (bAnyParsingError) _errorOrFail ("The provided Schematron file contains errors. See log for details."); else { // Start validation _info ("Successfully parsed Schematron file '" + m_aSchematronFile.getPath () + "'"); // 2. for all XML files that match the pattern _performValidation (aSch, m_aResCollections, m_aSvrlDirectory, m_bExpectSuccess); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy