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