/**
 * 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.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 <String, String> 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 <ResourceCollection> 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 <Schematron.ErrorRole> 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;

  /**
   * 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 <Schematron.Parameter> m_aParameters = new CommonsArrayList <> ();

  public Schematron ()
  {}

  public void setSchematronFile (@Nonnull final File aFile)
  {
    m_aSchematronFile = aFile;
    if (!m_aSchematronFile.isAbsolute ())
      m_aSchematronFile = new File (getProject ().getBaseDir (), aFile.getPath ());
    log ("Using Schematron file '" + m_aSchematronFile + "'", Project.MSG_DEBUG);
  }

  public void setSchematronProcessingEngine (@Nullable final String sEngine)
  {
    m_eSchematronProcessingEngine = ESchematronMode.getFromIDOrNull (sEngine);
    log ("Schematron processing mode set to '" + m_eSchematronProcessingEngine + "'", Project.MSG_DEBUG);
  }

  /**
   * 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 ());
    log ("Writing SVRL files to directory '" + m_aSvrlDirectory + "'", Project.MSG_DEBUG);
  }

  public void setPhaseName (@Nullable final String sPhaseName)
  {
    m_sPhaseName = sPhaseName;

    if (m_sPhaseName == null)
      log ("Using default phase", Project.MSG_DEBUG);
    else
      log ("Using the phase '" + m_sPhaseName + "'", Project.MSG_DEBUG);
  }

  public void setLanguageCode (@Nullable final String sLanguageCode)
  {
    m_sLanguageCode = sLanguageCode;

    if (m_sLanguageCode == null)
      log ("Using default language code", Project.MSG_DEBUG);
    else
      log ("Using the language code '" + m_sLanguageCode + "'", Project.MSG_DEBUG);
  }

  public void setExpectSuccess (final boolean bExpectSuccess)
  {
    m_bExpectSuccess = bExpectSuccess;

    log ("Expecting that XML files " +
         (bExpectSuccess ? "conform" : "do not conform") +
         " to the provided Schematron file",
         Project.MSG_DEBUG);
  }

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

    log (bFailOnError ? "Will fail on error" : "Will not fail on error", Project.MSG_DEBUG);
  }

  /**
   * 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);
    log ("Added XMLCatalog " + aXmlCatalog, Project.MSG_DEBUG);
  }

  @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 _error (@Nonnull final String sMsg)
  {
    _error (sMsg, null);
  }

  private void _error (@Nonnull final String sMsg, @Nullable final Throwable t)
  {
    if (m_bFailOnError)
      throw new BuildException (sMsg, t);
    log (sMsg, t, Project.MSG_ERR);
  }

  private void _performValidation (@Nonnull final ISchematronResource aSch,
                                   @Nonnull final ICommonsList <ResourceCollection> aResCollections,
                                   @Nullable final File aSVRLDirectory,
                                   final boolean bExpectSuccess) throws BuildException
  {
    // Resolve resourceCollections - pain in the ass
    final ICommonsMap <File, DirectoryData> aFiles = new CommonsHashMap <> ();
    for (final ResourceCollection aResCollection : aResCollections)
    {
      if (!aResCollection.isFilesystemOnly ())
        _error ("Only FileSystem resources are supported.");
      else
        for (final Resource aRes : aResCollection)
        {
          if (!aRes.isExists ())
          {
            _error ("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
            _error ("Could not resolve resource " + aRes.toLongString () + " to a file.");
        }
    }

    for (final DirectoryData aBaseDirData : aFiles.values ())
    {
      log ("Scanning directory " + aBaseDirData.getBaseDir () + " for XMLs to be Schematron validated",
           Project.MSG_DEBUG);

      final ICommonsList <String> 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
          log ("Validating XML file '" +
               aXMLFile.getPath () +
               "' against Schematron rules from '" +
               m_aSchematronFile.getName () +
               "' expecting " +
               (bExpectSuccess ? "success" : "failure"),
               Project.MSG_INFO);
          try
          {
            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 ())
                log ("Failed to create parent directory of '" + aSVRLFile.getAbsolutePath () + "'!", Project.MSG_ERR);

              if (new SVRLMarshaller ().write (aSOT, aSVRLFile).isSuccess ())
                log ("Successfully saved SVRL file '" + aSVRLFile.getPath () + "'", Project.MSG_INFO);
              else
                log ("Error saving SVRL file '" + aSVRLFile.getPath () + "'", Project.MSG_ERR);
            }

            if (false)
              System.out.println (new SVRLMarshaller ().getAsString (aSOT));

            final ICommonsList <AbstractSVRLMessage> aMessages = SVRLHelper.getAllFailedAssertionsAndSuccessfulReports (aSOT);
            final int nErrorMessages = aMessages.getCount (x -> x.getFlag ().isError ());
            final int nWarningMessages = aMessages.size () - nErrorMessages;
            final String sErrors = nErrorMessages + " Schematron error" + (nErrorMessages == 1 ? "" : "s");
            final String sWarnings = nWarningMessages + " Schematron warning" + (nWarningMessages == 1 ? "" : "s");

            if (bExpectSuccess)
            {
              // No failed assertions expected
              if (nErrorMessages > 0)
              {
                final StringBuilder aMessage = new StringBuilder ();
                aMessage.append (sErrors +
                                 (nWarningMessages > 0 ? " and " + sWarnings : "") +
                                 " for XML file '" +
                                 aXMLFile.getPath () +
                                 "'");

                for (final AbstractSVRLMessage aMsg : aMessages)
                {
                  aMessage.append ("\n  ")
                          .append (ErrorTextProvider.DEFAULT.getErrorText (aMsg.getAsResourceError (aXMLFile.getPath ()),
                                                                           Locale.US));
                }

                // As at least one error is contained, it's okay to throw an
                // exception in case
                _error (aMessage.toString ());
                continue;
              }

              // Success as expected
              log ("XML file '" +
                   aXMLFile.getPath () +
                   "' was validated against Schematron '" +
                   aSch.getResource ().getPath () +
                   "' and matches the rules" +
                   (nWarningMessages > 0 ? " - only " + sWarnings + " are contained" : ""),
                   Project.MSG_INFO);
            }
            else
            {
              // At least one failed assertions expected
              if (nErrorMessages == 0)
              {
                String sMessage = "No Schematron errors for erroneous XML file '" + aXMLFile.getPath () + "'";
                if (nWarningMessages > 0)
                  sMessage += " - only " + sWarnings + " are contained";

                _error (sMessage);
                continue;
              }

              // Success as expected
              log ("XML file '" +
                   aXMLFile.getPath () +
                   "' was validated against Schematron '" +
                   aSch.getResource ().getPath () +
                   "' " +
                   sErrors +
                   (nWarningMessages > 0 ? " and " + sWarnings : "") +
                   " were found (as expected)",
                   Project.MSG_INFO);
            }
          }
          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 ();
            _error (sMessage, ex);
            continue;
          }
        }
      }
    }
  }

  @Override
  public void execute () throws BuildException
  {
    boolean bCanRun = false;
    if (m_aSchematronFile == null)
      _error ("No Schematron file specified!");
    else
      if (m_aSchematronFile.exists () && !m_aSchematronFile.isFile ())
        _error ("The specified Schematron file " + m_aSchematronFile + " is not a file!");
      else
        if (m_eSchematronProcessingEngine == null)
          _error ("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 ())
            _error ("No XML resources to be validated specified! Add e.g. a <fileset> element.");
          else
            if (m_aSvrlDirectory != null && !m_aSvrlDirectory.exists () && !m_aSvrlDirectory.mkdirs ())
              _error ("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 ())
            log ("Using the following custom parameters: " + aParams, Project.MSG_INFO);

          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 ())
            log ("Using the following custom parameters: " + aParams, Project.MSG_INFO);

          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:
          _error ("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))
          {
            log ("Error in Schematron: " + aError.getAsString (aDisplayLocale), Project.MSG_ERR);
            bAnyParsingError = true;
          }
          else
            if (aError.getErrorLevel ().isGE (EErrorLevel.WARN))
              log ("Warning in Schematron: " + aError.getAsString (aDisplayLocale), Project.MSG_WARN);

        if (bAnyParsingError)
          _error ("The provided Schematron file contains errors. See log for details.");
        else
        {
          // Start validation
          log ("Successfully parsed Schematron file '" + m_aSchematronFile.getPath () + "'", Project.MSG_INFO);

          // 2. for all XML files that match the pattern
          _performValidation (aSch, m_aResCollections, m_aSvrlDirectory, m_bExpectSuccess);
        }
      }
    }
  }
}


/**
 * Copyright (C) 2014-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.pure.preprocess;

import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import com.helger.commons.collection.impl.CommonsHashSet;
import com.helger.commons.collection.impl.ICommonsSet;
import com.helger.commons.string.ToStringGenerator;

/**
 * Utility lookup cache for all used IDs within a schema.
 *
 * @author Philip Helger
 */
@NotThreadSafe
final class PreprocessorIDPool
{
  private final ICommonsSet <String> m_aUsedIDs = new CommonsHashSet <> ();

  public PreprocessorIDPool ()
  {}

  /**
   * Create a unique ID based on the passed one. If the passed ID is not yet
   * contained, than a numeric index is appended until the ID is unique.
   *
   * @param sID
   *        The source ID. May be null.
   * @return null if the passed ID is null indicating
   *         that no ID is present. Otherwise a unique ID is returned.
   */
  @Nullable
  public String getUniqueID (@Nullable final String sID)
  {
    if (sID == null)
      return null;

    if (m_aUsedIDs.add (sID))
    {
      // Unique ID
      return sID;
    }

    // Non-unique ID
    int nIndex = 0;
    String sNewID = sID + nIndex;
    do
    {
      if (m_aUsedIDs.add (sNewID))
      {
        // Now it's unique
        return sNewID;
      }
      ++nIndex;
      sNewID = sID + nIndex;
    } while (true);
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("usedIDs", m_aUsedIDs).getToString ();
  }
}


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

import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.xml.xpath.XPathFunctionResolver;
import javax.xml.xpath.XPathVariableResolver;

import org.oclc.purl.dsdl.svrl.SchematronOutputType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.EntityResolver;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.io.resource.ClassPathResource;
import com.helger.commons.io.resource.FileSystemResource;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.io.resource.URLResource;
import com.helger.commons.io.resource.inmemory.AbstractMemoryReadableResource;
import com.helger.commons.io.resource.inmemory.ReadableResourceByteArray;
import com.helger.commons.io.resource.inmemory.ReadableResourceInputStream;
import com.helger.commons.state.EValidity;
import com.helger.schematron.AbstractSchematronResource;
import com.helger.schematron.SchematronDebug;
import com.helger.schematron.SchematronException;
import com.helger.schematron.pure.bound.IPSBoundSchema;
import com.helger.schematron.pure.bound.PSBoundSchemaCache;
import com.helger.schematron.pure.bound.PSBoundSchemaCacheKey;
import com.helger.schematron.pure.errorhandler.DoNothingPSErrorHandler;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.exchange.PSWriter;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.schematron.svrl.SVRLMarshaller;
import com.helger.xml.serialize.write.XMLWriterSettings;

/**
 * A Schematron resource that is not XSLT based but using the pure (native Java)
 * implementation. This class itself is not thread safe, but the underlying
 * cache is thread safe. So once you configured this object fully (with all the
 * setter), it can be considered thread safe.

 * Important: This class can only handle XPath expressions but no
 * XSLT functions in Schematron asserts and reports! If your Schematrons use
 * XSLT functionality you're better off using the
 * {@link com.helger.schematron.xslt.SchematronResourceSCH} or
 * {@link com.helger.schematron.xslt.SchematronResourceXSLT} classes instead!
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class SchematronResourcePure extends AbstractSchematronResource
{
  private static final Logger LOGGER = LoggerFactory.getLogger (SchematronResourcePure.class);

  private String m_sPhase;
  private IPSErrorHandler m_aErrorHandler;
  private XPathVariableResolver m_aVariableResolver;
  private XPathFunctionResolver m_aFunctionResolver;
  // Status var
  private IPSBoundSchema m_aBoundSchema;

  public SchematronResourcePure (@Nonnull final IReadableResource aResource)
  {
    this (aResource, (String) null, (IPSErrorHandler) null);
  }

  public SchematronResourcePure (@Nonnull final IReadableResource aResource,
                                 @Nullable final String sPhase,
                                 @Nullable final IPSErrorHandler aErrorHandler)
  {
    super (aResource);
    setPhase (sPhase);
    setErrorHandler (aErrorHandler);
  }

  /**
   * @return The phase to be used. May be null.
   */
  @Nullable
  public String getPhase ()
  {
    return m_sPhase;
  }

  /**
   * Set the Schematron phase to be evaluated. Changing the phase will result in
   * a newly bound schema!
   *
   * @param sPhase
   *        The name of the phase to use. May be null which means
   *        all phases.
   * @return this
   */
  @Nonnull
  public SchematronResourcePure setPhase (@Nullable final String sPhase)
  {
    if (m_aBoundSchema != null)
      throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
    m_sPhase = sPhase;
    return this;
  }

  /**
   * @return The error handler to be used to bind the schema. May be
   *         null.
   */
  @Nullable
  public IPSErrorHandler getErrorHandler ()
  {
    return m_aErrorHandler;
  }

  /**
   * Set the error handler to be used during binding.
   *
   * @param aErrorHandler
   *        The error handler. May be null.
   * @return this
   */
  @Nonnull
  public SchematronResourcePure setErrorHandler (@Nullable final IPSErrorHandler aErrorHandler)
  {
    if (m_aBoundSchema != null)
      throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
    m_aErrorHandler = aErrorHandler;
    return this;
  }

  /**
   * @return The variable resolver to be used. May be null.
   */
  @Nullable
  public XPathVariableResolver getVariableResolver ()
  {
    return m_aVariableResolver;
  }

  /**
   * Set the variable resolver to be used in the XPath statements. This can only
   * be set before the Schematron is bound. If it is already bound an exception
   * is thrown to indicate the unnecessity of the call.
   *
   * @param aVariableResolver
   *        The variable resolver to set. May be null.
   * @return this
   */
  @Nonnull
  public SchematronResourcePure setVariableResolver (@Nullable final XPathVariableResolver aVariableResolver)
  {
    if (m_aBoundSchema != null)
      throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
    m_aVariableResolver = aVariableResolver;
    return this;
  }

  /**
   * @return The function resolver to be used. May be null.
   */
  @Nullable
  public XPathFunctionResolver getFunctionResolver ()
  {
    return m_aFunctionResolver;
  }

  /**
   * Set the function resolver to be used in the XPath statements. This can only
   * be set before the Schematron is bound. If it is already bound an exception
   * is thrown to indicate the unnecessity of the call.
   *
   * @param aFunctionResolver
   *        The function resolver to set. May be null.
   * @return this
   */
  @Nonnull
  public SchematronResourcePure setFunctionResolver (@Nullable final XPathFunctionResolver aFunctionResolver)
  {
    if (m_aBoundSchema != null)
      throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
    m_aFunctionResolver = aFunctionResolver;
    return this;
  }

  /**
   * Set the XML entity resolver to be used when reading the Schematron or the
   * XML to be validated. This can only be set before the Schematron is bound.
   * If it is already bound an exception is thrown to indicate the unnecessity
   * of the call.
   *
   * @param aEntityResolver
   *        The entity resolver to set. May be null.
   * @return this
   * @since 4.1.1
   */
  @Nonnull
  public SchematronResourcePure setEntityResolver (@Nullable final EntityResolver aEntityResolver)
  {
    if (m_aBoundSchema != null)
      throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
    internalSetEntityResolver (aEntityResolver);
    return this;
  }

  @Nonnull
  protected IPSBoundSchema createBoundSchema ()
  {
    final IReadableResource aResource = getResource ();
    final IPSErrorHandler aErrorHandler = getErrorHandler ();
    final PSBoundSchemaCacheKey aCacheKey = new PSBoundSchemaCacheKey (aResource,
                                                                       getPhase (),
                                                                       aErrorHandler,
                                                                       getVariableResolver (),
                                                                       getFunctionResolver (),
                                                                       getEntityResolver ());
    if (aResource instanceof AbstractMemoryReadableResource || !isUseCache ())
    {
      // No need to cache anything for memory resources
      try
      {
        return aCacheKey.createBoundSchema ();
      }
      catch (final SchematronException ex)
      {
        // Convert to runtime exception
        throw new IllegalStateException ("Failed to bind Schematron", ex);
      }
    }

    // Resolve from cache - inside the cacheKey the reading and binding
    // happens
    return PSBoundSchemaCache.getInstance ().getFromCache (aCacheKey);
  }

  /**
   * Get the cached bound schema or create a new one.
   *
   * @return The bound schema. Never null.
   */
  @Nonnull
  public IPSBoundSchema getOrCreateBoundSchema ()
  {
    if (m_aBoundSchema == null)
      try
      {
        m_aBoundSchema = createBoundSchema ();
      }
      catch (final RuntimeException ex)
      {
        if (m_aErrorHandler != null)
          m_aErrorHandler.error (getResource (), null, "Error creating bound schema", ex);
        throw ex;
      }

    return m_aBoundSchema;
  }

  public boolean isValidSchematron ()
  {
    // Use the provided error handler (if any)
    try
    {
      final IPSErrorHandler aErrorHandler = m_aErrorHandler != null ? m_aErrorHandler : new DoNothingPSErrorHandler ();
      return getOrCreateBoundSchema ().getOriginalSchema ().isValid (aErrorHandler);
    }
    catch (final RuntimeException ex)
    {
      // May happen when XPath errors are contained
      return false;
    }
  }

  /**
   * Use the internal error handler to validate all elements in the schematron.
   * It tries to catch as many errors as possible.
   */
  public void validateCompletely ()
  {
    // Use the provided error handler (if any)
    final IPSErrorHandler aErrorHandler = m_aErrorHandler != null ? m_aErrorHandler : new DoNothingPSErrorHandler ();
    validateCompletely (aErrorHandler);
  }

  /**
   * Use the provided error handler to validate all elements in the schematron.
   * It tries to catch as many errors as possible.
   *
   * @param aErrorHandler
   *        The error handler to use. May not be null.
   */
  public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
  {
    ValueEnforcer.notNull (aErrorHandler, "ErrorHandler");

    try
    {
      getOrCreateBoundSchema ().getOriginalSchema ().validateCompletely (aErrorHandler);
    }
    catch (final RuntimeException ex)
    {
      // May happen when XPath errors are contained
    }
  }

  @Nonnull
  public EValidity getSchematronValidity (@Nonnull final Node aXMLNode,
                                          @Nullable final String sBaseURI) throws Exception
  {
    ValueEnforcer.notNull (aXMLNode, "XMLNode");

    if (!isValidSchematron ())
      return EValidity.INVALID;

    return getOrCreateBoundSchema ().validatePartially (aXMLNode, sBaseURI);
  }

  /**
   * The main method to convert a node to an SVRL document.
   *
   * @param aXMLNode
   *        The source node to be validated. May not be null.
   * @param sBaseURI
   *        Base URI of the XML document to be validated. May be
   *        null.
   * @return The SVRL document. Never null.
   * @throws SchematronException
   *         in case of a sever error validating the schema
   */
  @Nonnull
  public SchematronOutputType applySchematronValidationToSVRL (@Nonnull final Node aXMLNode,
                                                               @Nullable final String sBaseURI) throws SchematronException
  {
    ValueEnforcer.notNull (aXMLNode, "XMLNode");

    final SchematronOutputType aSOT = getOrCreateBoundSchema ().validateComplete (aXMLNode, sBaseURI);

    // Debug print the created SVRL document
    if (SchematronDebug.isShowCreatedSVRL ())
      LOGGER.info ("Created SVRL:\n" + new SVRLMarshaller (false).getAsString (aSOT));

    return aSOT;
  }

  @Nullable
  public Document applySchematronValidation (@Nonnull final Node aXMLNode,
                                             @Nullable final String sBaseURI) throws Exception
  {
    ValueEnforcer.notNull (aXMLNode, "XMLNode");

    final SchematronOutputType aSO = applySchematronValidationToSVRL (aXMLNode, sBaseURI);
    return new SVRLMarshaller ().getAsDocument (aSO);
  }

  /**
   * Create a new {@link SchematronResourcePure} from a Classpath Schematron
   * rules
   *
   * @param sSCHPath
   *        The classpath relative path to the Schematron rules.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromClassPath (@Nonnull @Nonempty final String sSCHPath)
  {
    return new SchematronResourcePure (new ClassPathResource (sSCHPath));
  }

  /**
   * Create a new {@link SchematronResourcePure} from file system Schematron
   * rules
   *
   * @param sSCHPath
   *        The file system path to the Schematron rules.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromFile (@Nonnull @Nonempty final String sSCHPath)
  {
    return new SchematronResourcePure (new FileSystemResource (sSCHPath));
  }

  /**
   * Create a new {@link SchematronResourcePure} from file system Schematron
   * rules
   *
   * @param aSCHFile
   *        The file system path to the Schematron rules.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromFile (@Nonnull final File aSCHFile)
  {
    return new SchematronResourcePure (new FileSystemResource (aSCHFile));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * at a URL
   *
   * @param sSCHURL
   *        The URL to the Schematron rules. May neither be null
   *        nor empty.
   * @return Never null.
   * @throws MalformedURLException
   *         In case an invalid URL is provided
   */
  @Nonnull
  public static SchematronResourcePure fromURL (@Nonnull @Nonempty final String sSCHURL) throws MalformedURLException
  {
    return new SchematronResourcePure (new URLResource (sSCHURL));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * at a URL
   *
   * @param aSCHURL
   *        The URL to the Schematron rules. May not be null.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromURL (@Nonnull final URL aSCHURL)
  {
    return new SchematronResourcePure (new URLResource (aSCHURL));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * by an arbitrary {@link InputStream}.

   * Important: in this case, no include resolution will be performed!!
   *
   * @param aSchematronIS
   *        The {@link InputStream} to read the Schematron rules from. May not
   *        be null.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromInputStream (@Nonnull final InputStream aSchematronIS)
  {
    return new SchematronResourcePure (new ReadableResourceInputStream (aSchematronIS));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * by an arbitrary byte array.

   * Important: in this case, no include resolution will be performed!!
   *
   * @param aSchematron
   *        The byte array representing the Schematron. May not be
   *        null.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromByteArray (@Nonnull final byte [] aSchematron)
  {
    return new SchematronResourcePure (new ReadableResourceByteArray (aSchematron));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * by an arbitrary String.

   * Important: in this case, no include resolution will be performed!!
   *
   * @param sSchematron
   *        The String representing the Schematron. May not be null
   *        .
   * @param aCharset
   *        The charset to be used to convert the String to a byte array.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromString (@Nonnull final String sSchematron, @Nonnull final Charset aCharset)
  {
    return fromByteArray (sSchematron.getBytes (aCharset));
  }

  /**
   * Create a new {@link SchematronResourcePure} from Schematron rules provided
   * by a domain model.

   * Important: in this case, no include resolution will be performed!!
   *
   * @param aSchematron
   *        The Schematron model to be used. May not be null .
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourcePure fromSchema (@Nonnull final PSSchema aSchematron)
  {
    return fromString (new PSWriter ().getXMLString (aSchematron), XMLWriterSettings.DEFAULT_XML_CHARSET_OBJ);
  }
}


/**
 * Copyright (C) 2014-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.pure.preprocess;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.collection.impl.ICommonsNavigableMap;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.pure.binding.IPSQueryBinding;
import com.helger.schematron.pure.model.IPSElement;
import com.helger.schematron.pure.model.PSActive;
import com.helger.schematron.pure.model.PSAssertReport;
import com.helger.schematron.pure.model.PSDiagnostic;
import com.helger.schematron.pure.model.PSDiagnostics;
import com.helger.schematron.pure.model.PSDir;
import com.helger.schematron.pure.model.PSEmph;
import com.helger.schematron.pure.model.PSExtends;
import com.helger.schematron.pure.model.PSLet;
import com.helger.schematron.pure.model.PSNS;
import com.helger.schematron.pure.model.PSName;
import com.helger.schematron.pure.model.PSPattern;
import com.helger.schematron.pure.model.PSPhase;
import com.helger.schematron.pure.model.PSRule;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.schematron.pure.model.PSSpan;
import com.helger.schematron.pure.model.PSValueOf;

/**
 * This is the pre-processor class for pure Schematron. It converts an existing
 * schema to the minimal syntax (by default) but allows for a certain degree of
 * customization by keeping certain elements in the resulting schema. The actual
 * query binding is used, so that report test expressions can be converted to
 * assertions, and to replace the content of <param> elements into actual
 * values.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PSPreprocessor
{
  public static final boolean DEFAULT_KEEP_TITLES = false;
  public static final boolean DEFAULT_KEEP_DIAGNOSTICS = false;
  public static final boolean DEFAULT_KEEP_REPORTS = false;
  public static final boolean DEFAULT_KEEP_EMPTY_PATTERNS = true;
  public static final boolean DEFAULT_KEEP_EMPTY_SCHEMA = true;

  private final IPSQueryBinding m_aQueryBinding;
  private boolean m_bKeepTitles = DEFAULT_KEEP_TITLES;
  private boolean m_bKeepDiagnostics = DEFAULT_KEEP_DIAGNOSTICS;
  private boolean m_bKeepReports = DEFAULT_KEEP_REPORTS;
  private boolean m_bKeepEmptyPatterns = DEFAULT_KEEP_EMPTY_PATTERNS;
  private boolean m_bKeepEmptySchema = DEFAULT_KEEP_EMPTY_SCHEMA;

  public PSPreprocessor (@Nonnull final IPSQueryBinding aQueryBinding)
  {
    m_aQueryBinding = ValueEnforcer.notNull (aQueryBinding, "QueryBinding");
  }

  /**
   * @return The query binding to be used. Never null!
   */
  @Nonnull
  public IPSQueryBinding getQueryBinding ()
  {
    return m_aQueryBinding;
  }

  /**
   * @return true if <title>-elements should be kept. Default
   *         is {@value #DEFAULT_KEEP_TITLES}.
   */
  public boolean isKeepTitles ()
  {
    return m_bKeepTitles;
  }

  /**
   * Should <title>-elements be kept?
   *
   * @param bKeepTitles
   *        true to keep titles, false otherwise.
   * @return this for chaining
   */
  @Nonnull
  public PSPreprocessor setKeepTitles (final boolean bKeepTitles)
  {
    m_bKeepTitles = bKeepTitles;
    return this;
  }

  /**
   * @return true if <diagnostics>-elements should be kept.
   *         Default is {@value #DEFAULT_KEEP_DIAGNOSTICS}.
   */
  public boolean isKeepDiagnostics ()
  {
    return m_bKeepDiagnostics;
  }

  /**
   * Should <diagnostics>-elements be kept?
   *
   * @param bKeepDiagnostics
   *        true to keep diagnostics, false otherwise.
   * @return this for chaining
   */
  @Nonnull
  public PSPreprocessor setKeepDiagnostics (final boolean bKeepDiagnostics)
  {
    m_bKeepDiagnostics = bKeepDiagnostics;
    return this;
  }

  /**
   * @return true if <report>-elements should be kept,
   *         false if they should be converted to
   *         <assert>-elements. Default is {@value #DEFAULT_KEEP_REPORTS}.
   */
  public boolean isKeepReports ()
  {
    return m_bKeepReports;
  }

  /**
   * Should <report>-elements be kept or should they be converted to
   * <assert>-elements?
   *
   * @param bKeepReports
   *        true to keep <report>-elements,
   *        false to change them to <assert>-elements
   * @return this for chaining
   */
  @Nonnull
  public PSPreprocessor setKeepReports (final boolean bKeepReports)
  {
    m_bKeepReports = bKeepReports;
    return this;
  }

  /**
   * @return true if <pattern>-elements without a rule should
   *         be kept. Default is {@value #DEFAULT_KEEP_EMPTY_PATTERNS}.
   */
  public boolean isKeepEmptyPatterns ()
  {
    return m_bKeepEmptyPatterns;
  }

  /**
   * Should <pattern>-elements without a single rule be kept or deleted?
   *
   * @param bKeepEmptyPatterns
   *        true to keep <pattern>-elements without a rule,
   *        false to delete them
   * @return this for chaining
   */
  @Nonnull
  public PSPreprocessor setKeepEmptyPatterns (final boolean bKeepEmptyPatterns)
  {
    m_bKeepEmptyPatterns = bKeepEmptyPatterns;
    return this;
  }

  /**
   * @return true if <schema>-elements without a pattern
   *         should be kept. Default is {@value #DEFAULT_KEEP_EMPTY_SCHEMA}.
   */
  public boolean isKeepEmptySchema ()
  {
    return m_bKeepEmptySchema;
  }

  /**
   * Should schema objects without a pattern be kept? It makes only sense to set
   * it to false if {@link #setKeepEmptyPatterns(boolean)} is also
   * set to false, because otherwise patterns without rules are kept.
   *
   * @param bKeepEmptySchema
   *        true to keep them, false to discard them.
   * @return this
   */
  @Nonnull
  public PSPreprocessor setKeepEmptySchema (final boolean bKeepEmptySchema)
  {
    m_bKeepEmptySchema = bKeepEmptySchema;
    return this;
  }

  @Nonnull
  private static PSPhase _getPreprocessedPhase (@Nonnull final PSPhase aPhase,
                                                @Nonnull final PreprocessorIDPool aIDPool) throws SchematronPreprocessException
  {
    final PSPhase ret = new PSPhase ();
    ret.setID (aIDPool.getUniqueID (aPhase.getID ()));
    ret.setRich (aPhase.getRichClone ());
    if (aPhase.hasAnyInclude ())
      throw new SchematronPreprocessException ("Cannot preprocess <phase> with an <include>");
    for (final IPSElement aElement : aPhase.getAllContentElements ())
    {
      if (aElement instanceof PSActive)
        ret.addActive (((PSActive) aElement).getClone ());
      else
        if (aElement instanceof PSLet)
          ret.addLet (((PSLet) aElement).getClone ());
      // ps are ignored
    }
    ret.addForeignElements (aPhase.getAllForeignElements ());
    ret.addForeignAttributes (aPhase.getAllForeignAttributes ());
    return ret;
  }

  @Nonnull
  private PSAssertReport _getPreprocessedAssert (@Nonnull final PSAssertReport aAssertReport,
                                                 @Nonnull final PreprocessorIDPool aIDPool,
                                                 @Nullable final ICommonsMap <String, String> aParamValueMap)
  {
    String sTest = aAssertReport.getTest ();
    if (aAssertReport.isReport () && !m_bKeepReports)
    {
      // Negate the expression!
      sTest = m_aQueryBinding.getNegatedTestExpression (sTest);
    }

    // Keep report or make it always an assert
    final PSAssertReport ret = new PSAssertReport (m_bKeepReports ? aAssertReport.isAssert () : true);
    ret.setTest (m_aQueryBinding.getWithParamTextsReplaced (sTest, aParamValueMap));
    ret.setFlag (aAssertReport.getFlag ());
    ret.setID (aIDPool.getUniqueID (aAssertReport.getID ()));
    if (m_bKeepDiagnostics)
      ret.setDiagnostics (aAssertReport.getAllDiagnostics ());
    ret.setRich (aAssertReport.getRichClone ());
    ret.setLinkable (aAssertReport.getLinkableClone ());
    for (final Object aContent : aAssertReport.getAllContentElements ())
    {
      if (aContent instanceof String)
        ret.addText ((String) aContent);
      else
        if (aContent instanceof PSName)
          ret.addName (((PSName) aContent).getClone ());
        else
          if (aContent instanceof PSValueOf)
          {
            final PSValueOf aValueOf = ((PSValueOf) aContent).getClone ();
            aValueOf.setSelect (m_aQueryBinding.getWithParamTextsReplaced (aValueOf.getSelect (), aParamValueMap));
            ret.addValueOf (aValueOf);
          }
          else
            if (aContent instanceof PSEmph)
              ret.addEmph (((PSEmph) aContent).getClone ());
            else
              if (aContent instanceof PSDir)
                ret.addDir (((PSDir) aContent).getClone ());
              else
                if (aContent instanceof PSSpan)
                  ret.addSpan (((PSSpan) aContent).getClone ());
    }
    ret.addForeignElements (aAssertReport.getAllForeignElements ());
    ret.addForeignAttributes (aAssertReport.getAllForeignAttributes ());
    return ret;
  }

  /**
   * Resolve all <extends> elements. This method calls itself recursively
   * until all extends elements are resolved.
   *
   * @param aRuleContent
   *        A list consisting of {@link PSAssertReport} and {@link PSExtends}
   *        objects. Never null.
   * @param aLookup
   *        The rule lookup object
   * @throws SchematronPreprocessException
   *         If the base rule of an extends object could not be resolved.
   */
  private void _resolveRuleContent (@Nonnull final ICommonsList <IPSElement> aRuleContent,
                                    @Nonnull final PreprocessorLookup aLookup,
                                    @Nonnull final PreprocessorIDPool aIDPool,
                                    @Nullable final ICommonsMap <String, String> aParamValueMap,
                                    @Nonnull final PSRule aTargetRule) throws SchematronPreprocessException
  {
    for (final IPSElement aElement : aRuleContent)
    {
      if (aElement instanceof PSAssertReport)
      {
        final PSAssertReport aAssertReport = (PSAssertReport) aElement;
        aTargetRule.addAssertReport (_getPreprocessedAssert (aAssertReport, aIDPool, aParamValueMap));
      }
      else
      {
        final PSExtends aExtends = (PSExtends) aElement;
        final String sRuleID = aExtends.getRule ();
        final PSRule aBaseRule = aLookup.getAbstractRuleOfID (sRuleID);
        if (aBaseRule == null)
          throw new SchematronPreprocessException ("Failed to resolve rule ID '" +
                                                   sRuleID +
                                                   "' in extends statement. Available rules are: " +
                                                   aLookup.getAllAbstractRuleIDs ());

        // Recursively resolve the extends of the base rule
        _resolveRuleContent (aBaseRule.getAllContentElements (), aLookup, aIDPool, aParamValueMap, aTargetRule);

        // Copy all lets
        for (final PSLet aBaseLet : aBaseRule.getAllLets ())
          aTargetRule.addLet (aBaseLet.getClone ());
      }
    }
  }

  @Nullable
  private PSRule _getPreprocessedRule (@Nonnull final PSRule aRule,
                                       @Nonnull final PreprocessorLookup aLookup,
                                       @Nonnull final PreprocessorIDPool aIDPool,
                                       @Nullable final ICommonsMap <String, String> aParamValueMap) throws SchematronPreprocessException
  {
    if (aRule.isAbstract ())
    {
      // Will be inlined
      return null;
    }

    final PSRule ret = new PSRule ();
    ret.setFlag (aRule.getFlag ());
    ret.setRich (aRule.getRichClone ());
    ret.setLinkable (aRule.getLinkableClone ());
    // abstract is always false
    ret.setContext (m_aQueryBinding.getWithParamTextsReplaced (aRule.getContext (), aParamValueMap));
    ret.setID (aIDPool.getUniqueID (aRule.getID ()));
    if (aRule.hasAnyInclude ())
      throw new SchematronPreprocessException ("Cannot preprocess <rule> with an <include>");
    for (final PSLet aLet : aRule.getAllLets ())
      ret.addLet (aLet.getClone ());
    _resolveRuleContent (aRule.getAllContentElements (), aLookup, aIDPool, aParamValueMap, ret);
    ret.addForeignElements (aRule.getAllForeignElements ());
    ret.addForeignAttributes (aRule.getAllForeignAttributes ());
    return ret;
  }

  @Nullable
  private PSPattern _getPreprocessedPattern (@Nonnull final PSPattern aPattern,
                                             @Nonnull final PreprocessorLookup aLookup,
                                             @Nonnull final PreprocessorIDPool aIDPool) throws SchematronPreprocessException
  {
    if (aPattern.isAbstract ())
    {
      // Will be inlined
      return null;
    }

    final PSPattern ret = new PSPattern ();
    // abstract always false
    // is-a must be resolved
    ret.setID (aIDPool.getUniqueID (aPattern.getID ()));
    ret.setRich (aPattern.getRichClone ());
    if (aPattern.hasAnyInclude ())
      throw new SchematronPreprocessException ("Cannot preprocess <pattern> with an <include>");
    if (m_bKeepTitles && aPattern.hasTitle ())
      ret.setTitle (aPattern.getTitle ().getClone ());

    final String sIsA = aPattern.getIsA ();
    if (sIsA != null)
    {
      final PSPattern aBasePattern = aLookup.getAbstractPatternOfID (sIsA);
      if (aBasePattern == null)
        throw new SchematronPreprocessException ("Failed to resolve the pattern denoted by is-a='" + sIsA + "'");

      if (!ret.hasID ())
        ret.setID (aIDPool.getUniqueID (aBasePattern.getID ()));
      if (!ret.hasRich ())
        ret.setRich (aBasePattern.getRichClone ());

      // get the string replacements
      final ICommonsNavigableMap <String, String> aParamValueMap = m_aQueryBinding.getStringReplacementMap (aPattern.getAllParams ());

      for (final IPSElement aElement : aBasePattern.getAllContentElements ())
      {
        if (aElement instanceof PSLet)
          ret.addLet (((PSLet) aElement).getClone ());
        else
          if (aElement instanceof PSRule)
          {
            final PSRule aMinifiedRule = _getPreprocessedRule ((PSRule) aElement, aLookup, aIDPool, aParamValueMap);
            if (aMinifiedRule != null)
              ret.addRule (aMinifiedRule);
          }
        // params must have been resolved
        // ps are ignored
      }
    }
    else
    {
      for (final IPSElement aElement : aPattern.getAllContentElements ())
      {
        if (aElement instanceof PSLet)
          ret.addLet (((PSLet) aElement).getClone ());
        else
          if (aElement instanceof PSRule)
          {
            final PSRule aMinifiedRule = _getPreprocessedRule ((PSRule) aElement, aLookup, aIDPool, null);
            if (aMinifiedRule != null)
              ret.addRule (aMinifiedRule);
          }
        // params must beben resolved
        // ps are ignored
      }
    }
    ret.addForeignElements (aPattern.getAllForeignElements ());
    ret.addForeignAttributes (aPattern.getAllForeignAttributes ());
    return ret;
  }

  @Nonnull
  private static PSDiagnostics _getPreprocessedDiagnostics (@Nonnull final PSDiagnostics aDiagnostics) throws SchematronPreprocessException
  {
    final PSDiagnostics ret = new PSDiagnostics ();
    if (aDiagnostics.hasAnyInclude ())
      throw new SchematronPreprocessException ("Cannot preprocess <diagnostics> with an <include>");
    for (final PSDiagnostic aDiagnostic : aDiagnostics.getAllDiagnostics ())
      ret.addDiagnostic (aDiagnostic.getClone ());
    ret.addForeignElements (aDiagnostics.getAllForeignElements ());
    ret.addForeignAttributes (aDiagnostics.getAllForeignAttributes ());
    return ret;
  }

  /**
   * Convert the passed schema to a minimal schema.
   *
   * @param aSchema
   *        The schema to be made minimal. May not be null
   * @return The original schema object, if it is already minimal - a minimal
   *         copy otherwise! May be null if the original schema is
   *         not yet minimal and {@link #isKeepEmptySchema()} is set to
   *         false.
   * @throws SchematronPreprocessException
   *         In case a preprocessing error occurs
   */
  @Nullable
  public PSSchema getAsMinimalSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
  {
    ValueEnforcer.notNull (aSchema, "Schema");

    // Anything to do?
    if (aSchema.isMinimal ())
      return aSchema;

    return getForcedPreprocessedSchema (aSchema);
  }

  /**
   * Convert the passed schema to a pre-processed schema.
   *
   * @param aSchema
   *        The schema to pre-process. May not be null
   * @return The original schema object, if it is already pre-processed - a
   *         pre-processed copy otherwise! May be null if the
   *         original schema is not yet pre-processed and
   *         {@link #isKeepEmptySchema()} is set to false.
   * @throws SchematronPreprocessException
   *         In case a preprocessing error occurs
   */
  @Nullable
  public PSSchema getAsPreprocessedSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
  {
    ValueEnforcer.notNull (aSchema, "Schema");

    // Anything to do?
    if (aSchema.isPreprocessed ())
      return aSchema;

    return getForcedPreprocessedSchema (aSchema);
  }

  /**
   * Convert the passed schema to a pre-processed schema independent if it is
   * already minimal or not.
   *
   * @param aSchema
   *        The schema to be made minimal. May not be null
   * @return A minimal copy of the schema. May be null if the
   *         original schema is not yet minimal and {@link #isKeepEmptySchema()}
   *         is set to false.
   * @throws SchematronPreprocessException
   *         In case a preprocessing error occurs
   */
  @Nullable
  public PSSchema getForcedPreprocessedSchema (@Nonnull final PSSchema aSchema) throws SchematronPreprocessException
  {
    ValueEnforcer.notNull (aSchema, "Schema");

    final PreprocessorLookup aLookup = new PreprocessorLookup (aSchema);
    final PreprocessorIDPool aIDPool = new PreprocessorIDPool ();

    final PSSchema ret = new PSSchema (aSchema.getResource ());
    ret.setID (aIDPool.getUniqueID (aSchema.getID ()));
    ret.setRich (aSchema.getRichClone ());
    ret.setSchemaVersion (aSchema.getSchemaVersion ());
    ret.setDefaultPhase (aSchema.getDefaultPhase ());
    ret.setQueryBinding (aSchema.getQueryBinding ());
    if (m_bKeepTitles && aSchema.hasTitle ())
      ret.setTitle (aSchema.getTitle ().getClone ());
    if (aSchema.hasAnyInclude ())
      throw new SchematronPreprocessException ("Cannot preprocess <schema> with an <include>");
    for (final PSNS aNS : aSchema.getAllNSs ())
      ret.addNS (aNS.getClone ());
    // start ps are skipped
    for (final PSLet aLet : aSchema.getAllLets ())
      ret.addLet (aLet.getClone ());
    for (final PSPhase aPhase : aSchema.getAllPhases ())
      ret.addPhase (_getPreprocessedPhase (aPhase, aIDPool));
    for (final PSPattern aPattern : aSchema.getAllPatterns ())
    {
      final PSPattern aMinifiedPattern = _getPreprocessedPattern (aPattern, aLookup, aIDPool);
      if (aMinifiedPattern != null)
      {
        // Pattern without rules?
        if (aMinifiedPattern.getRuleCount () > 0 || m_bKeepEmptyPatterns)
          ret.addPattern (aMinifiedPattern);
      }
    }

    // Schema without patterns?
    if (aSchema.getPatternCount () == 0 && !m_bKeepEmptySchema)
      return null;

    // end ps are skipped
    if (m_bKeepDiagnostics && aSchema.hasDiagnostics ())
      ret.setDiagnostics (_getPreprocessedDiagnostics (aSchema.getDiagnostics ()));
    ret.addForeignElements (aSchema.getAllForeignElements ());
    ret.addForeignAttributes (aSchema.getAllForeignAttributes ());
    return ret;
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("queryBinding", m_aQueryBinding)
                                       .append ("keepTitles", m_bKeepTitles)
                                       .append ("keepDiagnostics", m_bKeepDiagnostics)
                                       .append ("keepReports", m_bKeepReports)
                                       .append ("keepEmptyPatterns", m_bKeepEmptyPatterns)
                                       .append ("keepEmptySchema", m_bKeepEmptySchema)
                                       .getToString ();
  }

  @Nonnull
  public static PSPreprocessor createPreprocessorWithoutInformationLoss (@Nonnull final IPSQueryBinding aQueryBinding)
  {
    final PSPreprocessor aPreprocessor = new PSPreprocessor (aQueryBinding);

    // Keep as much of the original information as possible, as it is not our
    // goal to minify the scheme
    aPreprocessor.setKeepReports (true);
    aPreprocessor.setKeepDiagnostics (true);
    aPreprocessor.setKeepTitles (true);

    return aPreprocessor;
  }
}


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

import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;

import com.helger.commons.ValueEnforcer;

/**
 * Global Schematron debug settings etc.
 *
 * @author Philip Helger
 * @since 4.3.4
 */
@ThreadSafe
public final class SchematronDebug
{
  private static final AtomicBoolean s_bSaveIntermediateXSLTFiles = new AtomicBoolean (false);
  private static File s_aIntermediateMinifiedSCHFolder = new File ("test-minified");
  private static File s_aIntermediateFinalXSLTFolder = new File ("test-final");
  private static final AtomicBoolean s_aShowCreatedXSLT = new AtomicBoolean (false);
  private static final AtomicBoolean s_aShowCreatedSVRL = new AtomicBoolean (false);
  private static final AtomicBoolean s_aShowPreprocessedSchematron = new AtomicBoolean (false);
  private static final AtomicBoolean s_aShowResolvedSourceSchematron = new AtomicBoolean (false);

  private SchematronDebug ()
  {}

  /**
   * Globally enable/disable debug mode
   *
   * @param bDebugMode
   *        true to enable debug mode, false
   *        otherwise.
   * @see #setSaveIntermediateXSLTFiles(boolean)
   * @see #setShowCreatedXSLT(boolean)
   * @see #setShowCreatedSVRL(boolean)
   * @see #setShowResolvedSourceSchematron(boolean)
   * @see #setShowPreprocessedSchematron(boolean)
   */
  public static void setDebugMode (final boolean bDebugMode)
  {
    setSaveIntermediateXSLTFiles (bDebugMode);
    setShowCreatedXSLT (bDebugMode);
    setShowCreatedSVRL (bDebugMode);
    setShowResolvedSourceSchematron (bDebugMode);
    setShowPreprocessedSchematron (bDebugMode);
  }

  /**
   * @return true if the intermediate files during XSLT creation.
   *         Applied only in XSTL based modes.
   */
  public static boolean isSaveIntermediateXSLTFiles ()
  {
    return s_bSaveIntermediateXSLTFiles.get ();
  }

  public static void setSaveIntermediateXSLTFiles (final boolean bSaveIntermediateFiles)
  {
    s_bSaveIntermediateXSLTFiles.set (bSaveIntermediateFiles);
  }

  /**
   * @return The folder to which the minified SCH should be stored. Never
   *         null. Only used in XSLT based modes if
   *         {@link #isSaveIntermediateXSLTFiles()} is true.
   */
  @Nonnull
  public static File getIntermediateMinifiedSCHFolder ()
  {
    return s_aIntermediateMinifiedSCHFolder;
  }

  public static void setIntermediateMinifiedSCHFolder (@Nonnull final File aIntermediateMinifiedSCHFolder)
  {
    ValueEnforcer.notNull (aIntermediateMinifiedSCHFolder, "IntermediateMinifiedSCHFolder");
    s_aIntermediateMinifiedSCHFolder = aIntermediateMinifiedSCHFolder;
  }

  /**
   * @return The folder to which the final XSLT should be stored. Never
   *         null. Only used in XSLT based modes if
   *         {@link #isSaveIntermediateXSLTFiles()} is true.
   */
  @Nonnull
  public static File getIntermediateFinalXSLTFolder ()
  {
    return s_aIntermediateFinalXSLTFolder;
  }

  public static void setIntermediateFinalXSLTFolder (@Nonnull final File aIntermediateFinalXSLTFolder)
  {
    ValueEnforcer.notNull (aIntermediateFinalXSLTFolder, "IntermediateFinalXSLTFolder");
    s_aIntermediateFinalXSLTFolder = aIntermediateFinalXSLTFolder;
  }

  public static void setShowCreatedXSLT (final boolean bShow)
  {
    s_aShowCreatedXSLT.set (bShow);
  }

  /**
   * @return true if the created XSLT should be logged. Only
   *         applied in XSLT based mode.
   */
  public static boolean isShowCreatedXSLT ()
  {
    return s_aShowCreatedXSLT.get ();
  }

  public static void setShowCreatedSVRL (final boolean bShow)
  {
    s_aShowCreatedSVRL.set (bShow);
  }

  /**
   * @return true to log the created SVRL.
   */
  public static boolean isShowCreatedSVRL ()
  {
    return s_aShowCreatedSVRL.get ();
  }

  public static void setShowResolvedSourceSchematron (final boolean bShow)
  {
    s_aShowResolvedSourceSchematron.set (bShow);
  }

  /**
   * @return true to log the read, with includes resolved,
   *         Schematron. This is only applied in pure mode.
   */
  public static boolean isShowResolvedSourceSchematron ()
  {
    return s_aShowResolvedSourceSchematron.get ();
  }

  public static void setShowPreprocessedSchematron (final boolean bShow)
  {
    s_aShowPreprocessedSchematron.set (bShow);
  }

  /**
   * @return true to log the created preprocessed Schematron. This
   *         is only applied in pure mode.
   */
  public static boolean isShowPreprocessedSchematron ()
  {
    return s_aShowPreprocessedSchematron.get ();
  }
}


/**
 * Copyright (C) 2014-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.pure.bound;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.xpath.XPathFunctionResolver;
import javax.xml.xpath.XPathVariableResolver;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.EntityResolver;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.hashcode.HashCodeGenerator;
import com.helger.commons.hashcode.IHashCodeGenerator;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.SchematronDebug;
import com.helger.schematron.SchematronException;
import com.helger.schematron.pure.binding.IPSQueryBinding;
import com.helger.schematron.pure.binding.PSQueryBindingRegistry;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.exchange.PSReader;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.schematron.pure.preprocess.PSPreprocessor;
import com.helger.schematron.pure.preprocess.SchematronPreprocessException;
import com.helger.xml.microdom.serialize.MicroWriter;

/**
 * This class represents keys for the {@link PSBoundSchemaCache}. It is a
 * combination of a resource and a phase. It is the responsible class for
 * reading and binding a Schematron resource.
 *
 * @author Philip Helger
 */
@Immutable
public class PSBoundSchemaCacheKey
{
  private static final Logger LOGGER = LoggerFactory.getLogger (PSBoundSchemaCacheKey.class);

  private final IReadableResource m_aResource;
  private final String m_sPhase;
  private final IPSErrorHandler m_aErrorHandler;
  private final XPathVariableResolver m_aVariableResolver;
  private final XPathFunctionResolver m_aFunctionResolver;
  private final EntityResolver m_aEntityResolver;
  // Status vars
  private transient int m_nHashCode = IHashCodeGenerator.ILLEGAL_HASHCODE;

  public PSBoundSchemaCacheKey (@Nonnull final IReadableResource aResource,
                                @Nullable final String sPhase,
                                @Nullable final IPSErrorHandler aErrorHandler,
                                @Nullable final XPathVariableResolver aVariableResolver,
                                @Nullable final XPathFunctionResolver aFunctionResolver,
                                @Nullable final EntityResolver aEntityResolver)
  {
    ValueEnforcer.notNull (aResource, "Resource");

    m_aResource = aResource;
    m_sPhase = sPhase;
    m_aErrorHandler = aErrorHandler;
    m_aVariableResolver = aVariableResolver;
    m_aFunctionResolver = aFunctionResolver;
    m_aEntityResolver = aEntityResolver;
  }

  /**
   * @return The resource passed in the constructor. Never null.
   */
  @Nonnull
  public final IReadableResource getResource ()
  {
    return m_aResource;
  }

  /**
   * @return The phase selected in the constructor. May be null.
   */
  @Nullable
  public final String getPhase ()
  {
    return m_sPhase;
  }

  /**
   * @return The error handler passed in the constructor. May be
   *         null.
   */
  @Nullable
  public final IPSErrorHandler getErrorHandler ()
  {
    return m_aErrorHandler;
  }

  /**
   * @return The variable resolver to be used. May be null.
   */
  @Nullable
  public final XPathVariableResolver getVariableResolver ()
  {
    return m_aVariableResolver;
  }

  /**
   * @return The function resolver to be used. May be null.
   */
  @Nullable
  public final XPathFunctionResolver getFunctionResolver ()
  {
    return m_aFunctionResolver;
  }

  /**
   * @return The XML entity resolver to be used. May be null.
   */
  @Nullable
  public final EntityResolver getEntityResolver ()
  {
    return m_aEntityResolver;
  }

  /**
   * Read the specified schema from the passed resource.
   *
   * @param aResource
   *        The resource to read from. Never null.
   * @param aErrorHandler
   *        The error handler to use. May be null.
   * @param aEntityResolver
   *        The XML entity resolver to be used. May be null.
   * @return The read schema. May not be null.
   * @throws SchematronException
   *         In case there is an error reading.
   */
  @Nonnull
  @OverrideOnDemand
  public PSSchema readSchema (@Nonnull final IReadableResource aResource,
                              @Nullable final IPSErrorHandler aErrorHandler,
                              @Nullable final EntityResolver aEntityResolver) throws SchematronException
  {
    return new PSReader (aResource, aErrorHandler, aEntityResolver).readSchema ();
  }

  /**
   * Determine the query binding for the read schema.
   *
   * @param aSchema
   *        The read schema. Never null.
   * @return The query binding to use. Never null.
   * @throws SchematronException
   *         In case the determination fails.
   */
  @Nonnull
  @OverrideOnDemand
  public IPSQueryBinding getQueryBinding (@Nonnull final PSSchema aSchema) throws SchematronException
  {
    return PSQueryBindingRegistry.getQueryBindingOfNameOrThrow (aSchema.getQueryBinding ());
  }

  /**
   * Create the pre-processor to be used for
   * {@link #createPreprocessedSchema(PSSchema, IPSQueryBinding)}.
   *
   * @param aQueryBinding
   *        The query binding to be determined from the read schema. Never
   *        null.
   * @return The pre-processor to be used.
   */
  @Nonnull
  @OverrideOnDemand
  public PSPreprocessor createPreprocessor (@Nonnull final IPSQueryBinding aQueryBinding)
  {
    final PSPreprocessor aPreprocessor = PSPreprocessor.createPreprocessorWithoutInformationLoss (aQueryBinding);
    return aPreprocessor;
  }

  /**
   * Pre-process the read schema, using the determined query binding.
   *
   * @param aSchema
   *        The read schema. Never null.
   * @param aQueryBinding
   *        The determined query binding. Never null.
   * @return The pre-processed schema and never null.
   * @throws SchematronException
   *         In case pre-processing fails
   */
  @Nonnull
  @OverrideOnDemand
  public PSSchema createPreprocessedSchema (@Nonnull final PSSchema aSchema,
                                            @Nonnull final IPSQueryBinding aQueryBinding) throws SchematronException
  {
    final PSPreprocessor aPreprocessor = createPreprocessor (aQueryBinding);
    final PSSchema aPreprocessedSchema = aPreprocessor.getAsPreprocessedSchema (aSchema);
    if (aPreprocessedSchema == null)
      throw new SchematronPreprocessException ("Failed to preprocess schema " +
                                               aSchema +
                                               " with query binding " +
                                               aQueryBinding);
    if (SchematronDebug.isShowPreprocessedSchematron ())
      LOGGER.info ("Preprocessed Schematron:\n" +
                      MicroWriter.getNodeAsString (aPreprocessedSchema.getAsMicroElement ()));
    return aPreprocessedSchema;
  }

  /**
   * The main routine to create a bound schema from the passed resource and
   * phase. The usual routine is to
   *

   *
read the schema from the resource - see
   * {@link #readSchema(IReadableResource, IPSErrorHandler, EntityResolver)}

   *
resolve the query binding - see {@link #getQueryBinding(PSSchema)}

   *
pre-process the schema -
   * {@link #createPreprocessedSchema(PSSchema, IPSQueryBinding)}

   *
and finally bind it -
   * {@link IPSQueryBinding#bind(PSSchema, String, IPSErrorHandler, javax.xml.xpath.XPathVariableResolver, javax.xml.xpath.XPathFunctionResolver)}
   *

   *

   *
   * @return The bound schema. Never null.
   * @throws SchematronException
   *         In case reading or binding fails.
   */
  @Nonnull
  public IPSBoundSchema createBoundSchema () throws SchematronException
  {
    // Read schema from resource
    final PSSchema aSchema = readSchema (getResource (), getErrorHandler (), getEntityResolver ());

    // Resolve the query binding to be used
    final IPSQueryBinding aQueryBinding = getQueryBinding (aSchema);

    // Pre-process schema
    final PSSchema aPreprocessedSchema = createPreprocessedSchema (aSchema, aQueryBinding);

    // And finally bind the pre-processed schema
    return aQueryBinding.bind (aPreprocessedSchema,
                               getPhase (),
                               getErrorHandler (),
                               getVariableResolver (),
                               getFunctionResolver ());
  }

  @Override
  public boolean equals (final Object o)
  {
    if (o == this)
      return true;
    if (o == null || !getClass ().equals (o.getClass ()))
      return false;
    final PSBoundSchemaCacheKey rhs = (PSBoundSchemaCacheKey) o;
    return m_aResource.equals (rhs.m_aResource) &&
           EqualsHelper.equals (m_sPhase, rhs.m_sPhase) &&
           EqualsHelper.equals (m_aVariableResolver, rhs.m_aVariableResolver) &&
           EqualsHelper.equals (m_aFunctionResolver, rhs.m_aFunctionResolver);
  }

  @Override
  public int hashCode ()
  {
    int ret = m_nHashCode;
    if (ret == IHashCodeGenerator.ILLEGAL_HASHCODE)
      ret = m_nHashCode = new HashCodeGenerator (this).append (m_aResource)
                                                      .append (m_sPhase)
                                                      .append (m_aVariableResolver)
                                                      .append (m_aFunctionResolver)
                                                      .getHashCode ();
    return ret;
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("resource", m_aResource)
                                       .append ("phase", m_sPhase)
                                       .appendIfNotNull ("errorHandler", m_aErrorHandler)
                                       .appendIfNotNull ("variableResolver", m_aVariableResolver)
                                       .appendIfNotNull ("functionResolver", m_aFunctionResolver)
                                       .getToString ();
  }
}


/**
 * Copyright (C) 2014-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.pure.exchange;

import java.io.File;
import java.io.OutputStream;
import java.io.Writer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.state.ESuccess;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.pure.model.IPSElement;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.IMicroNode;
import com.helger.xml.microdom.MicroDocument;
import com.helger.xml.microdom.serialize.MicroWriter;

/**
 * This class serializes the Schematron created within the domain object
 *
 * @author Philip Helger
 */
public class PSWriter
{
  private final IPSWriterSettings m_aWriterSettings;

  /**
   * Constructor using the default {@link PSWriterSettings} instance.
   */
  public PSWriter ()
  {
    this (PSWriterSettings.DEFAULT_SETTINGS);
  }

  /**
   * Constructor.
   *
   * @param aWriterSettings
   *        The writer settings to be used. May not be null.
   */
  public PSWriter (@Nonnull final IPSWriterSettings aWriterSettings)
  {
    m_aWriterSettings = ValueEnforcer.notNull (aWriterSettings, "WriterSettings");
  }

  /**
   * @return The writer settings specified in the constructor.
   */
  @Nonnull
  public IPSWriterSettings getWriterSettings ()
  {
    return m_aWriterSettings;
  }

  @Nonnull
  @OverrideOnDemand
  protected IMicroNode getAsDocument (@Nonnull final IMicroElement aElement)
  {
    final IMicroDocument aDoc = new MicroDocument ();
    aDoc.appendChild (aElement);
    return aDoc;
  }

  /**
   * Write the passed Schematron element to the passed file.
   *
   * @param aPSElement
   *        The schematron element to write. May not be null.
   * @param aFile
   *        The file to write things to. May not be null.
   * @return {@link ESuccess}.
   */
  @Nonnull
  public ESuccess writeToFile (@Nonnull final IPSElement aPSElement, @Nonnull final File aFile)
  {
    ValueEnforcer.notNull (aPSElement, "PSElement");
    final IMicroElement eXML = aPSElement.getAsMicroElement ();
    return MicroWriter.writeToFile (getAsDocument (eXML), aFile, m_aWriterSettings.getXMLWriterSettings ());
  }

  /**
   * Write the passed Schematron element to the passed output stream.
   *
   * @param aPSElement
   *        The schematron element to write. May not be null.
   * @param aOS
   *        The output stream to write things to. May not be null.
   *        The stream is automatically closed.
   * @return {@link ESuccess}.
   */
  @Nonnull
  public ESuccess writeToStream (@Nonnull final IPSElement aPSElement, @Nonnull @WillClose final OutputStream aOS)
  {
    ValueEnforcer.notNull (aPSElement, "PSElement");
    final IMicroElement eXML = aPSElement.getAsMicroElement ();
    return MicroWriter.writeToStream (getAsDocument (eXML), aOS, m_aWriterSettings.getXMLWriterSettings ());
  }

  /**
   * Write the passed Schematron element to the passed writer.
   *
   * @param aPSElement
   *        The schematron element to write. May not be null.
   * @param aWriter
   *        The writer to write things to. May not be null. The
   *        writer is automatically closed.
   * @return {@link ESuccess}.
   */
  @Nonnull
  public ESuccess writeToWriter (@Nonnull final IPSElement aPSElement, @Nonnull @WillClose final Writer aWriter)
  {
    ValueEnforcer.notNull (aPSElement, "PSElement");
    final IMicroElement eXML = aPSElement.getAsMicroElement ();
    return MicroWriter.writeToWriter (getAsDocument (eXML), aWriter, m_aWriterSettings.getXMLWriterSettings ());
  }

  /**
   * Get the passed Schematron element as a String
   *
   * @param aPSElement
   *        The schematron element to convert to a string. May not be
   *        null.
   * @return The passed element as a string or null if
   *         serialization failed.
   */
  @Nullable
  public String getXMLString (@Nonnull final IPSElement aPSElement)
  {
    ValueEnforcer.notNull (aPSElement, "PSElement");
    final IMicroElement eXML = aPSElement.getAsMicroElement ();
    return MicroWriter.getNodeAsString (getAsDocument (eXML), m_aWriterSettings.getXMLWriterSettings ());
  }

  /**
   * Get the passed Schematron element as a String
   *
   * @param aPSElement
   *        The schematron element to convert to a string. May not be
   *        null.
   * @return The passed element as a string and never null.
   * @throws IllegalStateException
   *         if serialization failed
   */
  @Nullable
  public String getXMLStringNotNull (@Nonnull final IPSElement aPSElement)
  {
    final String ret = getXMLString (aPSElement);
    if (ret == null)
      throw new IllegalStateException ("Failed to serialize the passed PS element: " + aPSElement);
    return ret;
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("writerSettings", m_aWriterSettings).getToString ();
  }
}