AbstractModuleLoaderStrategy.java

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

package gov.nist.secauto.metaschema.databind;

import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.IBindingContext.IBindingMatcher;
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.annotations.MetaschemaModule;
import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
import gov.nist.secauto.metaschema.databind.model.impl.DefinitionAssembly;
import gov.nist.secauto.metaschema.databind.model.impl.DefinitionField;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

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

/**
 * Provides basic module loading capabilities.
 *
 * @since 2.0.0
 */
public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
  @SuppressWarnings("PMD.UseConcurrentHashMap")
  @NonNull
  private final Map<IBoundDefinitionModelAssembly, IBindingMatcher> bindingMatchers = new HashMap<>();
  @NonNull
  private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
  @SuppressWarnings("PMD.UseConcurrentHashMap")
  @NonNull
  private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();

  @NonNull
  private final Lock modulesLock = new ReentrantLock();
  @SuppressWarnings("PMD.UseConcurrentHashMap")
  @NonNull
  private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
      = new HashMap<>();
  @NonNull
  private final Lock definitionsLock = new ReentrantLock();

  @Override
  public IBoundModule loadModule(
      @NonNull Class<? extends IBoundModule> clazz,
      @NonNull IBindingContext bindingContext) {
    return lookupInstance(clazz, bindingContext);
  }

  @Override
  public IBoundModule registerModule(
      IModule module,
      IBindingContext bindingContext) {
    modulesLock.lock();
    try {
      return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
        assert key != null;

        IBoundModule boundModule;
        if (key instanceof IBoundModule) {
          boundModule = (IBoundModule) key;
        } else {
          Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
          boundModule = lookupInstance(moduleClass, bindingContext);
        }

        boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
          assert assembly != null;
          if (assembly.isRoot()) {
            // force the binding matchers to load
            registerBindingMatcher(assembly);
          }
        });

        return boundModule;
      }));
    } finally {
      modulesLock.unlock();
    }
  }

  @NonNull
  protected abstract Class<? extends IBoundModule> handleUnboundModule(@NonNull IModule key);

  /**
   * Get the Module instance for a given class annotated by the
   * {@link MetaschemaModule} annotation, instantiating it if needed.
   * <p>
   * Will also load any imported Metaschemas.
   *
   *
   * @param moduleClass
   *          the Module class
   * @param bindingContext
   *          the Metaschema binding context used to lookup binding information
   * @return the new Module instance
   */
  @NonNull
  protected IBoundModule lookupInstance(
      @NonNull Class<? extends IBoundModule> moduleClass,
      @NonNull IBindingContext bindingContext) {
    IBoundModule retval;
    modulesLock.lock();
    try {
      retval = modulesByClass.get(moduleClass);
      if (retval == null) {
        if (!moduleClass.isAnnotationPresent(MetaschemaModule.class)) {
          throw new IllegalStateException(String.format("The class '%s' is missing the '%s' annotation",
              moduleClass.getCanonicalName(), MetaschemaModule.class.getCanonicalName()));
        }

        retval = IBoundModule.newInstance(moduleClass, bindingContext, getImportedModules(moduleClass, bindingContext));
        modulesByClass.put(moduleClass, retval);
      }
    } finally {
      modulesLock.unlock();
    }
    return retval;
  }

  @NonNull
  protected IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
    IBindingMatcher retval;
    modulesLock.lock();
    try {
      retval = bindingMatchers.get(definition);
      if (retval == null) {
        if (!definition.isRoot()) {
          throw new IllegalArgumentException(
              String.format("The provided definition '%s' is not a root assembly.",
                  definition.getBoundClass().getName()));
        }

        retval = IBindingMatcher.of(definition);
        bindingMatchers.put(definition, retval);
      }
    } finally {
      modulesLock.unlock();
    }
    return retval;
  }

  @Override
  public final List<IBindingMatcher> getBindingMatchers() {
    modulesLock.lock();
    try {
      // make a defensive copy
      return new ArrayList<>(bindingMatchers.values());
    } finally {
      modulesLock.unlock();
    }
  }

  @NonNull
  private List<IBoundModule> getImportedModules(
      @NonNull Class<? extends IBoundModule> moduleClass,
      @NonNull IBindingContext bindingContext) {
    MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class);

    return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports())
        .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext))
        .collect(Collectors.toUnmodifiableList()));
  }

  @Override
  public IBoundDefinitionModelComplex getBoundDefinitionForClass(
      @NonNull Class<? extends IBoundObject> clazz,
      @NonNull IBindingContext bindingContext) {

    IBoundDefinitionModelComplex retval;
    definitionsLock.lock();
    try {
      retval = definitionsByClass.get(clazz);
      if (retval == null) {
        retval = newBoundDefinition(clazz, bindingContext);
        definitionsByClass.put(clazz, retval);
      }

      // // force loading of metaschema information to apply constraints
      // IModule module = retval.getContainingModule();
      // registerModule(module, bindingContext);
      return retval;
    } finally {
      definitionsLock.unlock();
    }
  }

  @NonNull
  private IBoundDefinitionModelComplex newBoundDefinition(
      @NonNull Class<? extends IBoundObject> clazz,
      @NonNull IBindingContext bindingContext) {
    IBoundDefinitionModelComplex retval;
    if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) {
      MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class);
      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
      IBoundModule module = loadModule(moduleClass, bindingContext);
      retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext);
    } else if (clazz.isAnnotationPresent(MetaschemaField.class)) {
      MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class);
      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
      IBoundModule module = loadModule(moduleClass, bindingContext);
      retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext);
    } else {
      throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
          clazz.getName()));
    }
    return retval;
  }
}