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

com.helger.maven.schematron.Schematron2XSLTMojo Maven / Gradle / Ivy

/*
 * Copyright (C) 2014-2024 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.maven.schematron;

import java.io.File;
import java.io.OutputStream;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.XMLConstants;
import javax.xml.transform.ErrorListener;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.DirectoryScanner;
import org.sonatype.plexus.build.incremental.BuildContext;
import org.w3c.dom.Document;

import com.helger.commons.CGlobal;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.Since;
import com.helger.commons.annotation.VisibleForTesting;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.concurrent.ExecutorServiceHelper;
import com.helger.commons.concurrent.ThreadHelper;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.file.FilenameHelper;
import com.helger.commons.io.resource.FileSystemResource;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.string.StringHelper;
import com.helger.commons.wrapper.Wrapper;
import com.helger.schematron.sch.SchematronProviderXSLTFromSCH;
import com.helger.schematron.sch.TransformerCustomizerSCH;
import com.helger.schematron.svrl.CSVRL;
import com.helger.xml.XMLHelper;
import com.helger.xml.namespace.MapBasedNamespaceContext;
import com.helger.xml.serialize.write.XMLWriter;
import com.helger.xml.serialize.write.XMLWriterSettings;

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

/**
 * Converts one or more Schematron schema files into XSLT scripts.
 *
 * @author PEPPOL.AT, BRZ, Philip Helger
 */
@SuppressFBWarnings ({ "NP_UNWRITTEN_FIELD", "UWF_UNWRITTEN_FIELD" })
@Mojo (name = "convert", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true)
public final class Schematron2XSLTMojo extends AbstractMojo
{
  /**
   * BuildContext for m2e (it's a pass-though straight to the filesystem when
   * invoked from the Maven cli)
   */
  @Component
  private BuildContext buildContext;

  /**
   * The Maven Project.
   */
  @Parameter (defaultValue = "${project}", readonly = true)
  private MavenProject project;

  /**
   * The directory where the Schematron files reside.
   */
  @Parameter (name = "schematronDirectory", defaultValue = "${basedir}/src/main/schematron", required = true)
  private File m_aSchematronDirectory;

  /**
   * A pattern for the Schematron files. Can contain Ant-style wildcards and
   * double wildcards. All files that match the pattern will be converted. Files
   * in the schematronDirectory and its subdirectories will be considered.
   */
  @Parameter (name = "schematronPattern", defaultValue = "**/*.sch", required = true)
  private String m_sSchematronPattern;

  /**
   * The directory where the XSLT files will be saved.
   */
  @Parameter (name = "xsltDirectory", defaultValue = "${basedir}/src/main/xslt", required = true)
  private File m_aXsltDirectory;

  /**
   * The file extension of the created XSLT files.
   */
  @Parameter (name = "xsltExtension", defaultValue = ".xslt", required = true)
  private String m_sXsltExtension;

  /**
   * Overwrite existing Schematron files without notice? If this is set to
   * false than existing XSLT files are not overwritten.
   */
  @Parameter (name = "overwriteWithoutQuestion", defaultValue = "true")
  private boolean m_bOverwriteWithoutQuestion = true;

  /**
   * Define the phase to be used for XSLT creation. By default the
   * defaultPhase attribute of the Schematron file is used.
   */
  @Parameter (name = "phaseName")
  private String m_sPhaseName;

  /**
   * Define the language code for the XSLT creation. Default is English.
   * Supported language codes are: cs, de, en, fr, nl.
   */
  @Parameter (name = "languageCode")
  private String m_sLanguageCode;

  /**
   * Custom attributes to be used for the SCH to XSLT conversion.
   */
  @Parameter (name = "parameters")
  @Since ("5.0.2")
  private Map  m_aCustomParameters;

  /**
   * Force the results to be cached.
   */
  @Parameter (name = "forceCacheResult", defaultValue = "false")
  @Since ("5.2.1")
  private boolean m_bForceCacheResult = TransformerCustomizerSCH.DEFAULT_FORCE_CACHE_RESULT;

  /**
   * A constant header string that should be added to all XSLT files, e.g. as a
   * version number etc.
   */
  @Parameter (name = "xsltHeader")
  @Since ("6.2.2")
  private String m_sXSLTHeader;

  /**
   * If the transformation of a Schematron to XSLT takes longer than 5 seconds,
   * a message is displayed every 5 seconds to inform you that the
   * transformation is still in progress. This is enabled by default.
   */
  @Parameter (name = "showProgress", defaultValue = "true")
  @Since ("6.2.8")
  private boolean m_bShowProgress = true;

  /**
   * If an error occurs, shall the conversion stop or shall the next file be
   * tried? For backwards compatibility reason, this is enabled by default.
   */
  @Parameter (name = "stopOnError", defaultValue = "true")
  @Since ("6.3.2")
  private boolean m_bStopOnError = true;

  public void setSchematronDirectory (@Nonnull final File aDir)
  {
    m_aSchematronDirectory = aDir;
    if (!m_aSchematronDirectory.isAbsolute ())
      m_aSchematronDirectory = new File (project.getBasedir (), aDir.getPath ());
    getLog ().debug ("Searching Schematron files in the directory '" + m_aSchematronDirectory + "'");
  }

  public void setSchematronPattern (@Nonnull final String sPattern)
  {
    m_sSchematronPattern = sPattern;
    getLog ().debug ("Setting Schematron pattern to '" + sPattern + "'");
  }

  public void setXsltDirectory (@Nonnull final File aDir)
  {
    m_aXsltDirectory = aDir;
    if (!m_aXsltDirectory.isAbsolute ())
      m_aXsltDirectory = new File (project.getBasedir (), aDir.getPath ());
    getLog ().debug ("Writing XSLT files into directory '" + m_aXsltDirectory + "'");
  }

  public void setXsltExtension (@Nonnull final String sExt)
  {
    m_sXsltExtension = sExt;
    getLog ().debug ("Setting XSLT file extension to '" + sExt + "'");
  }

  public void setOverwriteWithoutQuestion (final boolean bOverwrite)
  {
    m_bOverwriteWithoutQuestion = bOverwrite;
    if (m_bOverwriteWithoutQuestion)
      getLog ().debug ("Overwriting XSLT files without notice");
    else
      getLog ().debug ("Ignoring existing Schematron files");
  }

  public void setPhaseName (@Nullable final String sPhaseName)
  {
    m_sPhaseName = sPhaseName;
    if (m_sPhaseName == null)
      getLog ().debug ("Using default phase");
    else
      getLog ().debug ("Using the phase '" + m_sPhaseName + "'");
  }

  public void setLanguageCode (@Nullable final String sLanguageCode)
  {
    m_sLanguageCode = sLanguageCode;
    if (m_sLanguageCode == null)
      getLog ().debug ("Using default language code");
    else
      getLog ().debug ("Using the language code '" + m_sLanguageCode + "'");
  }

  public void setParameters (@Nullable final Map  aParameters)
  {
    m_aCustomParameters = aParameters;
    if (m_aCustomParameters == null || m_aCustomParameters.isEmpty ())
      getLog ().debug ("Using no custom parameters");
    else
      getLog ().debug ("Using custom parameters " + m_aCustomParameters.toString ());
  }

  @Nonnull
  @ReturnsMutableCopy
  @VisibleForTesting
  ICommonsMap  getParameters ()
  {
    return new CommonsHashMap <> (m_aCustomParameters);
  }

  public void setForceCacheResult (final boolean bForceCacheResult)
  {
    m_bForceCacheResult = bForceCacheResult;
    if (m_bForceCacheResult)
      getLog ().debug ("Results are forcebly cached");
    else
      getLog ().debug ("Results not not forcebly cached");
  }

  public void setXsltHeader (final String s)
  {
    m_sXSLTHeader = s;
    if (StringHelper.hasText (m_sXSLTHeader))
      getLog ().debug ("Using the XSLT header '" + m_sXSLTHeader + "'");
    else
      getLog ().debug ("No XSLT header is configured");
  }

  public void setShowProgress (final boolean b)
  {
    m_bShowProgress = b;
    if (b)
      getLog ().debug ("Progress indicator is enabled");
    else
      getLog ().debug ("Progress indicator is disabled");
  }

  public void setStopOnError (final boolean b)
  {
    m_bStopOnError = b;
    if (b)
      getLog ().debug ("Stop on error is enabled");
    else
      getLog ().debug ("Stop on error is disabled");
  }

  public void execute () throws MojoExecutionException, MojoFailureException
  {
    if (m_aSchematronDirectory == null)
      throw new MojoExecutionException ("No Schematron directory specified!");
    if (m_aSchematronDirectory.exists () && !m_aSchematronDirectory.isDirectory ())
      throw new MojoExecutionException ("The specified Schematron directory " +
                                        m_aSchematronDirectory +
                                        " is not a directory!");
    if (StringHelper.hasNoText (m_sSchematronPattern))
      throw new MojoExecutionException ("No Schematron pattern specified!");
    if (m_aXsltDirectory == null)
      throw new MojoExecutionException ("No XSLT directory specified!");
    if (m_aXsltDirectory.exists () && !m_aXsltDirectory.isDirectory ())
      throw new MojoExecutionException ("The specified XSLT directory " + m_aXsltDirectory + " is not a directory!");
    if (StringHelper.hasNoText (m_sXsltExtension) || !m_sXsltExtension.startsWith ("."))
      throw new MojoExecutionException ("The XSLT extension '" + m_sXsltExtension + "' is invalid!");

    if (!m_aXsltDirectory.exists () && !m_aXsltDirectory.mkdirs ())
      throw new MojoExecutionException ("Failed to create the XSLT directory " + m_aXsltDirectory);

    // for all Schematron files that match the pattern
    final DirectoryScanner aScanner = new DirectoryScanner ();
    aScanner.setBasedir (m_aSchematronDirectory);
    aScanner.setIncludes (new String [] { m_sSchematronPattern });
    aScanner.setCaseSensitive (true);
    aScanner.scan ();
    final String [] aFilenames = aScanner.getIncludedFiles ();
    if (aFilenames != null)
    {
      for (final String sFilename : aFilenames)
      {
        final File aFile = new File (m_aSchematronDirectory, sFilename);

        // 1. build XSLT file name (outputdir + localpath with new extension)
        final File aXSLTFile = new File (m_aXsltDirectory,
                                         FilenameHelper.getWithoutExtension (sFilename) + m_sXsltExtension);

        getLog ().info ("Converting Schematron file '" +
                        aFile.getPath () +
                        "' to XSLT file '" +
                        aXSLTFile.getPath () +
                        "'");

        // 2. The Schematron resource
        final IReadableResource aSchematronResource = new FileSystemResource (aFile);

        // 3. Check if the XSLT file already exists
        if (aXSLTFile.exists () && !m_bOverwriteWithoutQuestion)
        {
          // 3.1 Not overwriting the existing file
          getLog ().debug ("Skipping XSLT file '" + aXSLTFile.getPath () + "' because it already exists!");
        }
        else
        {
          // 3.2 Create the directory, if necessary
          final File aXsltFileDirectory = aXSLTFile.getParentFile ();
          if (aXsltFileDirectory != null && !aXsltFileDirectory.exists ())
          {
            getLog ().debug ("Creating directory '" + aXsltFileDirectory.getPath () + "'");
            if (!aXsltFileDirectory.mkdirs ())
            {
              final String sMessage = "Failed to convert '" +
                                      aFile.getPath () +
                                      "' because directory '" +
                                      aXsltFileDirectory.getPath () +
                                      "' could not be created";
              getLog ().error (sMessage);
              throw new MojoFailureException (sMessage);
            }
          }

          // 3.3 The main SCH to XSLT conversion
          final Wrapper  fe = new Wrapper <> ();
          final Wrapper  ee = new Wrapper <> ();
          final Runnable r = () -> {
            try
            {
              buildContext.removeMessages (aFile);
              // Custom error listener to log to the Mojo logger
              final ErrorListener aMojoErrorListener = new PluginErrorListener (buildContext, aFile);

              // Custom error listener
              // No custom URI resolver
              // Specified phase - default = null
              // Specified language code - default = null
              final TransformerCustomizerSCH aCustomizer = new TransformerCustomizerSCH ().setErrorListener (aMojoErrorListener)
                                                                                          .setPhase (m_sPhaseName)
                                                                                          .setLanguageCode (m_sLanguageCode)
                                                                                          .setParameters (m_aCustomParameters)
                                                                                          .setForceCacheResult (m_bForceCacheResult);

              getLog ().debug ("Compiling Schematron instance " + aSchematronResource.toString ());

              final Document aXsltDoc = SchematronProviderXSLTFromSCH.createSchematronXSLT (aSchematronResource,
                                                                                            aCustomizer);
              if (aXsltDoc != null)
              {
                if (StringHelper.hasText (m_sXSLTHeader))
                {
                  // Inject the header into the XSLT
                  aXsltDoc.insertBefore (aXsltDoc.createComment (m_sXSLTHeader), aXsltDoc.getDocumentElement ());
                }

                // Write the resulting XSLT file to disk
                final MapBasedNamespaceContext aNSContext = new MapBasedNamespaceContext ().addMapping ("svrl",
                                                                                                        CSVRL.SVRL_NAMESPACE_URI);
                // Add all namespaces from XSLT document root
                final String sNSPrefix = XMLConstants.XMLNS_ATTRIBUTE + ":";
                XMLHelper.forAllAttributes (aXsltDoc.getDocumentElement (), (sAttrName, sAttrValue) -> {
                  if (sAttrName.startsWith (sNSPrefix))
                    aNSContext.addMapping (sAttrName.substring (sNSPrefix.length ()), sAttrValue);
                });

                final XMLWriterSettings aXWS = new XMLWriterSettings ().setNamespaceContext (aNSContext)
                                                                       .setPutNamespaceContextPrefixesInRoot (true);

                final OutputStream aOS = FileHelper.getOutputStream (aXSLTFile);
                if (aOS == null)
                  throw new IllegalStateException ("Failed to open output stream for file " +
                                                   aXSLTFile.getAbsolutePath ());
                XMLWriter.writeToStream (aXsltDoc, aOS, aXWS);

                getLog ().debug ("Finished creating XSLT file '" + aXSLTFile.getPath () + "'");

                buildContext.refresh (aXsltFileDirectory);
              }
              else
              {
                final String message = "Failed to convert '" +
                                       aFile.getPath () +
                                       "': the Schematron resource is invalid";
                getLog ().error (message);
                throw new MojoFailureException (message);
              }
            }
            catch (final MojoFailureException up)
            {
              fe.set (up);
            }
            catch (final Exception ex)
            {
              final String sMessage = "Failed to convert '" +
                                      aFile.getPath () +
                                      "' to XSLT file '" +
                                      aXSLTFile.getPath () +
                                      "'";
              getLog ().error (sMessage, ex);
              ee.set (new MojoExecutionException (sMessage, ex));
            }
          };

          if (m_bShowProgress)
          {
            // Run conversion in one thread and run another thread that logs the
            // time
            final ExecutorService aES = Executors.newSingleThreadExecutor ();
            aES.submit (r);

            final long nStartTime = System.currentTimeMillis ();
            final AtomicBoolean aLoggedAnything = new AtomicBoolean (false);
            final Thread t = new Thread ( () -> {
              long nLastSecs = 0;
              while (!Thread.currentThread ().isInterrupted ())
              {
                if (ThreadHelper.sleep (500).isSuccess ())
                {
                  final long nDurationSecs = (System.currentTimeMillis () - nStartTime) /
                                             CGlobal.MILLISECONDS_PER_SECOND;
                  // Log only every x seconds
                  if (nDurationSecs >= nLastSecs + 5)
                  {
                    getLog ().info ("Schematron conversion of '" +
                                    aFile.getName () +
                                    "' already takes " +
                                    nDurationSecs +
                                    " seconds - please wait...");
                    nLastSecs = nDurationSecs;
                    aLoggedAnything.set (true);
                  }
                }
              }
            });
            t.setDaemon (true);
            t.start ();
            ExecutorServiceHelper.shutdownAndWaitUntilAllTasksAreFinished (aES);
            t.interrupt ();
            if (aLoggedAnything.get ())
            {
              // Log finalization of conversion if it took longer
              final long nDurationSecs = (System.currentTimeMillis () - nStartTime) / CGlobal.MILLISECONDS_PER_SECOND;
              getLog ().info ("Schematron conversion of '" +
                              aFile.getName () +
                              "' was finalized after " +
                              nDurationSecs +
                              " seconds");
            }
          }
          else
          {
            // No progress version
            r.run ();
          }

          // Did any exceptions occur?
          if (fe.isSet ())
          {
            if (m_bStopOnError)
              throw fe.get ();
            getLog ().warn ("Received an error, but continuing anyway", fe.get ());
          }
          if (ee.isSet ())
          {
            if (m_bStopOnError)
              throw ee.get ();
            getLog ().warn ("Received an error, but continuing anyway", ee.get ());
          }
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy