001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind;
007
008import gov.nist.secauto.metaschema.core.model.IBoundObject;
009import gov.nist.secauto.metaschema.core.model.IModule;
010import gov.nist.secauto.metaschema.core.util.ObjectUtils;
011import gov.nist.secauto.metaschema.databind.IBindingContext.IBindingMatcher;
012import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
013import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
014import gov.nist.secauto.metaschema.databind.model.IBoundModule;
015import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
016import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
017import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaModule;
018import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
019import gov.nist.secauto.metaschema.databind.model.impl.DefinitionAssembly;
020import gov.nist.secauto.metaschema.databind.model.impl.DefinitionField;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030import java.util.stream.Collectors;
031
032import edu.umd.cs.findbugs.annotations.NonNull;
033
034/**
035 * Provides basic module loading capabilities.
036 *
037 * @since 2.0.0
038 */
039public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
040  @SuppressWarnings("PMD.UseConcurrentHashMap")
041  @NonNull
042  private final Map<IBoundDefinitionModelAssembly, IBindingMatcher> bindingMatchers = new HashMap<>();
043  @NonNull
044  private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
045  @SuppressWarnings("PMD.UseConcurrentHashMap")
046  @NonNull
047  private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();
048
049  @NonNull
050  private final Lock modulesLock = new ReentrantLock();
051  @SuppressWarnings("PMD.UseConcurrentHashMap")
052  @NonNull
053  private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
054      = new HashMap<>();
055  @NonNull
056  private final Lock definitionsLock = new ReentrantLock();
057
058  @Override
059  public IBoundModule loadModule(
060      @NonNull Class<? extends IBoundModule> clazz,
061      @NonNull IBindingContext bindingContext) {
062    return lookupInstance(clazz, bindingContext);
063  }
064
065  @Override
066  public IBoundModule registerModule(
067      IModule module,
068      IBindingContext bindingContext) {
069    modulesLock.lock();
070    try {
071      return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
072        assert key != null;
073
074        IBoundModule boundModule;
075        if (key instanceof IBoundModule) {
076          boundModule = (IBoundModule) key;
077        } else {
078          Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
079          boundModule = lookupInstance(moduleClass, bindingContext);
080        }
081
082        boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
083          assert assembly != null;
084          if (assembly.isRoot()) {
085            // force the binding matchers to load
086            registerBindingMatcher(assembly);
087          }
088        });
089
090        return boundModule;
091      }));
092    } finally {
093      modulesLock.unlock();
094    }
095  }
096
097  @NonNull
098  protected abstract Class<? extends IBoundModule> handleUnboundModule(@NonNull IModule key);
099
100  /**
101   * Get the Module instance for a given class annotated by the
102   * {@link MetaschemaModule} annotation, instantiating it if needed.
103   * <p>
104   * Will also load any imported Metaschemas.
105   *
106   *
107   * @param moduleClass
108   *          the Module class
109   * @param bindingContext
110   *          the Metaschema binding context used to lookup binding information
111   * @return the new Module instance
112   */
113  @NonNull
114  protected IBoundModule lookupInstance(
115      @NonNull Class<? extends IBoundModule> moduleClass,
116      @NonNull IBindingContext bindingContext) {
117    IBoundModule retval;
118    modulesLock.lock();
119    try {
120      retval = modulesByClass.get(moduleClass);
121      if (retval == null) {
122        if (!moduleClass.isAnnotationPresent(MetaschemaModule.class)) {
123          throw new IllegalStateException(String.format("The class '%s' is missing the '%s' annotation",
124              moduleClass.getCanonicalName(), MetaschemaModule.class.getCanonicalName()));
125        }
126
127        retval = IBoundModule.newInstance(moduleClass, bindingContext, getImportedModules(moduleClass, bindingContext));
128        modulesByClass.put(moduleClass, retval);
129      }
130    } finally {
131      modulesLock.unlock();
132    }
133    return retval;
134  }
135
136  @NonNull
137  protected IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
138    IBindingMatcher retval;
139    modulesLock.lock();
140    try {
141      retval = bindingMatchers.get(definition);
142      if (retval == null) {
143        if (!definition.isRoot()) {
144          throw new IllegalArgumentException(
145              String.format("The provided definition '%s' is not a root assembly.",
146                  definition.getBoundClass().getName()));
147        }
148
149        retval = IBindingMatcher.of(definition);
150        bindingMatchers.put(definition, retval);
151      }
152    } finally {
153      modulesLock.unlock();
154    }
155    return retval;
156  }
157
158  @Override
159  public final List<IBindingMatcher> getBindingMatchers() {
160    modulesLock.lock();
161    try {
162      // make a defensive copy
163      return new ArrayList<>(bindingMatchers.values());
164    } finally {
165      modulesLock.unlock();
166    }
167  }
168
169  @NonNull
170  private List<IBoundModule> getImportedModules(
171      @NonNull Class<? extends IBoundModule> moduleClass,
172      @NonNull IBindingContext bindingContext) {
173    MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class);
174
175    return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports())
176        .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext))
177        .collect(Collectors.toUnmodifiableList()));
178  }
179
180  @Override
181  public IBoundDefinitionModelComplex getBoundDefinitionForClass(
182      @NonNull Class<? extends IBoundObject> clazz,
183      @NonNull IBindingContext bindingContext) {
184
185    IBoundDefinitionModelComplex retval;
186    definitionsLock.lock();
187    try {
188      retval = definitionsByClass.get(clazz);
189      if (retval == null) {
190        retval = newBoundDefinition(clazz, bindingContext);
191        definitionsByClass.put(clazz, retval);
192      }
193
194      // // force loading of metaschema information to apply constraints
195      // IModule module = retval.getContainingModule();
196      // registerModule(module, bindingContext);
197      return retval;
198    } finally {
199      definitionsLock.unlock();
200    }
201  }
202
203  @NonNull
204  private IBoundDefinitionModelComplex newBoundDefinition(
205      @NonNull Class<? extends IBoundObject> clazz,
206      @NonNull IBindingContext bindingContext) {
207    IBoundDefinitionModelComplex retval;
208    if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) {
209      MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class);
210      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
211      IBoundModule module = loadModule(moduleClass, bindingContext);
212      retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext);
213    } else if (clazz.isAnnotationPresent(MetaschemaField.class)) {
214      MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class);
215      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
216      IBoundModule module = loadModule(moduleClass, bindingContext);
217      retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext);
218    } else {
219      throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
220          clazz.getName()));
221    }
222    return retval;
223  }
224}