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

import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.oclc.purl.dsdl.svrl.ActivePattern;
import org.oclc.purl.dsdl.svrl.DiagnosticReference;
import org.oclc.purl.dsdl.svrl.FailedAssert;
import org.oclc.purl.dsdl.svrl.FiredRule;
import org.oclc.purl.dsdl.svrl.NsPrefixInAttributeValues;
import org.oclc.purl.dsdl.svrl.SchematronOutputType;
import org.oclc.purl.dsdl.svrl.SuccessfulReport;
import org.w3c.dom.Node;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.collection.CollectionHelper;
import com.helger.commons.state.EContinue;
import com.helger.schematron.pure.bound.xpath.PSXPathBoundAssertReport;
import com.helger.schematron.pure.bound.xpath.PSXPathBoundDiagnostic;
import com.helger.schematron.pure.bound.xpath.PSXPathBoundElement;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.model.IPSElement;
import com.helger.schematron.pure.model.PSAssertReport;
import com.helger.schematron.pure.model.PSDiagnostics;
import com.helger.schematron.pure.model.PSDir;
import com.helger.schematron.pure.model.PSEmph;
import com.helger.schematron.pure.model.PSName;
import com.helger.schematron.pure.model.PSPattern;
import com.helger.schematron.pure.model.PSPhase;
import com.helger.schematron.pure.model.PSRule;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.schematron.pure.model.PSSpan;
import com.helger.schematron.pure.model.PSTitle;
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.xpath.XPathEvaluationHelper;
import com.helger.xml.XMLHelper;

/**
 * A special validation handler that creates an SVRL document. This class only
 * works for the XPath binding, as the special {@link PSXPathBoundAssertReport}
 * class is referenced!
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PSXPathValidationHandlerSVRL implements IPSValidationHandler
{
  private final IPSErrorHandler m_aErrorHandler;
  private SchematronOutputType m_aSchematronOutput;
  private PSSchema m_aSchema;
  private String m_sBaseURI;

  /**
   * Constructor
   *
   * @param aErrorHandler
   *        The error handler to be used. May not be null.
   */
  public PSXPathValidationHandlerSVRL (@Nonnull final IPSErrorHandler aErrorHandler)
  {
    ValueEnforcer.notNull (aErrorHandler, "ErrorHandler");
    m_aErrorHandler = aErrorHandler;
  }

  private void _warn (@Nonnull final IPSElement aSourceElement, @Nonnull final String sMsg)
  {
    if (m_aSchema == null)
      throw new IllegalStateException ("No schema is present!");

    m_aErrorHandler.warn (m_aSchema.getResource (), aSourceElement, sMsg);
  }

  private void _error (@Nonnull final IPSElement aSourceElement,
                       @Nonnull final String sMsg,
                       @Nullable final Throwable t)
  {
    if (m_aSchema == null)
      throw new IllegalStateException ("No schema is present!");

    m_aErrorHandler.error (m_aSchema.getResource (), aSourceElement, sMsg, t);
  }

  @Nullable
  private static String _getTitleAsString (@Nullable final PSTitle aTitle) throws SchematronValidationException
  {
    if (aTitle == null)
      return null;

    final StringBuilder aSB = new StringBuilder ();
    for (final Object aContent : aTitle.getAllContentElements ())
    {
      if (aContent instanceof String)
        aSB.append ((String) aContent);
      else
        if (aContent instanceof PSDir)
          aSB.append (((PSDir) aContent).getAsText ());
        else
          throw new SchematronValidationException ("Unsupported title content element: " + aContent);
    }
    return aSB.toString ();
  }

  @Override
  public void onStart (@Nonnull final PSSchema aSchema,
                       @Nullable final PSPhase aActivePhase,
                       @Nullable final String sBaseURI) throws SchematronValidationException
  {
    final SchematronOutputType aSchematronOutput = new SchematronOutputType ();
    if (aActivePhase != null)
      aSchematronOutput.setPhase (aActivePhase.getID ());
    aSchematronOutput.setSchemaVersion (aSchema.getSchemaVersion ());
    aSchematronOutput.setTitle (_getTitleAsString (aSchema.getTitle ()));

    // Add namespace prefixes
    for (final Map.Entry <String, String> aEntry : aSchema.getAsNamespaceContext ()
                                                          .getPrefixToNamespaceURIMap ()
                                                          .entrySet ())
    {
      final NsPrefixInAttributeValues aNsPrefix = new NsPrefixInAttributeValues ();
      aNsPrefix.setPrefix (aEntry.getKey ());
      aNsPrefix.setUri (aEntry.getValue ());
      aSchematronOutput.getNsPrefixInAttributeValues ().add (aNsPrefix);
    }
    m_aSchematronOutput = aSchematronOutput;
    m_aSchema = aSchema;
    m_sBaseURI = sBaseURI;
  }

  @Override
  public void onPattern (@Nonnull final PSPattern aPattern)
  {
    final ActivePattern aRetPattern = new ActivePattern ();
    // TODO document
    aRetPattern.setId (aPattern.getID ());
    // TODO name
    // TODO role
    m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aRetPattern);
  }

  @Override
  public void onRule (@Nonnull final PSRule aRule, @Nonnull final String sContext)
  {
    final FiredRule aRetRule = new FiredRule ();
    aRetRule.setContext (sContext);
    aRetRule.setFlag (aRule.getFlag ());
    aRetRule.setId (aRule.getID ());
    if (aRule.hasLinkable ())
      aRetRule.setRole (aRule.getLinkable ().getRole ());
    m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aRetRule);
  }

  /**
   * Get the error text from an assert or report element.
   *
   * @param aBoundContentElements
   *        The list of bound elements to be evaluated.
   * @param aSourceNode
   *        The XML node of the document currently validated.
   * @return A non-null String
   * @throws SchematronValidationException
   *         In case evaluating an XPath expression fails.
   */
  @Nonnull
  private String _getErrorText (@Nonnull final List <PSXPathBoundElement> aBoundContentElements,
                                @Nonnull final Node aSourceNode) throws SchematronValidationException
  {
    final StringBuilder aSB = new StringBuilder ();

    for (final PSXPathBoundElement aBoundElement : aBoundContentElements)
    {
      final Object aContent = aBoundElement.getElement ();
      if (aContent instanceof String)
        aSB.append ((String) aContent);
      else
        if (aContent instanceof PSName)
        {
          final PSName aName = (PSName) aContent;
          if (aName.hasPath ())
          {
            // XPath present
            try
            {
              aSB.append ((String) XPathEvaluationHelper.evaluate (aBoundElement.getBoundExpression (),
                                                                   aSourceNode,
                                                                   XPathConstants.STRING,
                                                                   m_sBaseURI));
            }
            catch (final XPathExpressionException ex)
            {
              _error (aName,
                      "Failed to evaluate XPath expression to a string: '" + aBoundElement.getExpression () + "'",
                      ex.getCause () != null ? ex.getCause () : ex);
              // Append the path so that something is present in the output
              aSB.append (aName.getPath ());
            }
          }
          else
          {
            // No XPath present
            aSB.append (aSourceNode.getNodeName ());
          }
        }
        else
          if (aContent instanceof PSValueOf)
          {
            final PSValueOf aValueOf = (PSValueOf) aContent;
            try
            {
              aSB.append ((String) XPathEvaluationHelper.evaluate (aBoundElement.getBoundExpression (),
                                                                   aSourceNode,
                                                                   XPathConstants.STRING,
                                                                   m_sBaseURI));
            }
            catch (final XPathExpressionException ex)
            {
              _error (aValueOf,
                      "Failed to evaluate XPath expression to a string: '" + aBoundElement.getExpression () + "'",
                      ex);
              // Append the path so that something is present in the output
              aSB.append (aValueOf.getSelect ());
            }
          }
          else
            if (aContent instanceof PSEmph)
              aSB.append (((PSEmph) aContent).getAsText ());
            else
              if (aContent instanceof PSDir)
                aSB.append (((PSDir) aContent).getAsText ());
              else
                if (aContent instanceof PSSpan)
                  aSB.append (((PSSpan) aContent).getAsText ());
                else
                  throw new SchematronValidationException ("Unsupported assert/report content element: " + aContent);
    }
    return aSB.toString ();
  }

  /**
   * Handle the diagnostic references of a single assert/report element
   *
   * @param aSrcDiagnostics
   *        The list of diagnostic reference IDs in the source assert/report
   *        element. May be null if no diagnostic references are
   *        present
   * @param aDstList
   *        The diagnostic reference list of the SchematronOutput to be filled.
   *        May not be null.
   * @param aBoundAssertReport
   *        The bound assert report element. Never null.
   * @param aRuleMatchingNode
   *        The XML node of the XML document currently validated. Never
   *        null.
   * @throws SchematronValidationException
   */
  private void _handleDiagnosticReferences (@Nullable final List <String> aSrcDiagnostics,
                                            @Nonnull final List <DiagnosticReference> aDstList,
                                            @Nonnull final PSXPathBoundAssertReport aBoundAssertReport,
                                            @Nonnull final Node aRuleMatchingNode) throws SchematronValidationException
  {
    if (CollectionHelper.isNotEmpty (aSrcDiagnostics))
    {
      if (m_aSchema.hasDiagnostics ())
      {
        final PSDiagnostics aDiagnostics = m_aSchema.getDiagnostics ();
        for (final String sDiagnosticID : aSrcDiagnostics)
        {
          final PSXPathBoundDiagnostic aDiagnostic = aBoundAssertReport.getBoundDiagnosticOfID (sDiagnosticID);
          if (aDiagnostic == null)
            _warn (aDiagnostics, "Failed to resolve diagnostics with ID '" + sDiagnosticID + "'");
          else
          {
            // Create the SVRL diagnostic-reference element
            final DiagnosticReference aDR = new DiagnosticReference ();
            aDR.setDiagnostic (sDiagnosticID);
            aDR.setText (_getErrorText (aDiagnostic.getAllBoundContentElements (), aRuleMatchingNode));
            aDstList.add (aDR);
          }
        }
      }
      else
        _warn (m_aSchema, "Failed to resolve diagnostic because schema has no diagnostics");
    }
  }

  @Nonnull
  private static String _getPathToNode (@Nonnull final Node aNode)
  {
    return XMLHelper.getPathToNode2 (aNode, "/");
  }

  @Override
  @Nonnull
  public EContinue onFailedAssert (@Nonnull final PSAssertReport aAssertReport,
                                   @Nonnull final String sTestExpression,
                                   @Nonnull final Node aRuleMatchingNode,
                                   final int nNodeIndex,
                                   @Nullable final Object aContext) throws SchematronValidationException
  {
    if (!(aContext instanceof PSXPathBoundAssertReport))
      throw new SchematronValidationException ("The passed context must be an XPath object but is a " + aContext);
    final PSXPathBoundAssertReport aBoundAssertReport = (PSXPathBoundAssertReport) aContext;

    final FailedAssert aFailedAssert = new FailedAssert ();
    aFailedAssert.setFlag (aAssertReport.getFlag ());
    aFailedAssert.setId (aAssertReport.getID ());
    aFailedAssert.setLocation (_getPathToNode (aRuleMatchingNode));
    if (aAssertReport.hasLinkable ())
      aFailedAssert.setRole (aAssertReport.getLinkable ().getRole ());
    aFailedAssert.setTest (sTestExpression);
    aFailedAssert.setText (_getErrorText (aBoundAssertReport.getAllBoundContentElements (), aRuleMatchingNode));
    _handleDiagnosticReferences (aAssertReport.getAllDiagnostics (),
                                 aFailedAssert.getDiagnosticReference (),
                                 aBoundAssertReport,
                                 aRuleMatchingNode);
    m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aFailedAssert);
    return EContinue.CONTINUE;
  }

  @Override
  @Nonnull
  public EContinue onSuccessfulReport (@Nonnull final PSAssertReport aAssertReport,
                                       @Nonnull final String sTestExpression,
                                       @Nonnull final Node aRuleMatchingNode,
                                       final int nNodeIndex,
                                       @Nullable final Object aContext) throws SchematronValidationException
  {
    if (!(aContext instanceof PSXPathBoundAssertReport))
      throw new SchematronValidationException ("The passed context must be an XPath object but is a " + aContext);
    final PSXPathBoundAssertReport aBoundAssertReport = (PSXPathBoundAssertReport) aContext;

    final SuccessfulReport aSuccessfulReport = new SuccessfulReport ();
    aSuccessfulReport.setFlag (aAssertReport.getFlag ());
    aSuccessfulReport.setId (aAssertReport.getID ());
    aSuccessfulReport.setLocation (_getPathToNode (aRuleMatchingNode));
    if (aAssertReport.hasLinkable ())
      aSuccessfulReport.setRole (aAssertReport.getLinkable ().getRole ());
    aSuccessfulReport.setTest (sTestExpression);
    aSuccessfulReport.setText (_getErrorText (aBoundAssertReport.getAllBoundContentElements (), aRuleMatchingNode));
    _handleDiagnosticReferences (aAssertReport.getAllDiagnostics (),
                                 aSuccessfulReport.getDiagnosticReference (),
                                 aBoundAssertReport,
                                 aRuleMatchingNode);
    m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aSuccessfulReport);
    return EContinue.CONTINUE;
  }

  @Nullable
  public SchematronOutputType getSVRL ()
  {
    return m_aSchematronOutput;
  }
}


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

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.CollectionHelper;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.regex.RegExHelper;
import com.helger.commons.string.StringHelper;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.CSchematron;
import com.helger.schematron.CSchematronXML;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.MicroElement;

/**
 * A single Schematron assert- or report-element.

 * An assertion made about the context nodes. The data content is a
 * natural-language assertion. The required test attribute is an assertion test
 * evaluated in the current context. If the test evaluates positive, the
 * assertion succeeds. The optional diagnostics attribute is a reference to
 * further diagnostic information.

 * The natural-language assertion shall be a positive statement of a constraint.
 * NOTE: The natural-language assertion may contain information about actual
 * values in addition to expected values and may contain diagnostic information.
 * Users should note, however, that the diagnostic element is provided for such
 * information to encourage clear statement of the natural-language assertion.
 *

 * The icon, see and fpi attributes allow rich interfaces and documentation.

 * The flag attribute allows more detailed outcomes.

 * The role and subject attributes allow explicit identification of some part of
 * a pattern.

 *
 * @author Philip Helger
 */
@NotThreadSafe
public class PSAssertReport implements
                            IPSElement,
                            IPSHasFlag,
                            IPSHasForeignElements,
                            IPSHasMixedContent,
                            IPSHasID,
                            IPSHasRichGroup,
                            IPSHasLinkableGroup
{
  private final boolean m_bIsAssert;
  private String m_sTest;
  private String m_sFlag;
  private String m_sID;
  private ICommonsList <String> m_aDiagnostics;
  private PSRichGroup m_aRich;
  private PSLinkableGroup m_aLinkable;
  private final ICommonsList <Object> m_aContent = new CommonsArrayList <> ();
  private ICommonsOrderedMap <String, String> m_aForeignAttrs;

  public PSAssertReport (final boolean bIsAssert)
  {
    m_bIsAssert = bIsAssert;
  }

  public boolean isValid (@Nonnull final IPSErrorHandler aErrorHandler)
  {
    for (final Object aContent : m_aContent)
      if (aContent instanceof IPSElement)
        if (!((IPSElement) aContent).isValid (aErrorHandler))
          return false;
    if (StringHelper.hasNoText (m_sTest))
    {
      aErrorHandler.error (this, (m_bIsAssert ? "<assert>" : "<report>") + " has no 'test'");
      return false;
    }
    return true;
  }

  public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
  {
    for (final Object aContent : m_aContent)
      if (aContent instanceof IPSElement)
        ((IPSElement) aContent).validateCompletely (aErrorHandler);
    if (StringHelper.hasNoText (m_sTest))
      aErrorHandler.error (this, (m_bIsAssert ? "<assert>" : "<report>") + " has no 'test'");
  }

  public boolean isMinimal ()
  {
    if (!m_bIsAssert)
      return false;
    for (final Object aContent : m_aContent)
      if (aContent instanceof IPSElement)
        if (!((IPSElement) aContent).isMinimal ())
          return false;
    return true;
  }

  public void addForeignElement (@Nonnull final IMicroElement aForeignElement)
  {
    ValueEnforcer.notNull (aForeignElement, "ForeignElement");
    if (aForeignElement.hasParent ())
      throw new IllegalArgumentException ("ForeignElement already has a parent!");
    m_aContent.add (aForeignElement);
  }

  public boolean hasForeignElements ()
  {
    return m_aContent.containsAny (x -> x instanceof IMicroElement);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <IMicroElement> getAllForeignElements ()
  {
    return m_aContent.getAllInstanceOf (IMicroElement.class);
  }

  public void addForeignAttribute (@Nonnull final String sAttrName, @Nonnull final String sAttrValue)
  {
    ValueEnforcer.notNull (sAttrName, "AttrName");
    ValueEnforcer.notNull (sAttrValue, "AttrValue");
    if (m_aForeignAttrs == null)
      m_aForeignAttrs = new CommonsLinkedHashMap <> ();
    m_aForeignAttrs.put (sAttrName, sAttrValue);
  }

  public boolean hasForeignAttributes ()
  {
    return m_aForeignAttrs != null && m_aForeignAttrs.isNotEmpty ();
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsOrderedMap <String, String> getAllForeignAttributes ()
  {
    return new CommonsLinkedHashMap <> (m_aForeignAttrs);
  }

  public boolean isAssert ()
  {
    return m_bIsAssert;
  }

  public boolean isReport ()
  {
    return !m_bIsAssert;
  }

  public void setTest (@Nullable final String sTest)
  {
    m_sTest = sTest;
  }

  @Nullable
  public String getTest ()
  {
    return m_sTest;
  }

  public void setFlag (@Nullable final String sFlag)
  {
    m_sFlag = sFlag;
  }

  @Nullable
  public String getFlag ()
  {
    return m_sFlag;
  }

  public void setID (@Nullable final String sID)
  {
    m_sID = sID;
  }

  @Nullable
  public String getID ()
  {
    return m_sID;
  }

  /**
   * Set the diagnostics, as an IDREFS value (multiple IDREF values separated by
   * spaces)
   *
   * @param sDiagnostics
   *        The value to be set. May be null.
   */
  public void setDiagnostics (@Nullable final String sDiagnostics)
  {
    if (StringHelper.hasText (sDiagnostics))
      setDiagnostics (RegExHelper.getSplitToList (sDiagnostics.trim (), "\\s+"));
    else
      m_aDiagnostics = null;
  }

  /**
   * Set the diagnostics, as an IDREFS value (multiple IDREF values separated by
   * spaces)
   *
   * @param aDiagnostics
   *        The value to be set. May be null.
   */
  public void setDiagnostics (@Nullable final ICommonsList <String> aDiagnostics)
  {
    m_aDiagnostics = aDiagnostics;
  }

  /**
   * @return List of references to {@link PSDiagnostic} elements.
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <String> getAllDiagnostics ()
  {
    // May be null
    return new CommonsArrayList <> (m_aDiagnostics);
  }

  public void setRich (@Nullable final PSRichGroup aRich)
  {
    m_aRich = aRich;
  }

  @Nullable
  public PSRichGroup getRich ()
  {
    return m_aRich;
  }

  public void setLinkable (@Nullable final PSLinkableGroup aLinkable)
  {
    m_aLinkable = aLinkable;
  }

  @Nullable
  public PSLinkableGroup getLinkable ()
  {
    return m_aLinkable;
  }

  public void addText (@Nonnull @Nonempty final String sText)
  {
    ValueEnforcer.notEmpty (sText, "Text");
    m_aContent.add (sText);
  }

  public boolean hasAnyText ()
  {
    return m_aContent.containsAny (e -> e instanceof String);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <String> getAllTexts ()
  {
    return m_aContent.getAllInstanceOf (String.class);
  }

  public void addName (@Nonnull final PSName aName)
  {
    ValueEnforcer.notNull (aName, "Name");
    m_aContent.add (aName);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <PSName> getAllNames ()
  {
    return m_aContent.getAllInstanceOf (PSName.class);
  }

  public void addValueOf (@Nonnull final PSValueOf aValueOf)
  {
    ValueEnforcer.notNull (aValueOf, "ValueOf");
    m_aContent.add (aValueOf);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <PSValueOf> getAllValueOfs ()
  {
    return m_aContent.getAllInstanceOf (PSValueOf.class);
  }

  public void addEmph (@Nonnull final PSEmph aEmph)
  {
    ValueEnforcer.notNull (aEmph, "Emph");
    m_aContent.add (aEmph);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <PSEmph> getAllEmphs ()
  {
    return m_aContent.getAllInstanceOf (PSEmph.class);
  }

  public void addDir (@Nonnull final PSDir aDir)
  {
    ValueEnforcer.notNull (aDir, "Dir");
    m_aContent.add (aDir);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <PSDir> getAllDirs ()
  {
    return m_aContent.getAllInstanceOf (PSDir.class);
  }

  public void addSpan (@Nonnull final PSSpan aSpan)
  {
    ValueEnforcer.notNull (aSpan, "Span");
    m_aContent.add (aSpan);
  }

  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <PSSpan> getAllSpans ()
  {
    return m_aContent.getAllInstanceOf (PSSpan.class);
  }

  /**
   * @return A list of {@link String}, {@link PSName}, {@link PSValueOf},
   *         {@link PSEmph}, {@link PSDir} and {@link PSSpan} elements.
   */
  @Nonnull
  @ReturnsMutableCopy
  public ICommonsList <Object> getAllContentElements ()
  {
    return m_aContent.getClone ();
  }

  @Nonnull
  public IMicroElement getAsMicroElement ()
  {
    final IMicroElement ret = new MicroElement (CSchematron.NAMESPACE_SCHEMATRON,
                                                m_bIsAssert ? CSchematronXML.ELEMENT_ASSERT
                                                            : CSchematronXML.ELEMENT_REPORT);
    ret.setAttribute (CSchematronXML.ATTR_ID, m_sID);
    ret.setAttribute (CSchematronXML.ATTR_FLAG, m_sFlag);
    ret.setAttribute (CSchematronXML.ATTR_TEST, m_sTest);
    if (CollectionHelper.isNotEmpty (m_aDiagnostics))
      ret.setAttribute (CSchematronXML.ATTR_DIAGNOSTICS, StringHelper.getImploded (' ', m_aDiagnostics));
    if (m_aRich != null)
      m_aRich.fillMicroElement (ret);
    if (m_aLinkable != null)
      m_aLinkable.fillMicroElement (ret);
    for (final Object aContent : m_aContent)
      if (aContent instanceof IMicroElement)
        ret.appendChild (((IMicroElement) aContent).getClone ());
      else
        if (aContent instanceof String)
          ret.appendText ((String) aContent);
        else
          ret.appendChild (((IPSElement) aContent).getAsMicroElement ());
    if (m_aForeignAttrs != null)
      for (final Map.Entry <String, String> aEntry : m_aForeignAttrs.entrySet ())
        ret.setAttribute (aEntry.getKey (), aEntry.getValue ());
    return ret;
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("isAssert", m_bIsAssert)
                                       .appendIfNotNull ("test", m_sTest)
                                       .appendIfNotNull ("flag", m_sFlag)
                                       .appendIfNotNull ("id", m_sID)
                                       .appendIfNotNull ("diagnostics", m_aDiagnostics)
                                       .appendIfNotNull ("rich", m_aRich)
                                       .appendIfNotNull ("linkable", m_aLinkable)
                                       .appendIf ("content", m_aContent, CollectionHelper::isNotEmpty)
                                       .appendIf ("foreignAttrs", m_aForeignAttrs, CollectionHelper::isNotEmpty)
                                       .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 javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

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

import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.lang.ServiceLoaderHelper;

/**
 * A central registry for all {@link ISVRLLocationBeautifierSPI} instances.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public final class SVRLLocationBeautifierRegistry
{
  private static final Logger LOGGER = LoggerFactory.getLogger (SVRLLocationBeautifierRegistry.class);
  private static final ICommonsList <ISVRLLocationBeautifierSPI> s_aList = ServiceLoaderHelper.getAllSPIImplementations (ISVRLLocationBeautifierSPI.class);

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

  private SVRLLocationBeautifierRegistry ()
  {}

  /**
   * Get the beautified location for the given namespace and local name.
   *
   * @param sNamespaceURI
   *        The namespace URI
   * @param sLocalName
   *        The element local name
   * @return null if no beautification is available
   */
  @Nullable
  public static String getBeautifiedLocation (@Nonnull final String sNamespaceURI, @Nonnull final String sLocalName)
  {
    for (final ISVRLLocationBeautifierSPI aBeautifier : s_aList)
    {
      final String sBeautified = aBeautifier.getReplacementText (sNamespaceURI, sLocalName);
      if (sBeautified != null)
        return sBeautified;
    }
    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("Unsupported elements for beautification: " + sNamespaceURI + " -- " + sLocalName);
    return null;
  }
}


/**
 * Copyright (C) 2014-2019 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.schematron.pure.exchange;

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

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

import com.helger.commons.ValueEnforcer;
import com.helger.commons.io.resource.IReadableResource;
import com.helger.commons.string.StringParser;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.CSchematron;
import com.helger.schematron.CSchematronXML;
import com.helger.schematron.SchematronDebug;
import com.helger.schematron.SchematronHelper;
import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
import com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler;
import com.helger.schematron.pure.model.*;
import com.helger.schematron.pure.model.PSDir.EDirValue;
import com.helger.schematron.pure.model.PSRichGroup.ESpace;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.IMicroText;
import com.helger.xml.microdom.serialize.MicroWriter;
import com.helger.xml.serialize.read.SAXReaderSettings;

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

/**
 * Utility class for reading all Schematron elements from a resource.
 *
 * @author Philip Helger
 */
@Immutable
@SuppressFBWarnings ("JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS")
public class PSReader
{
  private static final Logger LOGGER = LoggerFactory.getLogger (PSReader.class);

  private final IReadableResource m_aResource;
  private final IPSErrorHandler m_aErrorHandler;
  private final EntityResolver m_aEntityResolver;

  /**
   * Constructor without an error handler
   *
   * @param aResource
   *        The resource to read the Schematron from. May not be
   *        null.
   */
  public PSReader (@Nonnull final IReadableResource aResource)
  {
    this (aResource, null, null);
  }

  /**
   * Constructor with an error handler
   *
   * @param aResource
   *        The resource to read the Schematron from. May not be
   *        null.
   * @param aErrorHandler
   *        The error handler to use. May be null. If the error
   *        handler is null a {@link LoggingPSErrorHandler} is
   *        automatically created and used.
   */
  public PSReader (@Nonnull final IReadableResource aResource, @Nullable final IPSErrorHandler aErrorHandler)
  {
    this (aResource, aErrorHandler, null);
  }

  /**
   * Constructor with an error handler
   *
   * @param aResource
   *        The resource to read the Schematron from. May not be
   *        null.
   * @param aErrorHandler
   *        The error handler to use. May be null. If the error
   *        handler is null a {@link LoggingPSErrorHandler} is
   *        automatically created and used.
   * @param aEntityResolver
   *        The XML entity resolver to be used. May be null.
   * @since 4.1.1
   */
  public PSReader (@Nonnull final IReadableResource aResource,
                   @Nullable final IPSErrorHandler aErrorHandler,
                   @Nullable final EntityResolver aEntityResolver)
  {
    ValueEnforcer.notNull (aResource, "Resource");
    m_aResource = aResource;
    m_aErrorHandler = aErrorHandler != null ? aErrorHandler : new LoggingPSErrorHandler ();
    m_aEntityResolver = aEntityResolver;
  }

  /**
   * @return The resource from which the Schematron schema is read. Never
   *         null.
   */
  @Nonnull
  public IReadableResource getResource ()
  {
    return m_aResource;
  }

  /**
   * @return The error handler used. If no error handler was passed in the
   *         constructor, than a {@link LoggingPSErrorHandler} is automatically
   *         used.
   */
  @Nonnull
  public IPSErrorHandler getErrorHandler ()
  {
    return m_aErrorHandler;
  }

  /**
   * Utility method to get a real attribute value, by trimming spaces, if the
   * value is non-null.
   *
   * @param sAttrValue
   *        The source attribute value. May be null.
   * @return null if the input parameter is null.
   */
  @Nullable
  private static String _getAttributeValue (@Nullable final String sAttrValue)
  {
    return sAttrValue == null ? null : sAttrValue.trim ();
  }

  /**
   * Emit a warning with the registered error handler.
   *
   * @param aSourceElement
   *        The source element where the error occurred.
   * @param sMessage
   *        The main warning message.
   */
  private void _warn (@Nonnull final IPSElement aSourceElement, @Nonnull final String sMessage)
  {
    ValueEnforcer.notNull (aSourceElement, "SourceElement");
    ValueEnforcer.notNull (sMessage, "Message");

    m_aErrorHandler.warn (m_aResource, aSourceElement, sMessage);
  }

  /**
   * Read an <active> element
   *
   * @param eActive
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSActive readActiveFromXML (@Nonnull final IMicroElement eActive)
  {
    final PSActive ret = new PSActive ();
    eActive.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_PATTERN))
        ret.setPattern (sAttrValue);
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eActive.forAllChildren (aActiveChild -> {
      switch (aActiveChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aActiveChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aActiveChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            final String sLocalName = eElement.getLocalName ();
            if (sLocalName.equals (CSchematronXML.ELEMENT_DIR))
              ret.addDir (readDirFromXML (eElement));
            else
              if (sLocalName.equals (CSchematronXML.ELEMENT_EMPH))
                ret.addEmph (readEmphFromXML (eElement));
              else
                if (sLocalName.equals (CSchematronXML.ELEMENT_SPAN))
                  ret.addSpan (readSpanFromXML (eElement));
                else
                  _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aActiveChild);
      }
    });
    return ret;
  }

  private void _handleRichGroup (@Nonnull final String sAttrName,
                                 @Nonnull final String sAttrValue,
                                 @Nonnull final PSRichGroup aRichGroup)
  {
    if (sAttrName.equals (CSchematronXML.ATTR_ICON))
      aRichGroup.setIcon (sAttrValue);
    else
      if (sAttrName.equals (CSchematronXML.ATTR_SEE))
        aRichGroup.setSee (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_FPI))
          aRichGroup.setFPI (sAttrValue);
        else
          if (sAttrName.equals (CSchematronXML.ATTR_XML_LANG))
            aRichGroup.setXmlLang (sAttrValue);
          else
            if (sAttrName.equals (CSchematronXML.ATTR_XML_SPACE))
              aRichGroup.setXmlSpace (ESpace.getFromIDOrNull (sAttrValue));
  }

  private void _handleLinkableGroup (@Nonnull final String sAttrName,
                                     @Nonnull final String sAttrValue,
                                     @Nonnull final PSLinkableGroup aLinkableGroup)
  {
    if (sAttrName.equals (CSchematronXML.ATTR_ROLE))
      aLinkableGroup.setRole (sAttrValue);
    else
      if (sAttrName.equals (CSchematronXML.ATTR_SUBJECT))
        aLinkableGroup.setSubject (sAttrValue);
  }

  /**
   * Read an <assert> or a <report> element
   *
   * @param eAssertReport
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSAssertReport readAssertReportFromXML (@Nonnull final IMicroElement eAssertReport)
  {
    final PSAssertReport ret = new PSAssertReport (eAssertReport.getLocalName ()
                                                                .equals (CSchematronXML.ELEMENT_ASSERT));

    final PSRichGroup aRichGroup = new PSRichGroup ();
    final PSLinkableGroup aLinkableGroup = new PSLinkableGroup ();
    eAssertReport.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_TEST))
        ret.setTest (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_FLAG))
          ret.setFlag (sAttrValue);
        else
          if (sAttrName.equals (CSchematronXML.ATTR_ID))
            ret.setID (sAttrValue);
          else
            if (sAttrName.equals (CSchematronXML.ATTR_DIAGNOSTICS))
              ret.setDiagnostics (sAttrValue);
            else
              if (PSRichGroup.isRichAttribute (sAttrName))
                _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
              else
                if (PSLinkableGroup.isLinkableAttribute (sAttrName))
                  _handleLinkableGroup (sAttrName, sAttrValue, aLinkableGroup);
                else
                  ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);
    ret.setLinkable (aLinkableGroup);

    eAssertReport.forAllChildren (aAssertReportChild -> {
      switch (aAssertReportChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aAssertReportChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aAssertReportChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            final String sLocalName = eElement.getLocalName ();
            if (sLocalName.equals (CSchematronXML.ELEMENT_NAME))
              ret.addName (readNameFromXML (eElement));
            else
              if (sLocalName.equals (CSchematronXML.ELEMENT_VALUE_OF))
                ret.addValueOf (readValueOfFromXML (eElement));
              else
                if (sLocalName.equals (CSchematronXML.ELEMENT_EMPH))
                  ret.addEmph (readEmphFromXML (eElement));
                else
                  if (sLocalName.equals (CSchematronXML.ELEMENT_DIR))
                    ret.addDir (readDirFromXML (eElement));
                  else
                    if (sLocalName.equals (CSchematronXML.ELEMENT_SPAN))
                      ret.addSpan (readSpanFromXML (eElement));
                    else
                      _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aAssertReportChild);
      }
    });
    return ret;
  }

  /**
   * Read a <diagnostic> element
   *
   * @param eDiagnostic
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSDiagnostic readDiagnosticFromXML (@Nonnull final IMicroElement eDiagnostic)
  {
    final PSDiagnostic ret = new PSDiagnostic ();

    final PSRichGroup aRichGroup = new PSRichGroup ();
    eDiagnostic.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_ID))
        ret.setID (sAttrValue);
      else
        if (PSRichGroup.isRichAttribute (sAttrName))
          _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
        else
          ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);

    eDiagnostic.forAllChildren (aDiagnosticChild -> {
      switch (aDiagnosticChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aDiagnosticChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aDiagnosticChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            final String sLocalName = eElement.getLocalName ();
            if (sLocalName.equals (CSchematronXML.ELEMENT_VALUE_OF))
              ret.addValueOf (readValueOfFromXML (eElement));
            else
              if (sLocalName.equals (CSchematronXML.ELEMENT_EMPH))
                ret.addEmph (readEmphFromXML (eElement));
              else
                if (sLocalName.equals (CSchematronXML.ELEMENT_DIR))
                  ret.addDir (readDirFromXML (eElement));
                else
                  if (sLocalName.equals (CSchematronXML.ELEMENT_SPAN))
                    ret.addSpan (readSpanFromXML (eElement));
                  else
                    _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aDiagnosticChild);
      }
    });
    return ret;
  }

  /**
   * Read a <diagnostics> element
   *
   * @param eDiagnostics
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSDiagnostics readDiagnosticsFromXML (@Nonnull final IMicroElement eDiagnostics)
  {
    final PSDiagnostics ret = new PSDiagnostics ();

    eDiagnostics.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eDiagnostics.forAllChildElements (eDiagnosticsChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eDiagnosticsChild.getNamespaceURI ()))
      {
        if (eDiagnosticsChild.getLocalName ().equals (CSchematronXML.ELEMENT_INCLUDE))
          ret.addInclude (readIncludeFromXML (eDiagnosticsChild));
        else
          if (eDiagnosticsChild.getLocalName ().equals (CSchematronXML.ELEMENT_DIAGNOSTIC))
            ret.addDiagnostic (readDiagnosticFromXML (eDiagnosticsChild));
          else
            _warn (ret, "Unsupported Schematron element '" + eDiagnosticsChild.getLocalName () + "'");
      }
      else
        ret.addForeignElement (eDiagnosticsChild.getClone ());
    });
    return ret;
  }

  /**
   * Read a <dir> element
   *
   * @param eDir
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSDir readDirFromXML (@Nonnull final IMicroElement eDir)
  {
    final PSDir ret = new PSDir ();

    eDir.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_VALUE))
        ret.setValue (EDirValue.getFromIDOrNull (sAttrValue));
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eDir.forAllChildren (aDirChild -> {
      switch (aDirChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aDirChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aDirChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            _warn (ret, "Unsupported Schematron element '" + eElement.getLocalName () + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aDirChild);
      }
    });
    return ret;
  }

  /**
   * Read an <emph> element
   *
   * @param eEmph
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSEmph readEmphFromXML (@Nonnull final IMicroElement eEmph)
  {
    final PSEmph ret = new PSEmph ();

    eEmph.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      _warn (ret, "Unsupported attribute '" + sAttrName + "'='" + sAttrValue + "'");
    });

    eEmph.forAllChildren (aEmphChild -> {
      switch (aEmphChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aEmphChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aEmphChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            _warn (ret, "Unsupported Schematron element '" + eElement.getLocalName () + "'");
          }
          else
            _warn (ret, "Unsupported namespace URI '" + eElement.getNamespaceURI () + "'");

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aEmphChild);
      }
    });
    return ret;
  }

  /**
   * Read an <extends> element
   *
   * @param eExtends
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSExtends readExtendsFromXML (@Nonnull final IMicroElement eExtends)
  {
    final PSExtends ret = new PSExtends ();

    eExtends.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_RULE))
        ret.setRule (sAttrValue);
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eExtends.forAllChildElements (eChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read an <include> element
   *
   * @param eInclude
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSInclude readIncludeFromXML (@Nonnull final IMicroElement eInclude)
  {
    final PSInclude ret = new PSInclude ();

    eInclude.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_HREF))
        ret.setHref (sAttrValue);
      else
        _warn (ret, "Unsupported attribute '" + sAttrName + "'='" + sAttrValue + "'");
    });

    eInclude.forAllChildElements (eValueOfChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eValueOfChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eValueOfChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eValueOfChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read a <let> element
   *
   * @param eLet
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSLet readLetFromXML (@Nonnull final IMicroElement eLet)
  {
    final PSLet ret = new PSLet ();

    eLet.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_NAME))
        ret.setName (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_VALUE))
          ret.setValue (sAttrValue);
        else
          _warn (ret, "Unsupported attribute '" + sAttrName + "'='" + sAttrValue + "'");
    });

    eLet.forAllChildElements (eLetChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eLetChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eLetChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eLetChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read a <name> element
   *
   * @param eName
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSName readNameFromXML (@Nonnull final IMicroElement eName)
  {
    final PSName ret = new PSName ();

    eName.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_PATH))
        ret.setPath (sAttrValue);
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eName.forAllChildElements (eNameChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eNameChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eNameChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eNameChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read a <ns> element
   *
   * @param eNS
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSNS readNSFromXML (@Nonnull final IMicroElement eNS)
  {
    final PSNS ret = new PSNS ();

    eNS.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_URI))
        ret.setUri (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_PREFIX))
          ret.setPrefix (sAttrValue);
        else
          ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eNS.forAllChildElements (eLetChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eLetChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eLetChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eLetChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read a <p> element
   *
   * @param eP
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSP readPFromXML (@Nonnull final IMicroElement eP)
  {
    final PSP ret = new PSP ();
    eP.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_ID))
        ret.setID (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_CLASS))
          ret.setClazz (sAttrValue);
        else
          if (sAttrName.equals (CSchematronXML.ATTR_ICON))
            ret.setIcon (sAttrValue);
          else
            ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eP.forAllChildren (aChild -> {
      switch (aChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            final String sLocalName = eElement.getLocalName ();
            if (sLocalName.equals (CSchematronXML.ELEMENT_DIR))
              ret.addDir (readDirFromXML (eElement));
            else
              if (sLocalName.equals (CSchematronXML.ELEMENT_EMPH))
                ret.addEmph (readEmphFromXML (eElement));
              else
                if (sLocalName.equals (CSchematronXML.ELEMENT_SPAN))
                  ret.addSpan (readSpanFromXML (eElement));
                else
                  _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aChild);
      }
    });
    return ret;
  }

  /**
   * Read a <param> element
   *
   * @param eParam
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSParam readParamFromXML (@Nonnull final IMicroElement eParam)
  {
    final PSParam ret = new PSParam ();

    eParam.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_NAME))
        ret.setName (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_VALUE))
          ret.setValue (sAttrValue);
        else
          _warn (ret, "Unsupported attribute '" + sAttrName + "'='" + sAttrValue + "'");
    });

    eParam.forAllChildElements (eParamChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eParamChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eParamChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eParamChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read a <pattern> element
   *
   * @param ePattern
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSPattern readPatternFromXML (@Nonnull final IMicroElement ePattern)
  {
    final PSPattern ret = new PSPattern ();

    final PSRichGroup aRichGroup = new PSRichGroup ();
    ePattern.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_ABSTRACT))
        ret.setAbstract (StringParser.parseBool (sAttrValue));
      else
        if (sAttrName.equals (CSchematronXML.ATTR_ID))
          ret.setID (sAttrValue);
        else
          if (sAttrName.equals (CSchematronXML.ATTR_IS_A))
            ret.setIsA (sAttrValue);
          else
            if (PSRichGroup.isRichAttribute (sAttrName))
              _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
            else
              ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);

    ePattern.forAllChildElements (ePatternChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (ePatternChild.getNamespaceURI ()))
      {
        if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_INCLUDE))
          ret.addInclude (readIncludeFromXML (ePatternChild));
        else
          if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_TITLE))
            ret.setTitle (readTitleFromXML (ePatternChild));
          else
            if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_P))
              ret.addP (readPFromXML (ePatternChild));
            else
              if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_LET))
                ret.addLet (readLetFromXML (ePatternChild));
              else
                if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_RULE))
                  ret.addRule (readRuleFromXML (ePatternChild));
                else
                  if (ePatternChild.getLocalName ().equals (CSchematronXML.ELEMENT_PARAM))
                    ret.addParam (readParamFromXML (ePatternChild));
                  else
                    _warn (ret,
                           "Unsupported Schematron element '" +
                                ePatternChild.getLocalName () +
                                "' in " +
                                ret.toString ());
      }
      else
        ret.addForeignElement (ePatternChild.getClone ());
    });
    return ret;
  }

  /**
   * Read a <phase> element
   *
   * @param ePhase
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSPhase readPhaseFromXML (@Nonnull final IMicroElement ePhase)
  {
    final PSPhase ret = new PSPhase ();

    final PSRichGroup aRichGroup = new PSRichGroup ();
    ePhase.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_ID))
        ret.setID (sAttrValue);
      else
        if (PSRichGroup.isRichAttribute (sAttrName))
          _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
        else
          ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);

    ePhase.forAllChildElements (ePhaseChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (ePhaseChild.getNamespaceURI ()))
      {
        if (ePhaseChild.getLocalName ().equals (CSchematronXML.ELEMENT_INCLUDE))
          ret.addInclude (readIncludeFromXML (ePhaseChild));
        else
          if (ePhaseChild.getLocalName ().equals (CSchematronXML.ELEMENT_P))
            ret.addP (readPFromXML (ePhaseChild));
          else
            if (ePhaseChild.getLocalName ().equals (CSchematronXML.ELEMENT_LET))
              ret.addLet (readLetFromXML (ePhaseChild));
            else
              if (ePhaseChild.getLocalName ().equals (CSchematronXML.ELEMENT_ACTIVE))
                ret.addActive (readActiveFromXML (ePhaseChild));
              else
                _warn (ret, "Unsupported Schematron element '" + ePhaseChild.getLocalName () + "'");
      }
      else
        ret.addForeignElement (ePhaseChild.getClone ());
    });
    return ret;
  }

  /**
   * Read a <rule> element
   *
   * @param eRule
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSRule readRuleFromXML (@Nonnull final IMicroElement eRule)
  {
    final PSRule ret = new PSRule ();

    final PSRichGroup aRichGroup = new PSRichGroup ();
    final PSLinkableGroup aLinkableGroup = new PSLinkableGroup ();
    eRule.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_FLAG))
        ret.setFlag (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_ABSTRACT))
          ret.setAbstract (StringParser.parseBool (sAttrValue));
        else
          if (sAttrName.equals (CSchematronXML.ATTR_CONTEXT))
            ret.setContext (sAttrValue);
          else
            if (sAttrName.equals (CSchematronXML.ATTR_ID))
              ret.setID (sAttrValue);
            else
              if (PSRichGroup.isRichAttribute (sAttrName))
                _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
              else
                if (PSLinkableGroup.isLinkableAttribute (sAttrName))
                  _handleLinkableGroup (sAttrName, sAttrValue, aLinkableGroup);
                else
                  ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);
    ret.setLinkable (aLinkableGroup);

    eRule.forAllChildElements (eRuleChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eRuleChild.getNamespaceURI ()))
      {
        final String sLocalName = eRuleChild.getLocalName ();
        if (sLocalName.equals (CSchematronXML.ELEMENT_INCLUDE))
          ret.addInclude (readIncludeFromXML (eRuleChild));
        else
          if (sLocalName.equals (CSchematronXML.ELEMENT_LET))
            ret.addLet (readLetFromXML (eRuleChild));
          else
            if (sLocalName.equals (CSchematronXML.ELEMENT_ASSERT) || sLocalName.equals (CSchematronXML.ELEMENT_REPORT))
              ret.addAssertReport (readAssertReportFromXML (eRuleChild));
            else
              if (sLocalName.equals (CSchematronXML.ELEMENT_EXTENDS))
                ret.addExtends (readExtendsFromXML (eRuleChild));
              else
                _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
      }
      else
        ret.addForeignElement (eRuleChild.getClone ());
    });
    return ret;
  }

  /**
   * Parse the Schematron into a pure Java object. This method makes no
   * assumptions on the validity of the document!
   *
   * @param eSchema
   *        The XML element to use. May not be null.
   * @return The created {@link PSSchema} object or null in case of
   *         null document or a fatal error.
   * @throws SchematronReadException
   *         If reading fails
   */
  @Nonnull
  public PSSchema readSchemaFromXML (@Nonnull final IMicroElement eSchema) throws SchematronReadException
  {
    ValueEnforcer.notNull (eSchema, "Schema");
    if (!CSchematron.NAMESPACE_SCHEMATRON.equals (eSchema.getNamespaceURI ()))
      throw new SchematronReadException (m_aResource, "The passed element is not an ISO Schematron element!");

    final PSSchema ret = new PSSchema (m_aResource);
    final PSRichGroup aRichGroup = new PSRichGroup ();
    eSchema.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_ID))
        ret.setID (sAttrValue);
      else
        if (sAttrName.equals (CSchematronXML.ATTR_SCHEMA_VERSION))
          ret.setSchemaVersion (sAttrValue);
        else
          if (sAttrName.equals (CSchematronXML.ATTR_DEFAULT_PHASE))
            ret.setDefaultPhase (sAttrValue);
          else
            if (sAttrName.equals (CSchematronXML.ATTR_QUERY_BINDING))
              ret.setQueryBinding (sAttrValue);
            else
              if (PSRichGroup.isRichAttribute (sAttrName))
                _handleRichGroup (sAttrName, sAttrValue, aRichGroup);
              else
                ret.addForeignAttribute (sAttrName, sAttrValue);
    });
    ret.setRich (aRichGroup);

    eSchema.forAllChildElements (eSchemaChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eSchemaChild.getNamespaceURI ()))
      {
        if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_INCLUDE))
          ret.addInclude (readIncludeFromXML (eSchemaChild));
        else
          if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_TITLE))
            ret.setTitle (readTitleFromXML (eSchemaChild));
          else
            if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_NS))
              ret.addNS (readNSFromXML (eSchemaChild));
            else
              if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_P))
              {
                final PSP aP = readPFromXML (eSchemaChild);
                if (ret.hasNoPatterns ())
                  ret.addStartP (aP);
                else
                  ret.addEndP (aP);
              }
              else
                if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_LET))
                  ret.addLet (readLetFromXML (eSchemaChild));
                else
                  if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_PHASE))
                    ret.addPhase (readPhaseFromXML (eSchemaChild));
                  else
                    if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_PATTERN))
                      ret.addPattern (readPatternFromXML (eSchemaChild));
                    else
                      if (eSchemaChild.getLocalName ().equals (CSchematronXML.ELEMENT_DIAGNOSTICS))
                        ret.setDiagnostics (readDiagnosticsFromXML (eSchemaChild));
                      else
                        _warn (ret, "Unsupported Schematron element '" + eSchemaChild.getLocalName () + "'");
      }
      else
        ret.addForeignElement (eSchemaChild.getClone ());
    });
    return ret;
  }

  /**
   * Read a <span> element
   *
   * @param eSpan
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSSpan readSpanFromXML (@Nonnull final IMicroElement eSpan)
  {
    final PSSpan ret = new PSSpan ();

    eSpan.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_CLASS))
        ret.setClazz (sAttrValue);
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eSpan.forAllChildren (aSpanChild -> {
      switch (aSpanChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aSpanChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aSpanChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            _warn (ret, "Unsupported Schematron element '" + eElement.getLocalName () + "'");
          }
          else
            ret.addForeignElement (eElement.getClone ());

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aSpanChild);
      }
    });
    return ret;
  }

  /**
   * Read a <title> element
   *
   * @param eTitle
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSTitle readTitleFromXML (@Nonnull final IMicroElement eTitle)
  {
    final PSTitle ret = new PSTitle ();

    eTitle.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      _warn (ret, "Unsupported attribute '" + sAttrName + "'='" + sAttrValue + "'");
    });

    eTitle.forAllChildren (aTitleChild -> {
      switch (aTitleChild.getType ())
      {
        case TEXT:
          ret.addText (((IMicroText) aTitleChild).getNodeValue ());
          break;
        case ELEMENT:
          final IMicroElement eElement = (IMicroElement) aTitleChild;
          if (CSchematron.NAMESPACE_SCHEMATRON.equals (eElement.getNamespaceURI ()))
          {
            final String sLocalName = eElement.getLocalName ();
            if (sLocalName.equals (CSchematronXML.ELEMENT_DIR))
              ret.addDir (readDirFromXML (eElement));
            else
              _warn (ret, "Unsupported Schematron element '" + sLocalName + "'");
          }
          else
            _warn (ret, "Unsupported namespace URI '" + eElement.getNamespaceURI () + "'");

          break;
        case COMMENT:
          // Ignore comments
          break;
        default:
          _warn (ret, "Unsupported child node: " + aTitleChild);
      }
    });
    return ret;
  }

  /**
   * Read a <value-of> element
   *
   * @param eValueOf
   *        The source micro element. Never null.
   * @return The created domain object. May not be null.
   */
  @Nonnull
  public PSValueOf readValueOfFromXML (@Nonnull final IMicroElement eValueOf)
  {
    final PSValueOf ret = new PSValueOf ();

    eValueOf.forAllAttributes ( (sNS, sAttrName, sVal) -> {
      final String sAttrValue = _getAttributeValue (sVal);
      if (sAttrName.equals (CSchematronXML.ATTR_SELECT))
        ret.setSelect (sAttrValue);
      else
        ret.addForeignAttribute (sAttrName, sAttrValue);
    });

    eValueOf.forAllChildElements (eValueOfChild -> {
      if (CSchematron.NAMESPACE_SCHEMATRON.equals (eValueOfChild.getNamespaceURI ()))
      {
        _warn (ret, "Unsupported Schematron element '" + eValueOfChild.getLocalName () + "'");
      }
      else
        _warn (ret, "Unsupported namespace URI '" + eValueOfChild.getNamespaceURI () + "'");
    });
    return ret;
  }

  /**
   * Read the schema from the resource supplied in the constructor. First all
   * includes are resolved and than {@link #readSchemaFromXML(IMicroElement)} is
   * called.
   *
   * @return The read {@link PSSchema}.
   * @throws SchematronReadException
   *         If reading fails
   */
  @Nonnull
  public PSSchema readSchema () throws SchematronReadException
  {
    // Resolve all includes as the first action
    final SAXReaderSettings aSettings = new SAXReaderSettings ().setEntityResolver (m_aEntityResolver);

    final IMicroDocument aDoc = SchematronHelper.getWithResolvedSchematronIncludes (m_aResource,
                                                                                aSettings,
                                                                                m_aErrorHandler);
    if (aDoc == null || aDoc.getDocumentElement () == null)
      throw new SchematronReadException (m_aResource,
                                         "Failed to resolve includes in Schematron resource " + m_aResource);

    if (SchematronDebug.isShowResolvedSourceSchematron ())
      LOGGER.info ("Resolved source Schematron:\n" + MicroWriter.getNodeAsString (aDoc));

    return readSchemaFromXML (aDoc.getDocumentElement ());
  }

  @Override
  public String toString ()
  {
    return new ToStringGenerator (this).append ("resource", m_aResource)
                                       .append ("errorHandler", m_aErrorHandler)
                                       .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.saxon;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.URIResolver;

import com.helger.commons.CGlobal;
import com.helger.commons.exception.InitializationException;
import com.helger.commons.lang.ClassLoaderHelper;
import com.helger.xml.transform.DefaultTransformURIResolver;
import com.helger.xml.transform.LoggingTransformErrorListener;

import net.sf.saxon.lib.FeatureKeys;

/**
 * A special {@link TransformerFactory} handler that prefers Saxon's
 * {@link TransformerFactory} before calling the SPI version
 * TransformerFactory.newInstance (). This is mainly to solve the
 * interoperability issue when using Xalan and Saxon together in the class path.
 *
 * @author Philip Helger
 */
@Immutable
public final class SchematronTransformerFactory
{
  public static final String SAXON_TRANSFORMER_FACTORY_CLASS = "net.sf.saxon.TransformerFactoryImpl";
  private static final TransformerFactory s_aDefaultFactory;

  static
  {
    s_aDefaultFactory = createTransformerFactorySaxonFirst ((ClassLoader) null,
                                                            new LoggingTransformErrorListener (CGlobal.DEFAULT_LOCALE),
                                                            new DefaultTransformURIResolver ());
  }

  private SchematronTransformerFactory ()
  {}

  /**
   * @return The default "Saxon first" {@link TransformerFactory}. Never
   *         null.
   */
  @Nonnull
  public static TransformerFactory getDefaultSaxonFirst ()
  {
    return s_aDefaultFactory;
  }

  /**
   * Create a new {@link TransformerFactory} trying to invoke the Saxon
   * implementation first using the class
   * {@value #SAXON_TRANSFORMER_FACTORY_CLASS}.
   *
   * @param aClassLoader
   *        The optional class loader to be used. May be null.
   * @param aErrorListener
   *        An optional XSLT error listener to be used. May be
   *        null.
   * @param aURIResolver
   *        An optional XSLT URI resolver to be used. May be null.
   * @return A new {@link TransformerFactory} and not null.
   * @throws InitializationException
   *         In case initialization fails.
   */
  @Nonnull
  public static TransformerFactory createTransformerFactorySaxonFirst (@Nullable final ClassLoader aClassLoader,
                                                                       @Nullable final ErrorListener aErrorListener,
                                                                       @Nullable final URIResolver aURIResolver)
  {
    TransformerFactory aFactory;
    try
    {
      // Try Saxon first
      final ClassLoader aEffectiveClassLoader = aClassLoader != null ? aClassLoader
                                                                     : ClassLoaderHelper.getContextClassLoader ();
      aFactory = TransformerFactory.newInstance (SAXON_TRANSFORMER_FACTORY_CLASS, aEffectiveClassLoader);

      // Debug/testing only
      if (false)
        aFactory.setFeature (FeatureKeys.TRACE_OPTIMIZER_DECISIONS, true);
      if (false)
        aFactory.setFeature (FeatureKeys.COMPILE_WITH_TRACING, true);
      if (false)
        aFactory.setAttribute (FeatureKeys.XSLT_VERSION, "2.0");
    }
    catch (final TransformerFactoryConfigurationError | TransformerConfigurationException ex)
    {
      try
      {
        // Try default afterwards
        aFactory = TransformerFactory.newInstance ();
      }
      catch (final TransformerFactoryConfigurationError ex2)
      {
        throw new InitializationException ("Failed to create XML TransformerFactory", ex2);
      }
    }

    if (aErrorListener != null)
      aFactory.setErrorListener (aErrorListener);
    if (aURIResolver != null)
      aFactory.setURIResolver (aURIResolver);
    return aFactory;
  }
}


/**
 * Copyright (C) 2014-2019 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.schematron.pure.exchange;

import javax.annotation.Nonnull;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.lang.ICloneable;
import com.helger.commons.string.ToStringGenerator;
import com.helger.schematron.CSchematron;
import com.helger.schematron.pure.model.PSNS;
import com.helger.schematron.pure.model.PSSchema;
import com.helger.xml.namespace.MapBasedNamespaceContext;
import com.helger.xml.serialize.write.EXMLSerializeIndent;
import com.helger.xml.serialize.write.IXMLWriterSettings;
import com.helger.xml.serialize.write.XMLWriterSettings;

/**
 * This class contains the settings to be used with {@link PSWriter}.
 *
 * @author Philip Helger
 */
public class PSWriterSettings implements ICloneable <PSWriterSettings>, IPSWriterSettings
{
  /**
   * The default writer settings to be used when nothing else is specified. By
   * default indent and align is disabled, to avoid that newlines are
   * interpreted as part of the content.
   */
  public static final IPSWriterSettings DEFAULT_SETTINGS = new PSWriterSettings ().setXMLWriterSettings (new XMLWriterSettings ().setIndent (EXMLSerializeIndent.NONE));

  private IXMLWriterSettings m_aXMLWriterSettings = new XMLWriterSettings ();

  public PSWriterSettings ()
  {}

  public PSWriterSettings (@Nonnull final IPSWriterSettings aOther)
  {
    ValueEnforcer.notNull (aOther, "Other");
    m_aXMLWriterSettings = aOther.getXMLWriterSettings ();
  }

  @Nonnull
  public IPSWriterSettings setXMLWriterSettings (@Nonnull final IXMLWriterSettings aXMLWriterSettings)
  {
    ValueEnforcer.notNull (aXMLWriterSettings, "XMLWriterSettings");
    m_aXMLWriterSettings = new XMLWriterSettings (aXMLWriterSettings);
    return this;
  }

  @Nonnull
  @ReturnsMutableCopy
  public XMLWriterSettings getXMLWriterSettings ()
  {
    return new XMLWriterSettings (m_aXMLWriterSettings);
  }

  /**
   * Helper method to extract the namespace mapping from the provided
   * Schematron.
   *
   * @param aSchema
   *        The schema to extract the namespace context from. May not be
   *        null.
   * @return A non-null but maybe empty namespace context
   */
  @Nonnull
  @ReturnsMutableCopy
  public static MapBasedNamespaceContext createNamespaceMapping (@Nonnull final PSSchema aSchema)
  {
    final MapBasedNamespaceContext ret = new MapBasedNamespaceContext ();
    ret.addDefaultNamespaceURI (CSchematron.NAMESPACE_SCHEMATRON);
    for (final PSNS aItem : aSchema.getAllNSs ())
      ret.addMapping (aItem.getPrefix (), aItem.getUri ());
    return ret;
  }

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

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