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        LOGGER.atWarn().log("Replacing matcher for QName: {}", qname);
162      }
163
164      // retval = bindingMatchers.get(definition);
165      // if (retval == null) {
166      // if (!definition.isRoot()) {
167      // throw new IllegalArgumentException(
168      // String.format("The provided definition '%s' is not a root assembly.",
169      // definition.getBoundClass().getName()));
170      // }
171      //
172      // retval = IBindingMatcher.of(definition);
173      // bindingMatchers.put(definition, retval);
174      // }
175    } finally {
176      modulesLock.unlock();
177    }
178    return retval;
179  }
180
181  @Override
182  public final List<IBindingMatcher> getBindingMatchers() {
183    modulesLock.lock();
184    try {
185      // make a defensive copy
186      return new ArrayList<>(bindingMatchers.values());
187    } finally {
188      modulesLock.unlock();
189    }
190  }
191
192  @NonNull
193  private List<IBoundModule> getImportedModules(
194      @NonNull Class<? extends IBoundModule> moduleClass,
195      @NonNull IBindingContext bindingContext) {
196    MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class);
197
198    return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports())
199        .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext))
200        .collect(Collectors.toUnmodifiableList()));
201  }
202
203  @Override
204  public IBoundDefinitionModelComplex getBoundDefinitionForClass(
205      @NonNull Class<? extends IBoundObject> clazz,
206      @NonNull IBindingContext bindingContext) {
207
208    IBoundDefinitionModelComplex retval;
209    definitionsLock.lock();
210    try {
211      retval = definitionsByClass.get(clazz);
212      if (retval == null) {
213        retval = newBoundDefinition(clazz, bindingContext);
214        definitionsByClass.put(clazz, retval);
215      }
216
217      // // force loading of metaschema information to apply constraints
218      // IModule module = retval.getContainingModule();
219      // registerModule(module, bindingContext);
220      return retval;
221    } finally {
222      definitionsLock.unlock();
223    }
224  }
225
226  @NonNull
227  private IBoundDefinitionModelComplex newBoundDefinition(
228      @NonNull Class<? extends IBoundObject> clazz,
229      @NonNull IBindingContext bindingContext) {
230    IBoundDefinitionModelComplex retval;
231    if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) {
232      MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class);
233      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
234      IBoundModule module = loadModule(moduleClass, bindingContext);
235      retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext);
236    } else if (clazz.isAnnotationPresent(MetaschemaField.class)) {
237      MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class);
238      Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
239      IBoundModule module = loadModule(moduleClass, bindingContext);
240      retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext);
241    } else {
242      throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
243          clazz.getName()));
244    }
245    return retval;
246  }
247}