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