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.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.IModule;
import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
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.ObjectUtils;
import gov.nist.secauto.metaschema.databind.io.BindingException;
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 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.List;

import javax.xml.namespace.QName;
import javax.xml.transform.Source;

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 the singleton {@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
   */
  @NonNull
  static IBindingContext instance() {
    return DefaultBindingContext.instance();
  }

  /**
   * 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);

  /**
   * Register a matcher used to identify a bound class by the definition's root
   * name.
   *
   * @param clazz
   *          the definition class to match for, which must represent a root
   *          assembly definition
   * @return the matcher
   */
  @NonNull
  IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz);

  /**
   * 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
   * @see IBindingContext#registerBindingMatcher(Class)
   */
  @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
   * @see IBindingContext#registerBindingMatcher(Class)
   */
  @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
  <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz);

  /**
   * 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);

  /**
   * Generate, compile, and load a set of generated Module annotated Java classes
   * based on the provided Module {@code module}.
   *
   * @param module
   *          the Module module to generate classes for
   * @param compilePath
   *          the path to the directory to generate classes in
   * @return this instance
   * @throws IOException
   *           if an error occurred while generating or loading the classes
   */
  @NonNull
  IBindingContext registerModule(
      @NonNull IModule module,
      @NonNull Path compilePath) throws IOException;

  /**
   * 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.
   *
   * @return the instance
   */
  @NonNull
  IBoundLoader newBoundLoader();

  /**
   * 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);

    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);
  }

  interface IModuleLoaderStrategy {
    /**
     * Load the bound Metaschema module represented by the provided class.
     * <p>
     * Implementations are allowed to return a cached instance if the module has
     * already been loaded.
     *
     * @param clazz
     *          the Module class
     * @return the module
     * @throws IllegalStateException
     *           if an error occurred while processing the associated module
     *           information
     */
    @NonNull
    IBoundModule loadModule(@NonNull Class<? extends IBoundModule> clazz);

    /**
     * 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);
  }

  interface ISchemaValidationProvider {

    @NonNull
    default IValidationResult validateWithSchema(@NonNull URI target, @NonNull Format asFormat)
        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 = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, target);
        break;
      }
      case XML:
        try {
          List<Source> schemaSources = getXmlSchemas(targetResource);
          retval = new XmlSchemaContentValidator(schemaSources).validate(target);
        } catch (SAXException ex) {
          throw new IOException(ex);
        }
        break;
      case YAML: {
        JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
        assert json != null;
        retval = new JsonSchemaContentValidator(getJsonSchema(json)).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
     *
     * @return the JSON schema
     * @throws IOException
     *           if an error occurred while loading the schema
     */
    @NonNull
    JSONObject getJsonSchema(@NonNull JSONObject json) throws IOException;

    /**
     * Get a XML schema to use for content validation.
     *
     * @param targetResource
     *          the URL for the XML content to validate
     *
     * @return the XML schema sources
     * @throws IOException
     *           if an error occurred while loading the schema
     */
    @NonNull
    List<Source> getXmlSchemas(@NonNull URL targetResource) throws IOException;
  }

  /**
   * 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 {
    @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);
  }
}