/**
 * 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.IOException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;

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

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.error.list.ErrorList;
import com.helger.commons.error.list.IErrorList;
import com.helger.commons.hierarchy.visit.ChildrenProviderHierarchyVisitor;
import com.helger.commons.hierarchy.visit.DefaultHierarchyVisitorCallback;
import com.helger.commons.hierarchy.visit.EHierarchyVisitorReturn;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.state.ESuccess;
import com.helger.commons.wrapper.Wrapper;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler;
import com.helger.schematron.resolve.DefaultSchematronIncludeResolver;
import com.helger.schematron.svrl.SVRLFailedAssert;
import com.helger.schematron.svrl.SVRLHelper;
import com.helger.schematron.svrl.SVRLResourceError;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.IMicroNode;
import com.helger.xml.microdom.serialize.MicroReader;
import com.helger.xml.sax.InputSourceFactory;
import com.helger.xml.serialize.read.ISAXReaderSettings;

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

/**
 * This is a helper class that provides a way to easily apply an Schematron
 * resource on an XML resource.
 *
 * @author Philip Helger
 */
@Immutable
public final class SchematronHelper
{
  private static final Logger LOGGER = LoggerFactory.getLogger (SchematronHelper.class);

  @PresentForCodeCoverage
  private static final SchematronHelper s_aInstance = new SchematronHelper ();

  private SchematronHelper ()
  {}

  /**
   * Apply the passed schematron on the passed XML resource using a custom error
   * handler.
   *
   * @param aSchematron
   *        The Schematron resource. May not be null.
   * @param aXML
   *        The XML resource. May not be null.
   * @return null if either the Schematron or the XML could not be
   *         read.
   * @throws IllegalStateException
   *         if the processing throws an unexpected exception.
   */
  @Nullable
  public static SchematronOutputType applySchematron (@Nonnull final ISchematronResource aSchematron,
                                                      @Nonnull final IReadableResource aXML)
  {
    ValueEnforcer.notNull (aSchematron, "SchematronResource");
    ValueEnforcer.notNull (aXML, "XMLSource");

    try
    {
      // Apply Schematron on XML
      return aSchematron.applySchematronValidationToSVRL (aXML);
    }
    catch (final Exception ex)
    {
      throw new IllegalArgumentException ("Failed to apply Schematron " +
                                          aSchematron.getID () +
                                          " onto XML resource " +
                                          aXML.getResourceID (),
                                          ex);
    }
  }

  /**
   * Apply the passed schematron on the passed XML resource.
   *
   * @param aSchematron
   *        The Schematron resource. May not be null.
   * @param aXML
   *        The XML resource. May not be null.
   * @return null if either the Schematron or the XML could not be
   *         read.
   * @throws IllegalStateException
   *         if the processing throws an unexpected exception.
   */
  @Nullable
  public static SchematronOutputType applySchematron (@Nonnull final ISchematronResource aSchematron,
                                                      @Nonnull final Source aXML)
  {
    ValueEnforcer.notNull (aSchematron, "SchematronResource");
    ValueEnforcer.notNull (aXML, "XMLSource");

    try
    {
      // Apply Schematron on XML.
      return aSchematron.applySchematronValidationToSVRL (aXML);
    }
    catch (final Exception ex)
    {
      throw new IllegalArgumentException ("Failed to apply Schematron " +
                                          aSchematron.getID () +
                                          " onto XML source " +
                                          aXML,
                                          ex);
    }
  }

  /**
   * Apply the passed schematron on the passed XML node.
   *
   * @param aSchematron
   *        The Schematron resource. May not be null.
   * @param aNode
   *        The XML node. May not be null.
   * @return null if either the Schematron or the XML could not be
   *         read.
   * @throws IllegalStateException
   *         if the processing throws an unexpected exception.
   */
  @Nullable
  public static SchematronOutputType applySchematron (@Nonnull final ISchematronResource aSchematron,
                                                      @Nonnull final Node aNode)
  {
    ValueEnforcer.notNull (aSchematron, "SchematronResource");
    ValueEnforcer.notNull (aNode, "Node");

    return applySchematron (aSchematron, new DOMSource (aNode));
  }

  /**
   * Convert a {@link SchematronOutputType} to an {@link IErrorList}.
   *
   * @param aSchematronOutput
   *        The result of Schematron validation
   * @param sResourceName
   *        The name of the resource that was validated (may be a file path
   *        etc.)
   * @return List non-null error list of {@link SVRLResourceError}
   *         objects.
   */
  @Nonnull
  public static IErrorList convertToErrorList (@Nonnull final SchematronOutputType aSchematronOutput,
                                               @Nullable final String sResourceName)
  {
    ValueEnforcer.notNull (aSchematronOutput, "SchematronOutput");

    final ErrorList ret = new ErrorList ();
    for (final SVRLFailedAssert aFailedAssert : SVRLHelper.getAllFailedAssertions (aSchematronOutput))
      ret.add (aFailedAssert.getAsResourceError (sResourceName));
    return ret;
  }

  @SuppressFBWarnings ("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
  @Nonnull
  private static ESuccess _recursiveResolveAllSchematronIncludes (@Nonnull final IMicroElement eRoot,
                                                                  @Nonnull final IReadableResource aResource,
                                                                  @Nullable final ISAXReaderSettings aSettings,
                                                                  @Nonnull final IPSErrorHandler aErrorHandler)
  {
    if (eRoot != null)
    {
      final DefaultSchematronIncludeResolver aIncludeResolver = new DefaultSchematronIncludeResolver (aResource);

      for (final IMicroElement aElement : eRoot.getAllChildElementsRecursive ())
        if (CSchematron.NAMESPACE_SCHEMATRON.equals (aElement.getNamespaceURI ()) &&
            aElement.getLocalName ().equals (CSchematronXML.ELEMENT_INCLUDE))
        {
          String sHref = aElement.getAttributeValue (CSchematronXML.ATTR_HREF);
          try
          {
            final int nHashIndex = sHref.indexOf ('#');
            String sAnchor = null;
            if (nHashIndex >= 0)
            {
              sAnchor = sHref.substring (nHashIndex + 1);
              sHref = sHref.substring (0, nHashIndex);
            }

            final IReadableResource aIncludeRes = aIncludeResolver.getResolvedSchematronResource (sHref);
            if (aIncludeRes == null)
            {
              aErrorHandler.error (aResource, null, "Failed to resolve include '" + sHref + "'", null);
              return ESuccess.FAILURE;
            }

            if (LOGGER.isDebugEnabled ())
              LOGGER.debug ("Resolved '" +
                               sHref +
                               "' relative to '" +
                               aIncludeResolver.getBaseHref () +
                               "' as '" +
                               aIncludeRes.getPath () +
                               "'");

            // Read XML to be included
            final IMicroDocument aIncludedDoc = MicroReader.readMicroXML (aIncludeRes, aSettings);
            if (aIncludedDoc == null)
            {
              aErrorHandler.error (aResource, null, "Failed to parse include " + aIncludeRes, null);
              return ESuccess.FAILURE;
            }

            IMicroElement aIncludedContent;
            if (sAnchor == null)
            {
              // no anchor present - include the whole document

              // Return the document element
              aIncludedContent = aIncludedDoc.getDocumentElement ();
            }
            else
            {
              final String sFinalAnchor = sAnchor;
              final Wrapper <IMicroElement> aMatch = new Wrapper <> ();
              // Also include the root element in the search
              ChildrenProviderHierarchyVisitor.visitFrom (aIncludedDoc.getDocumentElement (),
                                                          new DefaultHierarchyVisitorCallback <IMicroNode> ()
                                                          {
                                                            @Override
                                                            public EHierarchyVisitorReturn onItemBeforeChildren (final IMicroNode aItem)
                                                            {
                                                              if (aItem.isElement ())
                                                              {
                                                                final IMicroElement aCurElement = (IMicroElement) aItem;
                                                                final String sID = aCurElement.getAttributeValue ("id");
                                                                if (sFinalAnchor.equals (sID))
                                                                  aMatch.set (aCurElement);
                                                              }
                                                              return EHierarchyVisitorReturn.CONTINUE;
                                                            }
                                                          },
                                                          true);
              aIncludedContent = aMatch.get ();
              if (aIncludedContent == null)
              {
                aErrorHandler.warn (aResource,
                                    null,
                                    "Failed to resolve an element with the ID '" +
                                          sAnchor +
                                          "' in " +
                                          aIncludeRes +
                                          "! Therefore including the whole document!");
                aIncludedContent = aIncludedDoc.getDocumentElement ();
              }
            }

            // Important to detach from parent!
            aIncludedContent.detachFromParent ();

            // Check for correct namespace URI of included content
            if (!CSchematron.NAMESPACE_SCHEMATRON.equals (aIncludedContent.getNamespaceURI ()))
            {
              aErrorHandler.error (aResource,
                                   null,
                                   "The included resource " +
                                         aIncludeRes +
                                         " contains the wrong XML namespace URI '" +
                                         aIncludedContent.getNamespaceURI () +
                                         "' but was expected to have '" +
                                         CSchematron.NAMESPACE_SCHEMATRON +
                                         "'",
                                   null);
              return ESuccess.FAILURE;
            }

            // Check that not a whole Schema but only a part is included
            if (CSchematronXML.ELEMENT_SCHEMA.equals (aIncludedContent.getLocalName ()))
            {
              aErrorHandler.warn (aResource,
                                  null,
                                  "The included resource " +
                                        aIncludeRes +
                                        " seems to be a complete schema. To includes parts of a schema the respective element must be the root element of the included resource.");
            }

            // Recursive resolve includes
            if (_recursiveResolveAllSchematronIncludes (aIncludedContent,
                                                        aIncludeRes,
                                                        aSettings,
                                                        aErrorHandler).isFailure ())
              return ESuccess.FAILURE;

            // Now replace "include" element with content in MicroDOM
            aElement.getParent ().replaceChild (aElement, aIncludedContent);
          }
          catch (final IOException ex)
          {
            aErrorHandler.error (aResource, null, "Failed to read include '" + sHref + "'", ex);
            return ESuccess.FAILURE;
          }
        }
    }
    return ESuccess.SUCCESS;
  }

  /**
   * Resolve all Schematron includes of the passed resource.
   *
   * @param aResource
   *        The Schematron resource to read. May not be null.
   * @return null if the passed resource could not be read as XML
   *         document
   */
  @Nullable
  public static IMicroDocument getWithResolvedSchematronIncludes (@Nonnull final IReadableResource aResource)
  {
    return getWithResolvedSchematronIncludes (aResource, (ISAXReaderSettings) null, new LoggingPSErrorHandler ());
  }

  /**
   * Resolve all Schematron includes of the passed resource.
   *
   * @param aResource
   *        The Schematron resource to read. May not be null.
   * @param aSettings
   *        The SAX reader settings to be used. May be null to use
   *        the default settings.
   * @param aErrorHandler
   *        The error handler to be used. May not be null.
   * @return null if the passed resource could not be read as XML
   *         document
   */
  @Nullable
  public static IMicroDocument getWithResolvedSchematronIncludes (@Nonnull final IReadableResource aResource,
                                                                  @Nullable final ISAXReaderSettings aSettings,
                                                                  @Nonnull final IPSErrorHandler aErrorHandler)
  {
    final InputSource aIS = InputSourceFactory.create (aResource);
    final IMicroDocument aDoc = MicroReader.readMicroXML (aIS, aSettings);
    if (aDoc != null)
    {
      // Resolve all Schematron includes
      if (_recursiveResolveAllSchematronIncludes (aDoc.getDocumentElement (),
                                                  aResource,
                                                  aSettings,
                                                  aErrorHandler).isFailure ())
      {
        // Error resolving includes
        return null;
      }
    }
    return aDoc;
  }
}


/**
 * 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.xslt;

import java.io.File;

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

import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.io.resource.ClassPathResource;
import com.helger.commons.io.resource.FileSystemResource;
import com.helger.commons.io.resource.IReadableResource;

/**
 * A Schematron resource that is based on the original SCH file.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class SchematronResourceSCH extends AbstractSchematronXSLTBasedResource <SchematronResourceSCH>
{
  private String m_sPhase;
  private String m_sLanguageCode;

  /**
   * Constructor
   *
   * @param aSCHResource
   *        The Schematron resource. May not be null.
   */
  public SchematronResourceSCH (@Nonnull final IReadableResource aSCHResource)
  {
    super (aSCHResource);
  }

  @Nullable
  public String getPhase ()
  {
    return m_sPhase;
  }

  public void setPhase (@Nullable final String sPhase)
  {
    m_sPhase = sPhase;
  }

  @Nullable
  public String getLanguageCode ()
  {
    return m_sLanguageCode;
  }

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

  @Nonnull
  @OverrideOnDemand
  protected SCHTransformerCustomizer createTransformerCustomizer ()
  {
    return new SCHTransformerCustomizer ().setErrorListener (getErrorListener ())
                                          .setURIResolver (getURIResolver ())
                                          .setParameters (parameters ())
                                          .setPhase (m_sPhase)
                                          .setLanguageCode (m_sLanguageCode);
  }

  @Override
  @Nullable
  public ISchematronXSLTBasedProvider getXSLTProvider ()
  {
    final SCHTransformerCustomizer aTransformerCustomizer = createTransformerCustomizer ();
    if (isUseCache ())
      return SchematronResourceSCHCache.getSchematronXSLTProvider (getResource (), aTransformerCustomizer);

    // Always create a new one
    return SchematronResourceSCHCache.createSchematronXSLTProvider (getResource (), aTransformerCustomizer);
  }

  /**
   * Create a new Schematron resource.
   *
   * @param sSCHPath
   *        The classpath relative path to the Schematron file. May neither be
   *        null nor empty.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourceSCH fromClassPath (@Nonnull @Nonempty final String sSCHPath)
  {
    return new SchematronResourceSCH (new ClassPathResource (sSCHPath));
  }

  /**
   * Create a new Schematron resource.
   *
   * @param sSCHPath
   *        The file system path to the Schematron file. May neither be
   *        null nor empty.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourceSCH fromFile (@Nonnull @Nonempty final String sSCHPath)
  {
    return new SchematronResourceSCH (new FileSystemResource (sSCHPath));
  }

  /**
   * Create a new Schematron resource.
   *
   * @param aSCHFile
   *        The Schematron file. May not be null.
   * @return Never null.
   */
  @Nonnull
  public static SchematronResourceSCH fromFile (@Nonnull final File aSCHFile)
  {
    return new SchematronResourceSCH (new FileSystemResource (aSCHFile));
  }
}


/**
 * 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.binding.xpath;

import java.util.Map;

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

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsTreeMap;
import com.helger.commons.collection.impl.ICommonsNavigableMap;
import com.helger.commons.compare.IComparator;
import com.helger.commons.state.EChange;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;

/**
 * This class manages all variables present in Schematron <let> elements.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PSXPathVariables implements IPSXPathVariables
{
  @Nonnull
  @ReturnsMutableCopy
  private static ICommonsNavigableMap <String, String> _createMap ()
  {
    return new CommonsTreeMap <> (IComparator.getComparatorStringLongestFirst ());
  }

  private final ICommonsNavigableMap <String, String> m_aMap;

  public PSXPathVariables ()
  {
    m_aMap = _createMap ();
  }

  public PSXPathVariables (@Nonnull final IPSXPathVariables aOther)
  {
    m_aMap = aOther.getAll ();
  }

  /**
   * Add a new variable.
   *
   * @param aEntry
   *        The entry to be added - key is the variable name and value is the
   *        variable value. May not be null.
   * @return {@link EChange#UNCHANGED} if a variable with the same name is
   *         already present. Never null.
   */
  @Nonnull
  public EChange add (@Nonnull final Map.Entry <String, String> aEntry)
  {
    return add (aEntry.getKey (), aEntry.getValue ());
  }

  /**
   * Add a new variable.
   *
   * @param sName
   *        The name of the variable. May neither be null nor
   *        empty.
   * @param sValue
   *        The value of the variable. May neither be null nor
   *        empty.
   * @return {@link EChange#UNCHANGED} if a variable with the same name is
   *         already present. Never null.
   */
  @Nonnull
  public EChange add (@Nonnull @Nonempty final String sName, @Nonnull @Nonempty final String sValue)
  {
    ValueEnforcer.notEmpty (sName, "Name");
    ValueEnforcer.notEmpty (sValue, "Value");

    // Prepend the "$" prefix to the variable name
    final String sRealName = PSXPathQueryBinding.PARAM_VARIABLE_PREFIX + sName;
    if (m_aMap.containsKey (sRealName))
      return EChange.UNCHANGED;

    // Apply all existing variables to this variable value!
    // This ensures that all variables used in variables are resolved correctly
    // as long as the order of declaration is correct.
    // Additionally this means that the order of the variables in this class is
    // irrelevant
    final String sRealValue = getAppliedReplacement (sValue);
    m_aMap.put (sRealName, sRealValue);
    return EChange.CHANGED;
  }

  @Nullable
  public String getAppliedReplacement (@Nullable final String sText)
  {
    return PSXPathQueryBinding.getWithParamTextsReplacedStatic (sText, m_aMap);
  }

  /**
   * Remove the variable with the specified name
   *
   * @param sVarName
   *        The name of the variable to be removed. May be null.
   * @return {@link EChange#CHANGED} if the variable was removed successfully.
   *         Never null.
   */
  @Nonnull
  public EChange remove (@Nullable final String sVarName)
  {
    if (StringHelper.hasText (sVarName))
      if (m_aMap.remove (PSXPathQueryBinding.PARAM_VARIABLE_PREFIX + sVarName) == null)
        return EChange.CHANGED;
    return EChange.UNCHANGED;
  }

  /**
   * Remove all variables with the specified names
   *
   * @param aVars
   *        A list of variable names to be removed. May be null.
   * @return {@link EChange#CHANGED} if at least one variable was removed
   *         successfully. Never null.
   */
  @Nonnull
  public EChange removeAll (@Nullable final Iterable <String> aVars)
  {
    EChange eChange = EChange.UNCHANGED;
    if (aVars != null)
      for (final String sName : aVars)
        eChange = eChange.or (remove (sName));
    return eChange;
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsNavigableMap <String, String> getAll ()
  {
    return m_aMap.getClone ();
  }

  public boolean contains (@Nullable final String sName)
  {
    if (StringHelper.hasNoText (sName))
      return false;
    return m_aMap.containsKey (sName);
  }

  @Nullable
  public String get (@Nullable final String sName)
  {
    if (StringHelper.hasNoText (sName))
      return null;
    return m_aMap.get (sName);
  }

  @Nonnull
  @ReturnsMutableCopy
  public PSXPathVariables getClone ()
  {
    return new PSXPathVariables (this);
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("map", m_aMap).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.svrl;

import java.util.regex.Matcher;

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

import org.oclc.purl.dsdl.svrl.FailedAssert;
import org.oclc.purl.dsdl.svrl.SchematronOutputType;
import org.oclc.purl.dsdl.svrl.SuccessfulReport;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.error.level.IErrorLevel;
import com.helger.commons.regex.RegExHelper;
import com.helger.commons.string.StringHelper;

/**
 * Miscellaneous utility methods for handling Schematron output (SVRL).
 *
 * @author Philip Helger
 */
@ThreadSafe
public final class SVRLHelper
{
  private static final SimpleReadWriteLock s_aRWLock = new SimpleReadWriteLock ();

  private static ISVRLErrorLevelDeterminator s_aELD = new DefaultSVRLErrorLevelDeterminator ();

  @PresentForCodeCoverage
  private static final SVRLHelper s_aInstance = new SVRLHelper ();

  private SVRLHelper ()
  {}

  /**
   * Get a list of all failed assertions in a given schematron output.
   *
   * @param aSchematronOutput
   *        The schematron output to be used. May be null.
   * @return A non-null list with all failed assertions.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsList <SVRLFailedAssert> getAllFailedAssertions (@Nullable final SchematronOutputType aSchematronOutput)
  {
    final ICommonsList <SVRLFailedAssert> ret = new CommonsArrayList <> ();
    if (aSchematronOutput != null)
      for (final Object aObj : aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ())
        if (aObj instanceof FailedAssert)
          ret.add (new SVRLFailedAssert ((FailedAssert) aObj));
    return ret;
  }

  /**
   * Get a list of all failed assertions in a given schematron output, with an
   * error level equally or more severe than the passed error level.
   *
   * @param aSchematronOutput
   *        The schematron output to be used. May be null.
   * @param aErrorLevel
   *        Minimum error level to be queried
   * @return A non-null list with all failed assertions.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsList <SVRLFailedAssert> getAllFailedAssertionsMoreOrEqualSevereThan (@Nullable final SchematronOutputType aSchematronOutput,
                                                                                @Nonnull final IErrorLevel aErrorLevel)
  {
    final ICommonsList <SVRLFailedAssert> ret = new CommonsArrayList <> ();
    if (aSchematronOutput != null)
      for (final Object aObj : aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ())
        if (aObj instanceof FailedAssert)
        {
          final SVRLFailedAssert aFA = new SVRLFailedAssert ((FailedAssert) aObj);
          if (aFA.getFlag ().isGE (aErrorLevel))
            ret.add (aFA);
        }
    return ret;
  }

  /**
   * Get a list of all successful reports in a given schematron output.
   *
   * @param aSchematronOutput
   *        The schematron output to be used. May be null.
   * @return A non-null list with all successful reports.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsList <SVRLSuccessfulReport> getAllSuccessfulReports (@Nullable final SchematronOutputType aSchematronOutput)
  {
    final ICommonsList <SVRLSuccessfulReport> ret = new CommonsArrayList <> ();
    if (aSchematronOutput != null)
      for (final Object aObj : aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ())
        if (aObj instanceof SuccessfulReport)
          ret.add (new SVRLSuccessfulReport ((SuccessfulReport) aObj));
    return ret;
  }

  /**
   * Get a list of all successful reports in a given schematron output, with an
   * error level equally or more severe than the passed error level.
   *
   * @param aSchematronOutput
   *        The schematron output to be used. May be null.
   * @param aErrorLevel
   *        Minimum error level to be queried
   * @return A non-null list with all successful reports.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsList <SVRLSuccessfulReport> getAllSuccessfulReportsMoreOrEqualSevereThan (@Nullable final SchematronOutputType aSchematronOutput,
                                                                                @Nonnull final IErrorLevel aErrorLevel)
  {
    final ICommonsList <SVRLSuccessfulReport> ret = new CommonsArrayList <> ();
    if (aSchematronOutput != null)
      for (final Object aObj : aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ())
        if (aObj instanceof SuccessfulReport)
        {
          final SVRLSuccessfulReport aSR = new SVRLSuccessfulReport ((SuccessfulReport) aObj);
          if (aSR.getFlag ().isGE (aErrorLevel))
            ret.add (aSR);
        }
    return ret;
  }

  /**
   * Get a list of all failed assertions and successful reports in a given
   * schematron output.
   *
   * @param aSchematronOutput
   *        The schematron output to be used. May be null.
   * @return A non-null list with all failed assertions and
   *         successful reports. Maybe an empty list if the input is
   *         null.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsList <AbstractSVRLMessage> getAllFailedAssertionsAndSuccessfulReports (@Nullable final SchematronOutputType aSchematronOutput)
  {
    final ICommonsList <AbstractSVRLMessage> ret = new CommonsArrayList <> ();
    if (aSchematronOutput != null)
      for (final Object aObj : aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ())
        if (aObj instanceof FailedAssert)
          ret.add (new SVRLFailedAssert ((FailedAssert) aObj));
        else
          if (aObj instanceof SuccessfulReport)
            ret.add (new SVRLSuccessfulReport ((SuccessfulReport) aObj));
    return ret;
  }

  /**
   * Get the error level associated with a single failed assertion.
   *
   * @param aFailedAssert
   *        The failed assert to be queried. May not be null.
   * @return The error level and never null.
   */
  @Nonnull
  public static IErrorLevel getErrorLevelFromFailedAssert (@Nonnull final FailedAssert aFailedAssert)
  {
    return getErrorLevelDeterminator ().getErrorLevelFromFailedAssert (aFailedAssert);
  }

  /**
   * Get the error level associated with a single successful report.
   *
   * @param aSuccessfulReport
   *        The failed assert to be queried. May not be null.
   * @return The error level and never null.
   */
  @Nonnull
  public static IErrorLevel getErrorLevelFromSuccessfulReport (@Nonnull final SuccessfulReport aSuccessfulReport)
  {
    return getErrorLevelDeterminator ().getErrorLevelFromSuccessfulReport (aSuccessfulReport);
  }

  /**
   * @return The default error level determinator. May not be null.
   */
  @Nonnull
  public static ISVRLErrorLevelDeterminator getErrorLevelDeterminator ()
  {
    return s_aRWLock.readLocked ( () -> s_aELD);
  }

  /**
   * Set the global error level determinator.
   *
   * @param aELD
   *        The determinator to use. May not be null.
   */
  public static void setErrorLevelDeterminator (@Nonnull final ISVRLErrorLevelDeterminator aELD)
  {
    ValueEnforcer.notNull (aELD, "ErrorLevelDeterminator");

    s_aRWLock.readLocked ( () -> s_aELD = aELD);
  }

  /**
   * Convert an "unsexy" location string in the for, of
   * *:xx[namespace-uri()='yy'] to something more readable like
   * prefix:xx by using the mapping registered in the
   * {@link SVRLLocationBeautifierRegistry}.
   *
   * @param sLocation
   *        The original location string. May not be null.
   * @return The beautified string. Never null. Might be identical
   *         to the original string if the pattern was not found.
   * @since 5.0.1
   */
  @Nonnull
  public static String getBeautifiedLocation (@Nonnull final String sLocation)
  {
    String sResult = sLocation;
    // Handle namespaces:
    // Search for "*:xx[namespace-uri()='yy']" where xx is the localname and yy
    // is the namespace URI
    final Matcher aMatcher = RegExHelper.getMatcher ("\\Q*:\\E([a-zA-Z0-9_]+)\\Q[namespace-uri()='\\E([^']+)\\Q']\\E",
                                                     sResult);
    while (aMatcher.find ())
    {
      final String sLocalName = aMatcher.group (1);
      final String sNamespaceURI = aMatcher.group (2);

      // Check if there is a known beautifier for this pair of namespace and
      // local name
      final String sBeautified = SVRLLocationBeautifierRegistry.getBeautifiedLocation (sNamespaceURI, sLocalName);
      if (sBeautified != null)
        sResult = StringHelper.replaceAll (sResult, aMatcher.group (), sBeautified);
    }
    return sResult;
  }
}


/**
 * 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.binding;

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

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.exception.InitializationException;
import com.helger.schematron.pure.binding.xpath.PSXPathQueryBinding;

/**
 * The registry class for all available query bindings. To register your own
 * query binding, call {@link #registerQueryBinding(String, IPSQueryBinding)}.
 *
 * @author Philip Helger
 */
@ThreadSafe
public final class PSQueryBindingRegistry
{
  /**
   * Name of the query binding for which the default binding is registered.
   */
  public static final String QUERY_BINDING_XSLT = "xslt";

  /**
   * Name of the query binding for which the default binding is registered.
   */
  public static final String QUERY_BINDING_XSLT2 = "xslt2";

  /**
   * The default XPath binding object to be used
   */
  public static final IPSQueryBinding DEFAULT_QUERY_BINDING = PSXPathQueryBinding.getInstance ();

  private static final SimpleReadWriteLock s_aRWLock = new SimpleReadWriteLock ();
  private static final ICommonsMap <String, IPSQueryBinding> s_aMap = new CommonsHashMap <> ();

  static
  {
    try
    {
      registerQueryBinding (QUERY_BINDING_XSLT, DEFAULT_QUERY_BINDING);
      registerQueryBinding (QUERY_BINDING_XSLT2, DEFAULT_QUERY_BINDING);
    }
    catch (final SchematronBindException ex)
    {
      throw new InitializationException (ex);
    }
  }

  @PresentForCodeCoverage
  private static final PSQueryBindingRegistry s_aInstance = new PSQueryBindingRegistry ();

  private PSQueryBindingRegistry ()
  {}

  public static void registerQueryBinding (@Nonnull @Nonempty final String sName,
                                           @Nonnull final IPSQueryBinding aQueryBinding) throws SchematronBindException
  {
    ValueEnforcer.notEmpty (sName, "Name");
    ValueEnforcer.notNull (aQueryBinding, "QueryBinding");

    s_aRWLock.writeLockedThrowing ( () -> {
      if (s_aMap.containsKey (sName))
        throw new SchematronBindException ("A queryBinding with the name '" + sName + "' is already registered!");
      s_aMap.put (sName, aQueryBinding);
    });
  }

  /**
   * Get the query binding with the specified name.
   *
   * @param sName
   *        The name to the query binding to retrieve. May be null.
   *        If it is null than the {@link #DEFAULT_QUERY_BINDING}
   *        object is returned.
   * @return null if no such query binding was found.
   */
  @Nullable
  public static IPSQueryBinding getQueryBindingOfName (@Nullable final String sName)
  {
    if (sName == null)
      return DEFAULT_QUERY_BINDING;

    return s_aRWLock.readLocked ( () -> s_aMap.get (sName));
  }

  /**
   * Get the query binding with the specified name
   *
   * @param sName
   *        The name of the query binding to look up. May be null.
   * @return Never null.
   * @throws SchematronBindException
   *         In case the query binding could not be resolved!
   */
  @Nonnull
  public static IPSQueryBinding getQueryBindingOfNameOrThrow (@Nullable final String sName) throws SchematronBindException
  {
    final IPSQueryBinding aQB = getQueryBindingOfName (sName);
    if (aQB == null)
      throw new SchematronBindException ("No query binding implementation present for query binding '" + sName + "'");
    return aQB;
  }

  /**
   * @return A non-null map with all contained query bindings from
   *         name to object.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsMap <String, IPSQueryBinding> getAllRegisteredQueryBindings ()
  {
    return s_aRWLock.readLocked ( () -> s_aMap.getClone ());
  }
}


/**
 * 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.xpath;

import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunctionResolver;
import javax.xml.xpath.XPathVariableResolver;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.helger.commons.ValueEnforcer;
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.string.ToStringGenerator;
import com.helger.schematron.pure.binding.IPSQueryBinding;
import com.helger.schematron.pure.binding.SchematronBindException;
import com.helger.schematron.pure.binding.xpath.IPSXPathVariables;
import com.helger.schematron.pure.binding.xpath.PSXPathVariables;
import com.helger.schematron.pure.bound.AbstractPSBoundSchema;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.model.IPSElement;
import com.helger.schematron.pure.model.IPSHasMixedContent;
import com.helger.schematron.pure.model.PSAssertReport;
import com.helger.schematron.pure.model.PSDiagnostic;
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.PSValueOf;
import com.helger.schematron.pure.validation.IPSValidationHandler;
import com.helger.schematron.pure.validation.SchematronValidationException;
import com.helger.schematron.saxon.SaxonNamespaceContext;
import com.helger.schematron.xpath.XPathEvaluationHelper;
import com.helger.schematron.xslt.util.PSErrorListener;
import com.helger.xml.namespace.MapBasedNamespaceContext;
import com.helger.xml.xpath.XPathHelper;

import net.sf.saxon.xpath.XPathEvaluator;

/**
 * The default XPath binding for the pure Schematron implementation.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PSXPathBoundSchema extends AbstractPSBoundSchema
{
  private static final Logger LOGGER = LoggerFactory.getLogger (PSXPathBoundSchema.class);

  private final XPathVariableResolver m_aXPathVariableResolver;
  private final XPathFunctionResolver m_aXPathFunctionResolver;
  private final XPathFactory m_aXPathFactory;
  private ICommonsList <PSXPathBoundPattern> m_aBoundPatterns;

  /**
   * Compile an XPath expression string to an {@link XPathExpressionException}
   * object. If expression contains any variables, the
   * {@link XPathVariableResolver} will be used to resolve them within this
   * method!
   *
   * @param aXPathContext
   *        Context to use. May not be null.
   * @param sXPathExpression
   *        The expression to be compiled. May not be null.
   * @return The precompiled {@link XPathExpression}
   * @throws XPathExpressionException
   *         If expression cannot be compiled.
   */
  @Nullable
  private static XPathExpression _compileXPath (@Nonnull final XPath aXPathContext,
                                                @Nonnull final String sXPathExpression) throws XPathExpressionException
  {
    XPathExpression ret = null;
    try
    {
      ret = aXPathContext.compile (sXPathExpression);
    }
    catch (final XPathExpressionException ex)
    {
      // Do something with it
      throw ex;
    }
    return ret;
  }

  @Nullable
  private ICommonsList <PSXPathBoundElement> _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
                                                                   @Nonnull final XPath aXPathContext,
                                                                   @Nonnull final IPSXPathVariables aVariables)
  {
    final ICommonsList <PSXPathBoundElement> ret = new CommonsArrayList <> ();
    boolean bHasAnyError = false;

    for (final Object aContentElement : aMixedContent.getAllContentElements ())
    {
      if (aContentElement instanceof PSName)
      {
        final PSName aName = (PSName) aContentElement;
        if (aName.hasPath ())
        {
          // Replace all variables
          final String sPath = aVariables.getAppliedReplacement (aName.getPath ());
          try
          {
            final XPathExpression aXpathExpression = _compileXPath (aXPathContext, sPath);
            ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
          }
          catch (final XPathExpressionException ex)
          {
            error (aName,
                   "Failed to compile XPath expression in <name>: '" + sPath + "'",
                   ex.getCause () != null ? ex.getCause () : ex);
            bHasAnyError = true;
          }
        }
        else
        {
          // No XPath required
          ret.add (new PSXPathBoundElement (aName));
        }
      }
      else
        if (aContentElement instanceof PSValueOf)
        {
          final PSValueOf aValueOf = (PSValueOf) aContentElement;

          // Replace variables
          final String sSelect = aVariables.getAppliedReplacement (aValueOf.getSelect ());
          try
          {
            final XPathExpression aXPathExpression = _compileXPath (aXPathContext, sSelect);
            ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
          }
          catch (final XPathExpressionException ex)
          {
            error (aValueOf, "Failed to compile XPath expression in <value-of>: '" + sSelect + "'", ex);
            bHasAnyError = true;
          }
        }
        else
        {
          // No XPath compilation necessary
          if (aContentElement instanceof String)
            ret.add (new PSXPathBoundElement ((String) aContentElement));
          else
            ret.add (new PSXPathBoundElement ((IPSElement) aContentElement));
        }
    }

    if (bHasAnyError)
      return null;

    return ret;
  }

  @Nullable
  private ICommonsMap <String, PSXPathBoundDiagnostic> _createBoundDiagnostics (@Nonnull final XPath aXPathContext,
                                                                                @Nonnull final IPSXPathVariables aGlobalVariables)
  {
    final ICommonsMap <String, PSXPathBoundDiagnostic> ret = new CommonsHashMap <> ();
    boolean bHasAnyError = false;

    final PSSchema aSchema = getOriginalSchema ();
    if (aSchema.hasDiagnostics ())
    {
      // For all contained diagnostic elements
      for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
      {
        final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aDiagnostic,
                                                                                aXPathContext,
                                                                                aGlobalVariables);
        if (aBoundElements == null)
        {
          // error already emitted
          bHasAnyError = true;
        }
        else
        {
          final PSXPathBoundDiagnostic aBoundDiagnostic = new PSXPathBoundDiagnostic (aDiagnostic, aBoundElements);
          if (ret.put (aDiagnostic.getID (), aBoundDiagnostic) != null)
          {
            error (aDiagnostic, "A diagnostic element with ID '" + aDiagnostic.getID () + "' was overwritten!");
            bHasAnyError = true;
          }
        }
      }
    }

    if (bHasAnyError)
      return null;

    return ret;
  }

  /**
   * Pre-compile all patterns incl. their content
   *
   * @param aXPathContext
   * @param aXPathContext
   *        Global XPath object to use. May not be null.
   * @param aBoundDiagnostics
   *        A map from DiagnosticID to its mapped counterpart. May not be
   *        null.
   * @param aGlobalVariables
   *        The global Schematron-let variables. May not be null.
   * @return null if an XPath error is contained
   */
  @Nullable
  private ICommonsList <PSXPathBoundPattern> _createBoundPatterns (@Nonnull final XPath aXPathContext,
                                                                   @Nonnull final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics,
                                                                   @Nonnull final IPSXPathVariables aGlobalVariables)
  {
    final ICommonsList <PSXPathBoundPattern> ret = new CommonsArrayList <> ();
    boolean bHasAnyError = false;

    // For all relevant patterns
    for (final PSPattern aPattern : getAllRelevantPatterns ())
    {
      // Handle pattern specific variables
      final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();

      if (aPattern.hasAnyLet ())
      {
        // The pattern has special variables, so we need to extend the variable
        // map
        for (final Map.Entry <String, String> aEntry : aPattern.getAllLetsAsMap ().entrySet ())
          if (aPatternVariables.add (aEntry).isUnchanged ())
            error (aPattern, "Duplicate <let> with name '" + aEntry.getKey () + "' in <pattern>");
      }

      // For all rules of the current pattern
      final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
      for (final PSRule aRule : aPattern.getAllRules ())
      {
        // Handle rule specific variables
        final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
        if (aRule.hasAnyLet ())
        {
          // The rule has special variables, so we need to extend the
          // variable map
          for (final Map.Entry <String, String> aEntry : aRule.getAllLetsAsMap ().entrySet ())
            if (aRuleVariables.add (aEntry).isUnchanged ())
              error (aRule, "Duplicate <let> with name '" + aEntry.getKey () + "' in <rule>");
        }

        // For all contained assert and reports within the current rule
        final ICommonsList <PSXPathBoundAssertReport> aBoundAssertReports = new CommonsArrayList <> ();
        for (final PSAssertReport aAssertReport : aRule.getAllAssertReports ())
        {
          final String sTest = aRuleVariables.getAppliedReplacement (aAssertReport.getTest ());
          try
          {
            final XPathExpression aTestExpr = _compileXPath (aXPathContext, sTest);
            final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aAssertReport,
                                                                                aXPathContext,
                                                                                aRuleVariables);
            if (aBoundElements == null)
            {
              // Error already emitted
              bHasAnyError = true;
            }
            else
            {
              final PSXPathBoundAssertReport aBoundAssertReport = new PSXPathBoundAssertReport (aAssertReport,
                                                                                sTest,
                                                                                aTestExpr,
                                                                                aBoundElements,
                                                                                aBoundDiagnostics);
              aBoundAssertReports.add (aBoundAssertReport);
            }
          }
          catch (final Throwable t)
          {
            error (aAssertReport,
                   "Failed to compile XPath expression in <" +
                                  (aAssertReport.isAssert () ? "assert" : "report") +
                                  ">: '" +
                                  sTest +
                                  "' with the following variables: " +
                                  aRuleVariables.getAll (),
                   t);
            bHasAnyError = true;
          }
        }

        // Evaluate base node set for this rule
        final String sRuleContext = aGlobalVariables.getAppliedReplacement (getValidationContext (aRule.getContext ()));
        PSXPathBoundRule aBoundRule = null;
        try
        {
          final XPathExpression aRuleContext = _compileXPath (aXPathContext, sRuleContext);
          aBoundRule = new PSXPathBoundRule (aRule, sRuleContext, aRuleContext, aBoundAssertReports);
          aBoundRules.add (aBoundRule);
        }
        catch (final XPathExpressionException ex)
        {
          error (aRule,
                 "Failed to compile XPath expression in <rule>: '" + sRuleContext + "'",
                 ex.getCause () != null ? ex.getCause () : ex);
          bHasAnyError = true;
        }
      }

      // Create the bound pattern
      final PSXPathBoundPattern aBoundPattern = new PSXPathBoundPattern (aPattern, aBoundRules);
      ret.add (aBoundPattern);
    }

    if (bHasAnyError)
      return null;

    return ret;
  }

  @Nonnull
  public static XPathFactory createXPathFactorySaxonFirst () throws SchematronBindException
  {
    try
    {
      return XPathHelper.createXPathFactorySaxonFirst ();
    }
    catch (final IllegalStateException ex)
    {
      throw new SchematronBindException (ex.getMessage (), ex.getCause ());
    }
  }

  /**
   * Create a new bound schema. All the XPath pre-compilation happens inside
   * this constructor, so that the
   * {@link #validate(Node, String, IPSValidationHandler)} method can be called
   * many times without compiling the XPath statements again and again.
   *
   * @param aQueryBinding
   *        The query binding to be used. May not be null.
   * @param aOrigSchema
   *        The original schema that should be bound. May not be
   *        null.
   * @param sPhase
   *        The selected phase. May be null indicating that the
   *        default phase of the schema should be used (if present) or all
   *        patterns should be evaluated if no default phase is present.
   * @param aCustomErrorListener
   *        A custom error listener to be used. May be null in
   *        which case a
   *        {@link com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler}
   *        is used internally.
   * @param aXPathVariableResolver
   *        Custom XPath variable resolver. May be null.
   * @param aXPathFunctionResolver
   *        Custom XPath function resolver. May be null.
   * @throws SchematronBindException
   *         In case XPath expressions are incorrect and pre-compilation fails
   */
  public PSXPathBoundSchema (@Nonnull final IPSQueryBinding aQueryBinding,
                             @Nonnull final PSSchema aOrigSchema,
                             @Nullable final String sPhase,
                             @Nullable final IPSErrorHandler aCustomErrorListener,
                             @Nullable final XPathVariableResolver aXPathVariableResolver,
                             @Nullable final XPathFunctionResolver aXPathFunctionResolver) throws SchematronBindException
  {
    super (aQueryBinding, aOrigSchema, sPhase, aCustomErrorListener);
    m_aXPathVariableResolver = aXPathVariableResolver;
    m_aXPathFunctionResolver = aXPathFunctionResolver;
    m_aXPathFactory = createXPathFactorySaxonFirst ();
  }

  @Nonnull
  private XPath _createXPathContext ()
  {
    final MapBasedNamespaceContext aNamespaceContext = getNamespaceContext ();
    final XPath aXPathContext = XPathHelper.createNewXPath (m_aXPathFactory,
                                                            m_aXPathVariableResolver,
                                                            m_aXPathFunctionResolver,
                                                            aNamespaceContext);

    if ("net.sf.saxon.xpath.XPathEvaluator".equals (aXPathContext.getClass ().getName ()))
    {
      // Saxon implementation special handling
      final XPathEvaluator aSaxonXPath = (XPathEvaluator) aXPathContext;

      // Since 9.7.0-4 it must implement NamespaceResolver
      aSaxonXPath.setNamespaceContext (new SaxonNamespaceContext (aNamespaceContext));

      // Wrap the PSErrorHandler to a ErrorListener
      aSaxonXPath.getConfiguration ().setErrorListener (new PSErrorListener (getErrorHandler ()));
    }
    return aXPathContext;
  }

  @Nonnull
  public PSXPathBoundSchema bind () throws SchematronBindException
  {
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Binding pure Schematron");

    if (m_aBoundPatterns != null)
      throw new IllegalStateException ("bind must only be called once!");

    final PSSchema aSchema = getOriginalSchema ();
    final PSPhase aPhase = getPhase ();

    // Get all "global" variables that are defined in the schema
    final PSXPathVariables aGlobalVariables = new PSXPathVariables ();
    if (aSchema.hasAnyLet ())
      for (final Map.Entry <String, String> aEntry : aSchema.getAllLetsAsMap ().entrySet ())
        if (aGlobalVariables.add (aEntry).isUnchanged ())
          error (aSchema, "Duplicate <let> with name '" + aEntry.getKey () + "' in global <schema>");

    if (aPhase != null)
    {
      // Get all variables that are defined in the specified phase
      for (final Map.Entry <String, String> aEntry : aPhase.getAllLetsAsMap ().entrySet ())
        if (aGlobalVariables.add (aEntry).isUnchanged ())
          error (aSchema,
                 "Duplicate <let> with name '" + aEntry.getKey () + "' in <phase> with name '" + getPhaseID () + "'");
    }

    final XPath aXPathContext = _createXPathContext ();

    // Pre-compile all diagnostics first
    final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics = _createBoundDiagnostics (aXPathContext,
                                                                                aGlobalVariables);
    if (aBoundDiagnostics == null)
      throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the " +
                                         (isDefaultErrorHandler () ? "log output" : "error listener") +
                                         " for XPath errors!");

    // Perform the pre-compilation of all XPath expressions in the patterns,
    // rules, asserts/reports and the content elements
    m_aBoundPatterns = _createBoundPatterns (aXPathContext, aBoundDiagnostics, aGlobalVariables);
    if (m_aBoundPatterns == null)
      throw new SchematronBindException ("Failed to precompile the supplied schema.");
    return this;
  }

  @Nullable
  public XPathVariableResolver getXPathVariableResolver ()
  {
    return m_aXPathVariableResolver;
  }

  @Nullable
  public XPathFunctionResolver getXPathFunctionResolver ()
  {
    return m_aXPathFunctionResolver;
  }

  @Nonnull
  public String getValidationContext (@Nonnull final String sRuleContext)
  {
    // Do we already have an absolute XPath?
    if (sRuleContext.startsWith ("/"))
      return sRuleContext;

    // Create an absolute XPath expression!
    return "//" + sRuleContext;
  }

  public void validate (@Nonnull final Node aNode,
                        @Nullable final String sBaseURI,
                        @Nonnull final IPSValidationHandler aValidationHandler) throws SchematronValidationException
  {
    ValueEnforcer.notNull (aNode, "Node");
    ValueEnforcer.notNull (aValidationHandler, "ValidationHandler");

    if (m_aBoundPatterns == null)
      throw new IllegalStateException ("bind was never called!");

    final PSSchema aSchema = getOriginalSchema ();
    final PSPhase aPhase = getPhase ();

    // Call the "start" callback method
    aValidationHandler.onStart (aSchema, aPhase, sBaseURI);

    // For all bound patterns
    for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
    {
      final PSPattern aPattern = aBoundPattern.getPattern ();
      aValidationHandler.onPattern (aPattern);

      // For all bound rules
      rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
      {
        final PSRule aRule = aBoundRule.getRule ();

        // Find all nodes matching the rules
        NodeList aRuleMatchingNodes = null;
        try
        {
          aRuleMatchingNodes = XPathEvaluationHelper.evaluate (aBoundRule.getBoundRuleExpression (),
                                                               aNode,
                                                               XPathConstants.NODESET,
                                                               sBaseURI);
        }
        catch (final XPathExpressionException ex)
        {
          // Handle the cause, because it is usually a wrapper only
          error (aRule,
                 "Failed to evaluate XPath expression to a nodeset: '" + aBoundRule.getRuleExpression () + "'",
                 ex.getCause () != null ? ex.getCause () : ex);
          continue rules;
        }

        final int nRuleMatchingNodes = aRuleMatchingNodes.getLength ();
        if (nRuleMatchingNodes > 0)
        {
          // For all contained assert and report elements
          for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
          {
            // XSLT does "fired-rule" for each node
            aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());

            final PSAssertReport aAssertReport = aBoundAssertReport.getAssertReport ();
            final boolean bIsAssert = aAssertReport.isAssert ();
            final XPathExpression aTestExpression = aBoundAssertReport.getBoundTestExpression ();

            // Check each node, if it matches the assert/report
            for (int i = 0; i < nRuleMatchingNodes; ++i)
            {
              final Node aRuleMatchingNode = aRuleMatchingNodes.item (i);
              try
              {
                final boolean bTestResult = ((Boolean) XPathEvaluationHelper.evaluate (aTestExpression,
                                                                                aRuleMatchingNode,
                                                                                XPathConstants.BOOLEAN,
                                                                                sBaseURI)).booleanValue ();
                if (bIsAssert)
                {
                  // It's an assert
                  if (!bTestResult)
                  {
                    // Assert failed
                    if (aValidationHandler.onFailedAssert (aAssertReport,
                                                           aBoundAssertReport.getTestExpression (),
                                                           aRuleMatchingNode,
                                                           i,
                                                           aBoundAssertReport)
                                          .isBreak ())
                    {
                      return;
                    }
                  }
                }
                else
                {
                  // It's a report
                  if (bTestResult)
                  {
                    // Successful report
                    if (aValidationHandler.onSuccessfulReport (aAssertReport,
                                                               aBoundAssertReport.getTestExpression (),
                                                               aRuleMatchingNode,
                                                               i,
                                                               aBoundAssertReport)
                                          .isBreak ())
                    {
                      return;
                    }
                  }
                }
              }
              catch (final XPathExpressionException ex)
              {
                error (aRule,
                       "Failed to evaluate XPath expression to a boolean: '" +
                              aBoundAssertReport.getTestExpression () +
                              "'",
                       ex.getCause () != null ? ex.getCause () : ex);
              }
            }
          }

          if (false)
          {
            // The rule matched at least one node. In this case continue with
            // the next pattern
            break rules;
          }
        }
      }
    }

    // Call the "end" callback method
    aValidationHandler.onEnd (aSchema, aPhase);
  }

  @Override
  public String toString ()
  {
    return ToStringGenerator.getDerived (super.toString ()).append ("boundPatterns", m_aBoundPatterns).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.xpath;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;

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

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.collection.iterate.IterableIterator;
import com.helger.commons.io.file.FileHelper;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.xml.xpath.MapBasedXPathFunctionResolver;

import net.sf.saxon.Configuration;
import net.sf.saxon.Controller;
import net.sf.saxon.expr.instruct.UserFunction;
import net.sf.saxon.functions.ExecutableFunctionLibrary;
import net.sf.saxon.functions.FunctionLibrary;
import net.sf.saxon.functions.FunctionLibraryList;
import net.sf.saxon.query.DynamicQueryContext;
import net.sf.saxon.query.StaticQueryContext;
import net.sf.saxon.query.XQueryExpression;
import net.sf.saxon.query.XQueryFunction;
import net.sf.saxon.query.XQueryFunctionLibrary;
import net.sf.saxon.trans.XPathException;

/**
 * This class loads XQuery modules and provides a list of XPath functions. This
 * class can only be used, if Saxon is on the classpath!
 *
 * @author Philip Helger
 */
public class XQueryAsXPathFunctionConverter
{
  private final String m_sBaseURL;

  /**
   * Default ctor using the current working directory as the base URL for the
   * XQuery resource resolver.
   *
   * @throws MalformedURLException
   *         In case the conversion to URL failed
   */
  public XQueryAsXPathFunctionConverter () throws MalformedURLException
  {
    this (new File (""));
  }

  /**
   * Constructor using the passed file as a working directory as the base URL
   * for the XQuery resource resolver.
   *
   * @param aBasePath
   *        Base path for XQuery resource resolving. May not be
   *        null.
   * @throws MalformedURLException
   *         In case the conversion to URL failed
   */
  public XQueryAsXPathFunctionConverter (@Nonnull final File aBasePath) throws MalformedURLException
  {
    this (FileHelper.getAsURLString (aBasePath));
  }

  /**
   * Constructor using the passed URL as a working directory as the base URL for
   * the XQuery resource resolver.
   *
   * @param sBaseURL
   *        Base URL for XQuery resource resolving. May neither be
   *        null nor empty.
   */
  public XQueryAsXPathFunctionConverter (@Nonnull @Nonempty final String sBaseURL)
  {
    m_sBaseURL = ValueEnforcer.notEmpty (sBaseURL, "BaseURL");
  }

  /**
   * @return The base URL provided in the constructor. Neither null
   *         nor empty.
   */
  @Nonnull
  @Nonempty
  public String getBaseURL ()
  {
    return m_sBaseURL;
  }

  /**
   * Load XQuery functions from an input stream. As this function is supposed to
   * work with Saxon HE, this method allows only for loading full XQuery modules
   * and not for XQuery libraries.
   *
   * @param aXQueryIS
   *        The Input Stream to read from. May not be null. Will be
   *        closed automatically in this method.
   * @return A non-null {@link MapBasedXPathFunctionResolver}
   *         containing all loaded functions.
   * @throws XPathException
   *         if the syntax of the expression is wrong, or if it references
   *         namespaces, variables, or functions that have not been declared, or
   *         any other static error is reported.
   * @throws IOException
   *         if a failure occurs reading the supplied input.
   */
  @Nonnull
  public MapBasedXPathFunctionResolver loadXQuery (@Nonnull @WillClose final InputStream aXQueryIS) throws XPathException,
                                                                                IOException
  {
    ValueEnforcer.notNull (aXQueryIS, "XQueryIS");

    try
    {
      final MapBasedXPathFunctionResolver aFunctionResolver = new MapBasedXPathFunctionResolver ();

      // create a Configuration object
      final Configuration aConfiguration = new Configuration ();
      final DynamicQueryContext aDynamicQueryContext = new DynamicQueryContext (aConfiguration);
      final StaticQueryContext aStaticQueryCtx = aConfiguration.newStaticQueryContext ();
      // The base URI required for resolving within the XQuery
      aStaticQueryCtx.setBaseURI (m_sBaseURL);
      // null == auto detect
      final String sEncoding = null;
      final XQueryExpression exp = aStaticQueryCtx.compileQuery (aXQueryIS, sEncoding);
      final Controller aXQController = exp.newController (aDynamicQueryContext);

      // find all loaded methods and convert them to XPath functions
      final FunctionLibraryList aFuncLibList = exp.getExecutable ().getFunctionLibrary ();
      for (final FunctionLibrary aFuncLib : aFuncLibList.getLibraryList ())
      {
        // Ignore all Vendor, System etc. internal libraries
        if (aFuncLib instanceof FunctionLibraryList)
        {
          // This block works with Saxon HE 9.5.1-x :)
          // This is the custom function library list
          final FunctionLibraryList aRealFuncLib = (FunctionLibraryList) aFuncLib;
          for (final FunctionLibrary aNestedFuncLib : aRealFuncLib.getLibraryList ())
          {
            // Currently the user functions are in ExecutableFunctionLibrary
            if (aNestedFuncLib instanceof ExecutableFunctionLibrary)
              for (final UserFunction aUserFunc : new IterableIterator <> (((ExecutableFunctionLibrary) aNestedFuncLib).iterateFunctions ()))
              {
                // Saxon 9.7 changes "getNumberOfArguments" to "getArity"
                aFunctionResolver.addUniqueFunction (aUserFunc.getFunctionName ().getNamespaceBinding ().getURI (),
                                                     aUserFunc.getFunctionName ().getLocalPart (),
                                                     aUserFunc.getArity (),
                                                     new XPathFunctionFromUserFunction (aConfiguration,
                                                                                aXQController,
                                                                                aUserFunc));
              }
          }
        }
        else
          if (aFuncLib instanceof XQueryFunctionLibrary)
          {
            // This block works with Saxon HE 9.6.0-x :)
            final XQueryFunctionLibrary aRealFuncLib = (XQueryFunctionLibrary) aFuncLib;
            for (final XQueryFunction aXQueryFunction : new IterableIterator <> (aRealFuncLib.getFunctionDefinitions ()))
            {
              // Ensure the function is compiled
              aXQueryFunction.compile ();
              aFunctionResolver.addUniqueFunction (aXQueryFunction.getFunctionName ().getNamespaceBinding ().getURI (),
                                                   aXQueryFunction.getFunctionName ().getLocalPart (),
                                                   aXQueryFunction.getNumberOfArguments (),
                                                   new XPathFunctionFromUserFunction (aConfiguration,
                                                                                aXQController,
                                                                                aXQueryFunction.getUserFunction ()));
            }
          }
      }

      return aFunctionResolver;
    }
    finally
    {
      StreamHelper.close (aXQueryIS);
    }
  }
}


/**
 * 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 org.slf4j.impl;

import org.apache.maven.plugin.logging.Log;
import org.slf4j.helpers.FormattingTuple;
import org.slf4j.helpers.MarkerIgnoringBase;
import org.slf4j.helpers.MessageFormatter;

/**
 * Implementation of {@link org.slf4j.Logger} transforming SLF4J messages to
 * Maven log messages.
 *

 * The class has too many methods, but we can't do anything with this since the
 * parent class requires us to implement them all.
 *

 * The class is thread-safe.
 *
 * @author Yegor Bugayenko (yegor@jcabi.com)
 * @version $Id$
 * @since 0.1.6
 * @see SLF4J FAQ
 */
final class Slf4jAdapter extends MarkerIgnoringBase
{
  /**
   * Serialization ID.
   */
  public static final long serialVersionUID = 0x12C0976798AB5439L;

  /**
   * The log to use.
   */
  private transient Log m_aMavenLog;

  /**
   * Set Maven log.
   *

   * Class variable is used as a synchronization lock, mostly because only one
   * instance of this class may exist.
   *
   * @param log
   *        The log to set
   */
  public void setMavenLog (final Log log)
  {
    synchronized (Slf4jAdapter.class)
    {
      m_aMavenLog = log;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getName ()
  {
    return getClass ().getName ();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isTraceEnabled ()
  {
    return false;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void trace (final String msg)
  {
    _log ().debug (msg);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void trace (final String format, final Object arg)
  {
    _log ().debug (_format (format, arg));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void trace (final String format, final Object first, final Object second)
  {
    _log ().debug (_format (format, first, second));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void trace (final String format, final Object... array)
  {
    _log ().debug (_format (format, array));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void trace (final String msg, final Throwable thr)
  {
    _log ().debug (msg, thr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDebugEnabled ()
  {
    return _log ().isDebugEnabled ();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void debug (final String msg)
  {
    _log ().debug (msg);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void debug (final String format, final Object arg)
  {
    _log ().debug (_format (format, arg));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void debug (final String format, final Object first, final Object second)
  {
    _log ().debug (_format (format, first, second));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void debug (final String format, final Object... array)
  {
    _log ().debug (_format (format, array));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void debug (final String msg, final Throwable thr)
  {
    _log ().debug (msg, thr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isInfoEnabled ()
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void info (final String msg)
  {
    _log ().info (msg);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void info (final String format, final Object arg)
  {
    _log ().info (_format (format, arg));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void info (final String format, final Object first, final Object second)
  {
    _log ().info (_format (format, first, second));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void info (final String format, final Object... array)
  {
    _log ().info (_format (format, array));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void info (final String msg, final Throwable thr)
  {
    _log ().info (msg, thr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isWarnEnabled ()
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void warn (final String msg)
  {
    _log ().warn (msg);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void warn (final String format, final Object arg)
  {
    _log ().warn (_format (format, arg));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void warn (final String format, final Object... array)
  {
    _log ().warn (_format (format, array));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void warn (final String format, final Object first, final Object second)
  {
    _log ().warn (_format (format, first, second));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void warn (final String msg, final Throwable thr)
  {
    _log ().warn (msg, thr);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isErrorEnabled ()
  {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void error (final String msg)
  {
    _log ().error (msg);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void error (final String format, final Object arg)
  {
    _log ().error (_format (format, arg));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void error (final String format, final Object first, final Object second)
  {
    _log ().error (_format (format, first, second));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void error (final String format, final Object... array)
  {
    _log ().error (_format (format, array));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void error (final String msg, final Throwable thr)
  {
    _log ().error (msg, thr);
  }

  /**
   * Get Maven Log.
   *
   * @return The log from Maven plugin
   */
  private Log _log ()
  {
    if (m_aMavenLog == null)
      throw new IllegalStateException ("initialize StaticLoggerBinder with #setMavenLog() first");
    return m_aMavenLog;
  }

  /**
   * Format with one object.
   *
   * @param format
   *        Format to use
   * @param arg
   *        One argument
   * @return The message
   */
  private String _format (final String format, final Object arg)
  {
    final FormattingTuple tuple = MessageFormatter.format (format, arg);
    return tuple.getMessage ();
  }

  /**
   * Format with two objects.
   *
   * @param format
   *        Format to use
   * @param first
   *        First argument
   * @param second
   *        Second argument
   * @return The message
   */
  private String _format (final String format, final Object first, final Object second)
  {
    final FormattingTuple tuple = MessageFormatter.format (format, first, second);
    return tuple.getMessage ();
  }

  /**
   * Format with array.
   *
   * @param format
   *        Format to use
   * @param array
   *        List of arguments
   * @return The message
   */
  private String _format (final String format, final Object [] array)
  {
    final FormattingTuple tuple = MessageFormatter.format (format, array);
    return tuple.getMessage ();
  }
}


/**
 * 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.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.io.resource.URLResource;
import com.helger.commons.string.StringHelper;
import com.helger.xml.serialize.read.DOMReader;
import com.helger.xml.serialize.read.DOMReaderSettings;

/**
 * This is a common utility class.
 *
 * @author Philip Helger
 */
@Immutable
public final class SchematronResourceHelper
{
  private static final Logger LOGGER = LoggerFactory.getLogger (SchematronResourceHelper.class);

  @PresentForCodeCoverage
  private static final SchematronResourceHelper s_aInstance = new SchematronResourceHelper ();

  private SchematronResourceHelper ()
  {}

  /**
   * Convert the passed transform source into a DOM node. Currently on
   * {@link DOMSource} and {@link StreamSource} can be handled.
   *
   * @param aSource
   *        The transform source to use. May not be null.
   * @param aDRS
   *        DOMReader settings to use. May not be null.
   * @return The DOM node and never null.
   * @throws SAXException
   *         In case XML parsing fails
   * @throws IllegalArgumentException
   *         in case an unsupported {@link Source} implementation is provided.
   */
  @Nullable
  public static Node getNodeOfSource (@Nonnull final Source aSource,
                                      @Nonnull final DOMReaderSettings aDRS) throws SAXException
  {
    ValueEnforcer.notNull (aSource, "Source");
    ValueEnforcer.notNull (aDRS, "DOMReaderSettings");

    if (aSource instanceof DOMSource)
    {
      // Node is already in DOMSource
      return ((DOMSource) aSource).getNode ();
    }

    if (aSource instanceof StreamSource)
    {
      // In StreamSource it can either be a byte stream or a character stream or
      // a system ID
      final StreamSource aStreamSource = (StreamSource) aSource;

      final InputStream aIS = aStreamSource.getInputStream ();
      if (aIS != null)
      {
        // Byte stream
        final Document aDoc = DOMReader.readXMLDOM (aIS, aDRS != null ? aDRS : new DOMReaderSettings ());
        if (aDoc == null)
          throw new IllegalArgumentException ("Failed to read source " + aSource + " as XML from InputStream " + aIS);
        return aDoc;
      }

      final Reader aReader = aStreamSource.getReader ();
      if (aReader != null)
      {
        // CHaracter stream
        final Document aDoc = DOMReader.readXMLDOM (aReader, aDRS);
        if (aDoc == null)
          throw new IllegalArgumentException ("Failed to read source " + aSource + " as XML from Reader " + aReader);
        return aDoc;
      }

      final String sSystemID = aStreamSource.getSystemId ();
      if (StringHelper.hasText (sSystemID))
      {
        // System ID
        try
        {
          final URLResource aURL = new URLResource (sSystemID);
          final Document aDoc = DOMReader.readXMLDOM (aURL, aDRS);
          if (aDoc == null)
            throw new IllegalArgumentException ("Failed to read source " +
                                                aSource +
                                                " as XML from SystemID '" +
                                                sSystemID +
                                                "'");
          return aDoc;
        }
        catch (final MalformedURLException ex)
        {
          throw new IllegalArgumentException ("Failed to read source " +
                                              aSource +
                                              " as XML from SystemID '" +
                                              sSystemID +
                                              "': " +
                                              ex.getMessage ());
        }
      }

      // Neither InputStream nor Reader present
      LOGGER.error ("StreamSource contains neither InputStream nor Reader nor SystemID - cannot handle!");
      return null;
    }

    final String sMsg = "Can only handle DOMSource and StreamSource - having " +
                        aSource +
                        " with system ID '" +
                        aSource.getSystemId () +
                        "'";
    LOGGER.error (sMsg);
    throw new IllegalArgumentException (sMsg);
  }
}


/**
 * 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.validator;

import java.io.IOException;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.transform.Source;
import javax.xml.validation.Validator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;

import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.error.level.EErrorLevel;
import com.helger.commons.io.resource.ClassPathResource;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.schematron.relaxng.RelaxNGCompactSchemaCache;
import com.helger.xml.microdom.IMicroNode;
import com.helger.xml.microdom.serialize.MicroWriter;
import com.helger.xml.sax.CollectingSAXErrorHandler;
import com.helger.xml.transform.TransformSourceFactory;

/**
 * Helper class that validates a Schematron against the RelaxNG Compact scheme.
 * This class is not suitable for validating an XML instance against a
 * Schematron instance.
 *
 * @author Philip Helger
 */
@Immutable
public final class SchematronValidator
{
  /** Path to the Schematron RelaxNG Compact file within the class path */
  public static final String SCHEMATRON_RNC_PATH = "schemas/iso-schematron-2006.rnc";

  /** The readable resource with the RelaxNG compact Schematron rules */
  public static final ClassPathResource SCHEMATRON_RNC = new ClassPathResource (SCHEMATRON_RNC_PATH);

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

  @PresentForCodeCoverage
  private static final SchematronValidator s_aInstance = new SchematronValidator ();

  private SchematronValidator ()
  {}

  /**
   * Check if the passed micro node is a valid schematron instance.
   *
   * @param aNode
   *        The micro node to check. May be null.
   * @return true if the schematron is valid, false
   *         otherwise.
   */
  public static boolean isValidSchematron (@Nullable final IMicroNode aNode)
  {
    if (aNode == null)
      return false;

    return isValidSchematron (TransformSourceFactory.create (MicroWriter.getNodeAsString (aNode)));
  }

  /**
   * Check if the passed DOM node is a valid schematron instance.
   *
   * @param aNode
   *        The DOM node to check. May be null.
   * @return true if the schematron is valid, false
   *         otherwise.
   */
  public static boolean isValidSchematron (@Nullable final Node aNode)
  {
    if (aNode == null)
      return false;

    return isValidSchematron (TransformSourceFactory.create (aNode));
  }

  /**
   * Check if the passed resource is a valid schematron instance.
   *
   * @param aRes
   *        The resource to check. May be null.
   * @return true if the schematron is valid, false
   *         otherwise.
   */
  public static boolean isValidSchematron (@Nullable final IReadableResource aRes)
  {
    if (aRes == null)
      return false;

    return isValidSchematron (TransformSourceFactory.create (aRes));
  }

  /**
   * Check if the passed source is a valid schematron instance.
   *
   * @param aSource
   *        The source to check. May be null.
   * @return true if the schematron is valid, false
   *         otherwise.
   */
  public static boolean isValidSchematron (@Nullable final Source aSource)
  {
    if (aSource == null)
      return false;

    try
    {
      // Get a validator from the RNC schema.
      final Validator aValidator = RelaxNGCompactSchemaCache.getInstance ().getValidator (SCHEMATRON_RNC);

      // Ensure a collecting error handler is set
      final ErrorHandler aOldEH = aValidator.getErrorHandler ();
      CollectingSAXErrorHandler aCEH;
      if (aOldEH instanceof CollectingSAXErrorHandler)
        aCEH = (CollectingSAXErrorHandler) aOldEH;
      else
      {
        aCEH = new CollectingSAXErrorHandler ();
        aValidator.setErrorHandler (aCEH.andThen (aOldEH));
      }

      // Perform validation
      aValidator.validate (aSource, null);

      // Check results
      return aCEH.getErrorList ().getMostSevereErrorLevel ().isLT (EErrorLevel.ERROR);
    }
    catch (final SAXException ex)
    {
      // Document is not valid in respect to XML
    }
    catch (final IOException ex)
    {
      LOGGER.warn ("Failed to read source " + aSource, ex);
    }
    return false;
  }
}


/**
 * 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.xslt;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.string.StringHelper;
import com.helger.xml.serialize.write.XMLWriter;

/**
 * Factory for creating {@link ISchematronXSLTBasedProvider} objects.
 *
 * @author Philip Helger
 */
@ThreadSafe
public final class SchematronResourceSCHCache
{
  private static final Logger LOGGER = LoggerFactory.getLogger (SchematronResourceSCHCache.class);
  private static final Lock s_aLock = new ReentrantLock ();
  private static final ICommonsMap <String, SchematronProviderXSLTFromSCH> s_aCache = new CommonsHashMap <> ();

  private SchematronResourceSCHCache ()
  {}

  /**
   * Create a new Schematron validator for the passed resource.
   *
   * @param aSchematronResource
   *        The resource of the Schematron rules. May not be null.
   * @param aTransformerCustomizer
   *        The XSLT transformer customizer to be used. May not be
   *        null.
   * @return null if the passed Schematron resource does not exist
   *         or is invalid.
   */
  @Nullable
  public static SchematronProviderXSLTFromSCH createSchematronXSLTProvider (@Nonnull final IReadableResource aSchematronResource,
                                                                            @Nonnull final SCHTransformerCustomizer aTransformerCustomizer)
  {
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Compiling Schematron instance " + aSchematronResource.toString ());

    final SchematronProviderXSLTFromSCH aXSLTPreprocessor = new SchematronProviderXSLTFromSCH (aSchematronResource,
                                                                                aTransformerCustomizer);
    if (!aXSLTPreprocessor.isValidSchematron ())
    {
      // Schematron is invalid -> parsing failed
      LOGGER.warn ("The Schematron resource '" + aSchematronResource.getResourceID () + "' is invalid!");
      if (LOGGER.isDebugEnabled () && aXSLTPreprocessor.getXSLTDocument () != null)
      {
        // Log the created XSLT document for better error tracking
        LOGGER.debug ("  Created XSLT document:\n" +
                         XMLWriter.getNodeAsString (aXSLTPreprocessor.getXSLTDocument ()));
      }
      return null;
    }

    // If it is a valid schematron, there must be a result XSLT present!
    if (aXSLTPreprocessor.getXSLTDocument () == null)
      throw new IllegalStateException ("No XSLT document retrieved from Schematron resource '" +
                                       aSchematronResource.getResourceID () +
                                       "'!");

    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Finished compiling Schematron instance " + aSchematronResource.toString ());

    // Create the main validator for the schematron
    return aXSLTPreprocessor;
  }

  /**
   * Get the Schematron validator for the passed resource. If no custom
   * parameter are present, the result is cached. The respective cache key is a
   * combination of the Schematron resource path, the phase and the language
   * code.
   *
   * @param aSchematronResource
   *        The resource of the Schematron rules. May not be null.
   * @param aTransformerCustomizer
   *        The XSLT transformer customizer to be used. May not be
   *        null.
   * @return null if the passed Schematron resource does not exist
   *         or is invalid.
   */
  @Nullable
  public static SchematronProviderXSLTFromSCH getSchematronXSLTProvider (@Nonnull final IReadableResource aSchematronResource,
                                                                         @Nonnull final SCHTransformerCustomizer aTransformerCustomizer)
  {
    ValueEnforcer.notNull (aSchematronResource, "SchematronResource");
    ValueEnforcer.notNull (aTransformerCustomizer, "TransformerCustomizer");

    if (!aSchematronResource.exists ())
    {
      LOGGER.warn ("Schematron resource " + aSchematronResource + " does not exist!");
      return null;
    }

    if (!aTransformerCustomizer.canCacheResult ())
    {
      // Create new object and return without cache handling because the custom
      // parameters may have side effects on the created XSLT!
      return createSchematronXSLTProvider (aSchematronResource, aTransformerCustomizer);
    }

    // Determine the unique resource ID for caching
    final String sCacheKey = StringHelper.<String> getImploded (':',
                                                                aSchematronResource.getResourceID (),
                                                                StringHelper.getNotNull (aTransformerCustomizer.getPhase ()),
                                                                StringHelper.getNotNull (aTransformerCustomizer.getLanguageCode ()));

    s_aLock.lock ();
    try
    {
      // Validator already in the cache?
      SchematronProviderXSLTFromSCH aProvider = s_aCache.get (sCacheKey);
      if (aProvider == null)
      {
        // Create new object and put in cache
        aProvider = createSchematronXSLTProvider (aSchematronResource, aTransformerCustomizer);
        if (aProvider != null)
          s_aCache.put (sCacheKey, aProvider);
      }
      return aProvider;
    }
    finally
    {
      s_aLock.unlock ();
    }
  }
}