IBindingContext.java

/*
 * Portions of this software was developed by employees of the National Institute
 * of Standards and Technology (NIST), an agency of the Federal Government and is
 * being made available as a public service. Pursuant to title 17 United States
 * Code Section 105, works of NIST employees are not subject to copyright
 * protection in the United States. This software may be subject to foreign
 * copyright. Permission in the United States and in foreign countries, to the
 * extent that NIST may hold copyright, to use, copy, modify, create derivative
 * works, and distribute this software and its documentation without fee is hereby
 * granted on a non-exclusive basis, provided that this notice and disclaimer
 * of warranty appears in all copies.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
 */

package gov.nist.secauto.metaschema.databind;

import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.StaticContext;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
import gov.nist.secauto.metaschema.core.model.IFlagContainer;
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.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.codegen.ModuleCompilerHelper;
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.IAssemblyClassBinding;
import gov.nist.secauto.metaschema.databind.model.IClassBinding;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;

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

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
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 extends IModuleLoaderStrategy {

  /**
   * 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 content's root name.
   *
   * @param matcher
   *          the matcher implementation
   * @return this instance
   */
  @NonNull
  IBindingContext registerBindingMatcher(@NonNull IBindingMatcher matcher);

  /**
   * 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(IBindingMatcher)
   */
  @Nullable
  Class<?> getBoundClassForXmlQName(@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(IBindingMatcher)
   */
  @Nullable
  Class<?> getBoundClassForJsonName(@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);

  /**
   * 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
   */
  @SuppressWarnings("PMD.UseProperClassLoader") // false positive
  @NonNull
  default IBindingContext registerModule(
      @NonNull IModule module,
      @NonNull Path compilePath) throws IOException {
    Files.createDirectories(compilePath);

    ClassLoader classLoader = ModuleCompilerHelper.newClassLoader(
        compilePath,
        ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));

    ModuleCompilerHelper.compileMetaschema(module, compilePath).getGlobalDefinitionClassesAsStream()
        .filter(definitionInfo -> {
          boolean retval = false;
          IFlagContainer definition = definitionInfo.getDefinition();
          if (definition instanceof IAssemblyDefinition) {
            IAssemblyDefinition assembly = (IAssemblyDefinition) definition;
            if (assembly.isRoot()) {
              retval = true;
            }
          }
          return retval;
        })
        .map(
            generatedClass -> {
              try {
                @SuppressWarnings("unchecked") Class<IAssemblyClassBinding> clazz
                    = ObjectUtils.notNull((Class<IAssemblyClassBinding>) classLoader
                        .loadClass(generatedClass.getClassName().reflectionName()));

                IAssemblyDefinition definition = (IAssemblyDefinition) generatedClass.getDefinition();
                return new DynamicBindingMatcher(
                    definition,
                    clazz);
              } catch (ClassNotFoundException ex) {
                throw new IllegalStateException(ex);
              }
            })
        .forEachOrdered(
            matcher -> registerBindingMatcher(
                ObjectUtils.notNull(
                    matcher)));
    return this;
  }

  /**
   * Gets a data {@link ISerializer} which can be used to write Java instance data
   * for the provided class to the requested format. The provided class must be a
   * bound Java class with a {@link MetaschemaAssembly} or {@link MetaschemaField}
   * annotation for which a {@link IClassBinding} exists.
   *
   * @param <CLASS>
   *          the Java type this deserializer can write data from
   * @param format
   *          the format to serialize into
   * @param clazz
   *          the Java data type 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 #getClassBinding(Class)
   */
  @NonNull
  <CLASS> 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. The provided class
   * must be a bound Java class with a {@link MetaschemaAssembly} or
   * {@link MetaschemaField} annotation for which a {@link IClassBinding} 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 #getClassBinding(Class)
   */
  @NonNull
  <CLASS> 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> CLASS copyBoundObject(@NonNull CLASS other, Object parentInstance) throws BindingException;

  /**
   * Get a new single use constraint validator.
   *
   * @param handler
   *          the validation handler to use to process the validation results
   *
   * @return the validator
   */
  default IConstraintValidator newValidator(@NonNull IConstraintValidationHandler handler) {
    IBoundLoader loader = newBoundLoader();
    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);

    DynamicContext context = StaticContext.newInstance().newDynamicContext();
    context.setDocumentLoader(loader);

    return new DefaultConstraintValidator(context, handler);
  }

  /**
   * Perform constraint validation on the provided bound object represented as an
   * {@link INodeItem}.
   *
   * @param nodeItem
   *          the node item to validate
   * @return the validation result
   * @throws IllegalArgumentException
   *           if the provided class is not bound to a Module assembly or field
   */
  default IValidationResult validate(@NonNull INodeItem nodeItem) {
    FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
    IConstraintValidator validator = newValidator(handler);
    validator.validate(nodeItem);
    validator.finalizeValidation();
    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
   * @return the validation result
   * @throws IOException
   *           if an error occurred while reading the target
   * @throws SAXException
   *           if an error occurred when parsing the target as XML
   */
  default IValidationResult validate(
      @NonNull Path target,
      @NonNull Format asFormat,
      @NonNull IValidationSchemaProvider schemaProvider) throws IOException, SAXException {
    IValidationResult retval;
    switch (asFormat) {
    case JSON:
      retval = new JsonSchemaContentValidator(schemaProvider.getJsonSchema()).validate(target);
      break;
    case XML:
      List<Source> schemaSources = schemaProvider.getXmlSchemas();
      retval = new XmlSchemaContentValidator(schemaSources).validate(target);
      break;
    case YAML:
      JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
      assert json != null;
      retval = new JsonSchemaContentValidator(schemaProvider.getJsonSchema())
          .validate(json, ObjectUtils.notNull(target.toUri()));
      break;
    default:
      throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
    }

    if (retval.isPassing()) {
      IValidationResult constraintValidationResult = validateWithConstraints(target);
      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
   * @return the validation results
   * @throws IOException
   *           if an error occurred while loading the document
   */
  default IValidationResult validateWithConstraints(@NonNull Path target) throws IOException {
    IBoundLoader loader = newBoundLoader();
    loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);

    DynamicContext dynamicContext = StaticContext.newInstance().newDynamicContext();
    dynamicContext.setDocumentLoader(loader);
    IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);

    return validate(nodeItem);
  }

  interface IValidationSchemaProvider {
    /**
     * Get a JSON schema to use for content validation.
     *
     * @return the JSON schema
     * @throws IOException
     *           if an error occurred while loading the schema
     */
    @NonNull
    JSONObject getJsonSchema() throws IOException;

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

}