IBindingContext.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.databind;

import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
import gov.nist.secauto.metaschema.core.datatype.DataTypeService;
import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.core.model.IModuleLoader;
import gov.nist.secauto.metaschema.core.model.MetaschemaException;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.codegen.DefaultModuleBindingGenerator;
import gov.nist.secauto.metaschema.databind.io.BindingException;
import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
import gov.nist.secauto.metaschema.databind.io.Format;
import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
import gov.nist.secauto.metaschema.databind.io.IDeserializer;
import gov.nist.secauto.metaschema.databind.io.ISerializer;
import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
import gov.nist.secauto.metaschema.databind.model.IBoundModule;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;

import org.json.JSONObject;
import org.json.JSONTokener;
import org.xml.sax.SAXException;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;

import javax.xml.namespace.QName;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
 * Provides information supporting a binding between a set of Module models and
 * corresponding Java classes.
 */
public interface IBindingContext {
  /**
   * Get a new builder that can produce a new, configured binding context.
   *
   * @return the builder
   * @since 2.0.0
   */
  static BindingContextBuilder builder() {
    return new BindingContextBuilder();
  }

  /**
   * Get a new {@link IBindingContext} instance, which can be used to load
   * information that binds a model to a set of Java classes.
   *
   * @return a new binding context
   * @since 2.0.0
   */
  @NonNull
  static IBindingContext newInstance() {
    return new DefaultBindingContext();
  }

  /**
   * Get a new {@link IBindingContext} instance, which can be used to load
   * information that binds a model to a set of Java classes.
   *
   * @param strategy
   *          the loader strategy to use when loading Metaschema modules
   * @return a new binding context
   * @since 2.0.0
   */
  @NonNull
  static IBindingContext newInstance(@NonNull IBindingContext.IModuleLoaderStrategy strategy) {
    return new DefaultBindingContext(strategy);
  }

  /**
   * Get the Metaschema module loader strategy used by this binding context to
   * load modules.
   *
   * @return the strategy instance
   * @since 2.0.0
   */
  @NonNull
  IModuleLoaderStrategy getModuleLoaderStrategy();

  /**
   * Get a loader that supports loading a Metaschema module from a specified
   * resource.
   * <p>
   * Modules loaded with this loader are automatically registered with this
   * binding context.
   * <p>
   * Use of this method requires that the binding context is initialized using a
   * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
   * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
   * initialized using the {@link DefaultModuleBindingGenerator}. * @return the
   * loader
   *
   * @return the loader
   * @since 2.0.0
   */
  @NonNull
  IBindingModuleLoader newModuleLoader();

  /**
   * Loads a Metaschema module from the specified path.
   * <p>
   * This method automatically registers the module with this binding context.
   * <p>
   * Use of this method requires that the binding context is initialized using a
   * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
   * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
   * initialized using the {@link DefaultModuleBindingGenerator}.
   *
   * @param path
   *          the path to load the module from
   * @return the loaded Metaschema module
   * @throws MetaschemaException
   *           if an error occurred while processing the resource
   * @throws IOException
   *           if an error occurred parsing the resource
   * @throws UnsupportedOperationException
   *           if this binding context is not configured to support dynamic bound
   *           module loading
   * @since 2.0.0
   */
  @NonNull
  default IBindingMetaschemaModule loadMetaschema(@NonNull Path path) throws MetaschemaException, IOException {
    return newModuleLoader().load(path);
  }

  /**
   * Loads a Metaschema module from the specified URL.
   * <p>
   * This method automatically registers the module with this binding context.
   * <p>
   * Use of this method requires that the binding context is initialized using a
   * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
   * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
   * initialized using the {@link DefaultModuleBindingGenerator}.
   *
   * @param url
   *          the URL to load the module from
   * @return the loaded Metaschema module
   * @throws MetaschemaException
   *           if an error occurred while processing the resource
   * @throws IOException
   *           if an error occurred parsing the resource
   * @throws UnsupportedOperationException
   *           if this binding context is not configured to support dynamic bound
   *           module loading
   * @since 2.0.0
   */
  @NonNull
  default IBindingMetaschemaModule loadMetaschema(@NonNull URL url) throws MetaschemaException, IOException {
    return newModuleLoader().load(url);
  }

  /**
   * Get a loader that supports loading Metaschema module constraints from a
   * specified resource.
   * <p>
   * Metaschema module constraints loaded this need to be used with a new
   * {@link IBindingContext} instance to be applied to loaded modules. The new
   * binding context must initialized using the
   * {@link PostProcessingModuleLoaderStrategy} that is initialized with a
   * {@link ExternalConstraintsModulePostProcessor} instance.
   *
   * @return the loader
   * @since 2.0.0
   */
  @NonNull
  static IConstraintLoader getConstraintLoader() {
    return new BindingConstraintLoader(DefaultBindingContext.instance());
  }

  /**
   * Get a loader that supports loading Metaschema module constraints from a
   * specified resource.
   * <p>
   * Metaschema module constraints loaded this need to be used with a new
   * {@link IBindingContext} instance to be applied to loaded modules. The new
   * binding context must initialized using the
   * {@link PostProcessingModuleLoaderStrategy} that is initialized with a
   * {@link ExternalConstraintsModulePostProcessor} instance.
   *
   * @return the loader
   * @since 2.0.0
   */
  @NonNull
  default IConstraintLoader newConstraintLoader() {
    return new BindingConstraintLoader(this);
  }

  /**
   * Load a bound Metaschema module implemented by the provided class.
   * <p>
   * Also registers any associated bound classes.
   * <p>
   * Implementations are expected to return the same IModule instance for multiple
   * calls to this method with the same class argument.
   *
   * @param clazz
   *          the class implementing a bound Metaschema module
   * @return the loaded module
   */
  @NonNull
  IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz);

  /**
   * Registers the provided Metaschema module with this binding context.
   * <p>
   * If the provided instance is not an instance of {@link IBoundModule}, then
   * annotated Java classes for this module will be generated, compiled, and
   * loaded based on the provided Module.
   *
   * @param module
   *          the Module module to generate classes for
   * @return the registered module, which may be a different instance than what
   *         was provided if dynamic compilation was performed
   * @throws UnsupportedOperationException
   *           if this binding context is not configured to support dynamic bound
   *           module loading and the module instance is not a subclass of
   *           {@link IBoundModule}
   * @since 2.0.0
   */
  @NonNull
  default IBoundModule registerModule(@NonNull IModule module) {
    return getModuleLoaderStrategy().registerModule(module, this);
  }

  /**
   * Register a class binding for a given bound class.
   *
   * @param definition
   *          the bound class information to register
   * @return the old bound class information or {@code null} if no binding existed
   *         for the associated class
   */
  @Nullable
  IBoundDefinitionModelComplex registerClassBinding(@NonNull IBoundDefinitionModelComplex definition);

  /**
   * Get the {@link IBoundDefinitionModel} instance associated with the provided
   * Java class.
   * <p>
   * Typically the class will have a {@link MetaschemaAssembly} or
   * {@link MetaschemaField} annotation.
   *
   * @param clazz
   *          the class binding to load
   * @return the associated class binding instance or {@code null} if the class is
   *         not bound
   */
  @Nullable
  IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);

  /**
   * Determine the bound class for the provided XML {@link QName}.
   *
   * @param rootQName
   *          the root XML element's QName
   * @return the bound class or {@code null} if not recognized
   */
  @Nullable
  Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName);

  /**
   * Determine the bound class for the provided JSON/YAML property/item name using
   * any registered matchers.
   *
   * @param rootName
   *          the JSON/YAML property/item name
   * @return the bound class or {@code null} if not recognized
   */
  @Nullable
  Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName);

  /**
   * Get's the {@link IDataTypeAdapter} associated with the specified Java class,
   * which is used to read and write XML, JSON, and YAML data to and from
   * instances of that class. Thus, this adapter supports a direct binding between
   * the Java class and structured data in one of the supported formats. Adapters
   * are used to support bindings for simple data objects (e.g., {@link String},
   * {@link BigInteger}, {@link ZonedDateTime}, etc).
   *
   * @param <TYPE>
   *          the class type of the adapter
   * @param clazz
   *          the Java {@link Class} for the bound type
   * @return the adapter instance or {@code null} if the provided class is not
   *         bound
   */
  @Nullable
  default <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz) {
    return DataTypeService.getInstance().getJavaTypeAdapterByClass(clazz);
  }

  /**
   * Gets a data {@link ISerializer} which can be used to write Java instance data
   * for the provided class in the requested format.
   * <p>
   * The provided class must be a bound Java class with a
   * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
   * {@link IBoundDefinitionModel} exists.
   *
   * @param <CLASS>
   *          the Java type this serializer can write data from
   * @param format
   *          the format to serialize into
   * @param clazz
   *          the Java data object to serialize
   * @return the serializer instance
   * @throws NullPointerException
   *           if any of the provided arguments, except the configuration, are
   *           {@code null}
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   * @throws UnsupportedOperationException
   *           if the requested format is not supported by the implementation
   * @see #getBoundDefinitionForClass(Class)
   */
  @NonNull
  <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
      @NonNull Format format,
      @NonNull Class<CLASS> clazz);

  /**
   * Gets a data {@link IDeserializer} which can be used to read Java instance
   * data for the provided class from the requested format.
   * <p>
   * The provided class must be a bound Java class with a
   * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
   * {@link IBoundDefinitionModel} exists.
   *
   * @param <CLASS>
   *          the Java type this deserializer can read data into
   * @param format
   *          the format to serialize into
   * @param clazz
   *          the Java data type to serialize
   * @return the deserializer instance
   * @throws NullPointerException
   *           if any of the provided arguments, except the configuration, are
   *           {@code null}
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   * @throws UnsupportedOperationException
   *           if the requested format is not supported by the implementation
   * @see #getBoundDefinitionForClass(Class)
   */
  @NonNull
  <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
      @NonNull Format format,
      @NonNull Class<CLASS> clazz);

  /**
   * Get a new {@link IBoundLoader} instance to load bound content instances.
   *
   * @return the instance
   */
  @NonNull
  default IBoundLoader newBoundLoader() {
    return new DefaultBoundLoader(this);
  }

  /**
   * Create a deep copy of the provided bound object.
   *
   * @param <CLASS>
   *          the bound object type
   * @param other
   *          the object to copy
   * @param parentInstance
   *          the object's parent or {@code null}
   * @return a deep copy of the provided object
   * @throws BindingException
   *           if an error occurred copying content between java instances
   * @throws NullPointerException
   *           if the provided object is {@code null}
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   */
  @NonNull
  <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
      throws BindingException;

  /**
   * Get a new single use constraint validator.
   *
   * @param handler
   *          the validation handler to use to process the validation results
   * @param config
   *          the validation configuration
   *
   * @return the validator
   */
  default IConstraintValidator newValidator(
      @NonNull IConstraintValidationHandler handler,
      @Nullable IConfiguration<ValidationFeature<?>> config) {
    IBoundLoader loader = newBoundLoader();
    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);

    DynamicContext context = new DynamicContext();
    context.setDocumentLoader(loader);

    DefaultConstraintValidator retval = new DefaultConstraintValidator(handler);
    if (config != null) {
      retval.applyConfiguration(config);
    }
    return retval;
  }

  /**
   * Perform constraint validation on the provided bound object represented as an
   * {@link IDocumentNodeItem}.
   *
   * @param nodeItem
   *          the node item to validate
   * @param loader
   *          a module loader used to load and resolve referenced resources
   * @param config
   *          the validation configuration
   * @return the validation result
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   */
  default IValidationResult validate(
      @NonNull IDocumentNodeItem nodeItem,
      @NonNull IBoundLoader loader,
      @Nullable IConfiguration<ValidationFeature<?>> config) {
    IRootAssemblyNodeItem root = nodeItem.getRootAssemblyNodeItem();
    return validate(root, loader, config);
  }

  /**
   * Perform constraint validation on the provided bound object represented as an
   * {@link IDefinitionNodeItem}.
   *
   * @param nodeItem
   *          the node item to validate
   * @param loader
   *          a module loader used to load and resolve referenced resources
   * @param config
   *          the validation configuration
   * @return the validation result
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   */
  default IValidationResult validate(
      @NonNull IDefinitionNodeItem<?, ?> nodeItem,
      @NonNull IBoundLoader loader,
      @Nullable IConfiguration<ValidationFeature<?>> config) {

    FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
    IConstraintValidator validator = newValidator(handler, config);

    DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
    dynamicContext.setDocumentLoader(loader);

    validator.validate(nodeItem, dynamicContext);
    validator.finalizeValidation(dynamicContext);
    return handler;
  }

  /**
   * Load and perform schema and constraint validation on the target. The
   * constraint validation will only be performed if the schema validation passes.
   *
   * @param target
   *          the target to validate
   * @param asFormat
   *          the schema format to use to validate the target
   * @param schemaProvider
   *          provides callbacks to get the appropriate schemas
   * @param config
   *          the validation configuration
   * @return the validation result
   * @throws IOException
   *           if an error occurred while reading the target
   */
  default IValidationResult validate(
      @NonNull URI target,
      @NonNull Format asFormat,
      @NonNull ISchemaValidationProvider schemaProvider,
      @Nullable IConfiguration<ValidationFeature<?>> config) throws IOException {

    IValidationResult retval = schemaProvider.validateWithSchema(target, asFormat, this);

    if (retval.isPassing()) {
      IValidationResult constraintValidationResult = validateWithConstraints(target, config);
      retval = AggregateValidationResult.aggregate(retval, constraintValidationResult);
    }
    return retval;
  }

  /**
   * Load and validate the provided {@code target} using the associated Module
   * module constraints.
   *
   * @param target
   *          the file to load and validate
   * @param config
   *          the validation configuration
   * @return the validation results
   * @throws IOException
   *           if an error occurred while parsing the target
   */
  default IValidationResult validateWithConstraints(
      @NonNull URI target,
      @Nullable IConfiguration<ValidationFeature<?>> config)
      throws IOException {
    IBoundLoader loader = newBoundLoader();
    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
    IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);

    return validate(nodeItem, loader, config);
  }

  /**
   * A behavioral class used by the binding context to load Metaschema modules.
   * <p>
   * A module will flow through the following process.
   * <ol>
   * <li><b>Loading:</b> The module is read from its source.</li>
   * <li><b>Post Processing:</b> The module is prepared for use.</li>
   * <li><b>Registration:</b> The module is registered for use.</li>
   * </ol>
   * <p>
   * A module will be loaded when either the module or one of its global
   * definitions is accessed the first time.
   */
  interface IModuleLoaderStrategy extends ModuleLoadingPostProcessor {
    /**
     * Load the bound Metaschema module represented by the provided class.
     * <p>
     * This is the primary entry point for loading an already bound module. This
     * method must ensure that the loaded module is post-processed and registered.
     * <p>
     * Implementations are allowed to return a cached instance if the module has
     * already been loaded by this method.
     *
     * @param clazz
     *          the Module class
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the module
     * @throws IllegalStateException
     *           if an error occurred while processing the associated module
     *           information
     * @since 2.0.0
     */
    @NonNull
    IBoundModule loadModule(
        @NonNull Class<? extends IBoundModule> clazz,
        @NonNull IBindingContext bindingContext);

    /**
     * Perform post-processing on the module.
     *
     * @param module
     *          the Metaschema module to post-process
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @since 2.0.0
     */
    @Override
    default void postProcessModule(
        @NonNull IModule module,
        @NonNull IBindingContext bindingContext) {
      // do nothing by default
    }

    /**
     * Registers the provided Metaschema module.
     * <p>
     * If this module has not been post-processed, this method is expected to drive
     * post-processing first.
     * <p>
     * If the provided instance is not an instance of {@link IBoundModule}, then
     * annotated Java classes for this module will be generated, compiled, and
     * loaded based on the provided Module.
     *
     * @param module
     *          the Module module to generate classes for
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the registered module, which may be a different instance than what
     *         was provided if dynamic compilation was performed
     * @throws UnsupportedOperationException
     *           if this binding context is not configured to support dynamic bound
     *           module loading and the module instance is not a subclass of
     *           {@link IBoundModule}
     * @since 2.0.0
     */
    @NonNull
    IBoundModule registerModule(
        @NonNull IModule module,
        @NonNull IBindingContext bindingContext);
    //
    // /**
    // * Register a matcher used to identify a bound class by the definition's root
    // * name.
    // *
    // * @param definition
    // * the definition to match for
    // * @return the matcher
    // */
    // @NonNull
    // IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly
    // definition);

    /**
     * Get the matchers used to identify the bound class associated with the
     * definition's root name.
     *
     * @return the matchers
     */
    @NonNull
    Collection<IBindingMatcher> getBindingMatchers();

    /**
     * Get the {@link IBoundDefinitionModel} instance associated with the provided
     * Java class.
     * <p>
     * Typically the class will have a {@link MetaschemaAssembly} or
     * {@link MetaschemaField} annotation.
     *
     * @param clazz
     *          the class binding to load
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the associated class binding instance
     * @throws IllegalArgumentException
     *           if the class is not a bound definition with a
     *           {@link MetaschemaAssembly} or {@link MetaschemaField} annotation
     */
    @NonNull
    IBoundDefinitionModelComplex getBoundDefinitionForClass(
        @NonNull Class<? extends IBoundObject> clazz,
        @NonNull IBindingContext bindingContext);
  }

  /**
   * Enables building a {@link IBindingContext} using common configuration options
   * based on the builder pattern.
   *
   * @since 2.0.0
   */
  final class BindingContextBuilder {
    private Path compilePath;
    private final List<IModuleLoader.IModulePostProcessor> postProcessors = new LinkedList<>();
    private final List<IConstraintSet> constraintSets = new LinkedList<>();
    @NonNull
    private final Function<IBindingContext.IModuleLoaderStrategy, IBindingContext> initializer;

    private BindingContextBuilder() {
      this(DefaultBindingContext::new);
    }

    /**
     * Construct a new builder.
     *
     * @param initializer
     *          the callback to use to get a new binding context instance
     */
    public BindingContextBuilder(
        @NonNull Function<IBindingContext.IModuleLoaderStrategy, IBindingContext> initializer) {
      this.initializer = initializer;
    }

    /**
     * Enable dynamic code generation and compilation for Metaschema module-based
     * classes.
     *
     * @param path
     *          the path to use to generate and compile Metaschema module-based
     *          classes
     * @return this builder
     */
    @NonNull
    public BindingContextBuilder compilePath(@NonNull Path path) {
      compilePath = path;
      return this;
    }

    /**
     * Configure a Metaschema module post processor.
     *
     * @param processor
     *          the post processor to configure
     * @return this builder
     */
    @NonNull
    public BindingContextBuilder postProcessor(@NonNull IModuleLoader.IModulePostProcessor processor) {
      postProcessors.add(processor);
      return this;
    }

    /**
     * Configure a set of constraints targeting Metaschema modules.
     *
     * @param set
     *          the constraint set to configure
     * @return this builder
     */
    @NonNull
    public BindingContextBuilder constraintSet(@NonNull IConstraintSet set) {
      constraintSets.add(set);
      return this;
    }

    /**
     * Configure a collection of constraint sets targeting Metaschema modules.
     *
     * @param set
     *          the constraint sets to configure
     * @return this builder
     */
    @NonNull
    public BindingContextBuilder constraintSet(@NonNull Collection<IConstraintSet> set) {
      constraintSets.addAll(set);
      return this;
    }

    /**
     * Build a {@link IBindingContext} using the configuration options provided to
     * the builder.
     *
     * @return a new, configured binding context
     */
    @NonNull
    public IBindingContext build() {
      // get loader strategy based on if code generation is configured
      IBindingContext.IModuleLoaderStrategy strategy = compilePath == null
          ? new SimpleModuleLoaderStrategy()
          : new SimpleModuleLoaderStrategy(new DefaultModuleBindingGenerator(compilePath));

      // determine if any post processors are configured or need to be
      List<IModuleLoader.IModulePostProcessor> processors = new LinkedList<>(postProcessors);
      if (!constraintSets.isEmpty()) {
        processors.add(new ExternalConstraintsModulePostProcessor(constraintSets));
      }

      if (!processors.isEmpty()) {
        // post processors are configured, configure the loader strategy to handle them
        strategy = new PostProcessingModuleLoaderStrategy(
            CollectionUtil.unmodifiableList(processors),
            strategy);
      }

      return ObjectUtils.notNull(initializer.apply(strategy));
    }
  }

  /**
   * Provides schema validation capabilities.
   */
  interface ISchemaValidationProvider {

    /**
     * Validate the target resource.
     *
     * @param target
     *          the resource to validate
     * @param asFormat
     *          the format to validate the content as
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the validation result
     * @throws FileNotFoundException
     *           if the resource was not found
     * @throws IOException
     *           if an error occurred while reading the resource
     */
    @NonNull
    default IValidationResult validateWithSchema(
        @NonNull URI target,
        @NonNull Format asFormat,
        @NonNull IBindingContext bindingContext)
        throws FileNotFoundException, IOException {
      URL targetResource = ObjectUtils.notNull(target.toURL());

      IValidationResult retval;
      switch (asFormat) {
      case JSON: {
        JSONObject json;
        try (@SuppressWarnings("resource")
        InputStream is
            = new BufferedInputStream(ObjectUtils.notNull(targetResource.openStream()))) {
          json = new JSONObject(new JSONTokener(is));
        }
        retval = getJsonSchema(json, bindingContext).validate(json, target);
        break;
      }
      case XML:
        try {
          retval = getXmlSchemas(targetResource, bindingContext).validate(target);
        } catch (SAXException ex) {
          throw new IOException(ex);
        }
        break;
      case YAML: {
        JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
        assert json != null;
        retval = getJsonSchema(json, bindingContext).validate(json, ObjectUtils.notNull(target));
        break;
      }
      default:
        throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
      }
      return retval;
    }

    /**
     * Get a JSON schema to use for content validation.
     *
     * @param json
     *          the JSON content to validate
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the JSON schema validator
     * @throws IOException
     *           if an error occurred while loading the schema
     * @since 2.0.0
     */
    @NonNull
    JsonSchemaContentValidator getJsonSchema(@NonNull JSONObject json, @NonNull IBindingContext bindingContext)
        throws IOException;

    /**
     * Get a XML schema to use for content validation.
     *
     * @param targetResource
     *          the URL for the XML content to validate
     * @param bindingContext
     *          the Metaschema binding context used to load bound resources
     * @return the XML schema validator
     * @throws IOException
     *           if an error occurred while loading the schema
     * @throws SAXException
     *           if an error occurred while parsing the schema
     * @since 2.0.0
     */
    @NonNull
    XmlSchemaContentValidator getXmlSchemas(@NonNull URL targetResource, @NonNull IBindingContext bindingContext)
        throws IOException, SAXException;
  }

  /**
   * Implementations of this interface provide a means by which a bound class can
   * be found that corresponds to an XML element, JSON property, or YAML item
   * name.
   */
  interface IBindingMatcher {
    /**
     * Construct a new binding matcher for the provided assembly definition.
     *
     * @param assembly
     *          the assembly definition that matcher is for
     * @return the matcher
     */
    @SuppressWarnings("PMD.ShortMethodName")
    @NonNull
    static IBindingMatcher of(IBoundDefinitionModelAssembly assembly) {
      if (!assembly.isRoot()) {
        throw new IllegalArgumentException(
            String.format("The provided class '%s' is not a root assembly.", assembly.getBoundClass().getName()));
      }
      return new RootAssemblyBindingMatcher(assembly);
    }

    /**
     * Determine the bound class for the provided XML {@link QName}.
     *
     * @param rootQName
     *          the root XML element's QName
     * @return the bound class for the XML qualified name or {@code null} if not
     *         recognized
     */
    Class<? extends IBoundObject> getBoundClassForXmlQName(QName rootQName);

    /**
     * Determine the bound class for the provided JSON/YAML property/item name.
     *
     * @param rootName
     *          the JSON/YAML property/item name
     * @return the bound class for the JSON property name or {@code null} if not
     *         recognized
     */
    Class<? extends IBoundObject> getBoundClassForJsonName(String rootName);
  }
}