/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.daffodil.validation

import java.io.IOException
import java.net.URL
import java.util.Properties
import javax.xml.XMLConstants
import javax.xml.transform.stream.StreamSource

import org.apache.daffodil.api
import org.apache.daffodil.lib.util.Misc
import org.apache.daffodil.lib.util.ThreadSafePool
import org.apache.daffodil.lib.xml.DFDLCatalogResolver
import org.apache.daffodil.lib.xml.XMLUtils

import XercesValidator.XercesValidatorImpl
import org.xml.sax.ErrorHandler
import org.xml.sax.SAXParseException

/**
 * Provides a XercesValidator instance
 *
 * SPI service name: xerces
 * 
 * Configuration requirements.
 *   <ul>
 *    <li>xerces=schema_file_url_string</li>
 *   </ul>
 */
class XercesValidatorFactory extends api.validation.ValidatorFactory {
  def name(): String = XercesValidator.name

  def make(config: Properties): api.validation.Validator =
    XercesValidatorFactory.makeValidator(config)
}

object XercesValidatorFactory {
  def makeValidator(config: Properties): api.validation.Validator = {
    val schemaPath = config.getProperty(XercesValidator.name)
    val schemaFile = {
      if (!Misc.isNullOrBlank(schemaPath)) schemaPath
      else
        throw new api.validation.ValidatorInitializationException(
          "invalid configuration: xerces property is empty or not defined"
        )
    }
    val url = new URL(schemaFile)
    XercesValidator.fromURL(url)
  }
}

/**
 * Use this for extra validation passes in the TDML Runner
 * to do a validation pass on the TDML expected Infoset w.r.t. the model and to
 * do a validation pass on the actual result w.r.t. the model as an XML document.
 */
class XercesValidator(schemaSource: javax.xml.transform.Source)
  extends api.validation.Validator {

  private val schema = {
    val factory = new org.apache.xerces.jaxp.validation.XMLSchemaFactory()
    factory.setResourceResolver(DFDLCatalogResolver.get)
    factory.newSchema(schemaSource)
  }

  // the Xerces Validator is not thread safe, so we use a ThreadSafePool. The use of a pool
  // allows reuse of objects that are expensive to create while ensuring each Thread gets their
  // own instance. Note that we do not use ThreadLocal, since that can lead to memory leaks that
  // cannot be easily cleaned up
  private val validatorPool = new ThreadSafePool[XercesValidatorImpl] {
    override def allocate(): XercesValidatorImpl = {
      val v = schema.newValidator
      v.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
      v.setFeature(XMLUtils.XML_DISALLOW_DOCTYPE_FEATURE, true)
      v.setFeature("http://xml.org/sax/features/validation", true)
      v.setFeature("http://apache.org/xml/features/validation/schema", true)
      v.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true)
      v
    }
  }

  def validateXML(
    document: java.io.InputStream,
    validationHandler: api.validation.ValidationHandler
  ): Unit = {
    val eh = new XercesErrorHandler(validationHandler)
    validateXML(document, eh)
  }

  def validateXML(
    document: java.io.InputStream,
    eh: ErrorHandler
  ): Unit = {

    val documentSource = new StreamSource(document)

    validatorPool.withInstance { xv =>
      try {
        // DFDLCatalogResolver.get returns a resolver for use by a thread, but the validator in
        // the pool might have been created with another thread. We need to make sure this
        // validator uses the resolver allocated for this thread.
        xv.setResourceResolver(DFDLCatalogResolver.get)
        xv.setErrorHandler(eh)
        xv.validate(documentSource)
      } catch {
        // can be thrown by the resolver if it cannot
        // resolve the schemaLocation of an include/import.
        // Regular Xerces doesn't report this as an error.
        case spe: SAXParseException => eh.error(spe)
      }
    }
  }
}

object XercesValidator {
  private type XercesValidatorImpl = javax.xml.validation.Validator
  val name = "xerces"

  def fromURL(schemaURL: URL) = new XercesValidator({
    val is =
      try {
        schemaURL.openStream()
      } catch {
        case e: IOException =>
          throw new api.validation.ValidatorInitializationException(e.getMessage)
      }
    val stream = new StreamSource(is)
    stream.setSystemId(
      schemaURL.toString
    ) // must set this so that relative URIs will be created for import/include files.
    stream
  })
}

private class XercesErrorHandler(validationHandler: api.validation.ValidationHandler)
  extends ErrorHandler {
  override def warning(spe: SAXParseException): Unit =
    validationHandler.validationErrorNoContext(spe)
  override def error(spe: SAXParseException): Unit =
    validationHandler.validationErrorNoContext(spe)
  override def fatalError(spe: SAXParseException): Unit =
    validationHandler.validationErrorNoContext(spe)
}
