1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind;
7   
8   import gov.nist.secauto.metaschema.core.model.IBoundObject;
9   import gov.nist.secauto.metaschema.core.model.IModule;
10  import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
11  import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
12  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
13  import gov.nist.secauto.metaschema.databind.IBindingContext.IBindingMatcher;
14  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
15  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
16  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
17  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
18  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
19  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaModule;
20  import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
21  import gov.nist.secauto.metaschema.databind.model.impl.DefinitionAssembly;
22  import gov.nist.secauto.metaschema.databind.model.impl.DefinitionField;
23  import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
24  
25  import org.apache.logging.log4j.LogManager;
26  import org.apache.logging.log4j.Logger;
27  
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.HashMap;
31  import java.util.LinkedHashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.concurrent.ConcurrentHashMap;
35  import java.util.concurrent.locks.Lock;
36  import java.util.concurrent.locks.ReentrantLock;
37  import java.util.stream.Collectors;
38  
39  import edu.umd.cs.findbugs.annotations.NonNull;
40  
41  /**
42   * Provides basic module loading capabilities.
43   *
44   * @since 2.0.0
45   */
46  public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
47    private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);
48  
49    @SuppressWarnings("PMD.UseConcurrentHashMap")
50    @NonNull
51    private final Map<IEnhancedQName, IBindingMatcher> bindingMatchers = new LinkedHashMap<>();
52    @NonNull
53    private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
54    @SuppressWarnings("PMD.UseConcurrentHashMap")
55    @NonNull
56    private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();
57  
58    @NonNull
59    private final Lock modulesLock = new ReentrantLock();
60    @SuppressWarnings("PMD.UseConcurrentHashMap")
61    @NonNull
62    private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
63        = new HashMap<>();
64    @NonNull
65    private final Lock definitionsLock = new ReentrantLock();
66  
67    @Override
68    public IBoundModule loadModule(
69        @NonNull Class<? extends IBoundModule> clazz,
70        @NonNull IBindingContext bindingContext) {
71      return lookupInstance(clazz, bindingContext);
72    }
73  
74    @Override
75    public IBoundModule registerModule(
76        IModule module,
77        IBindingContext bindingContext) {
78      modulesLock.lock();
79      try {
80        return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
81          assert key != null;
82  
83          IBoundModule boundModule;
84          if (key instanceof IBoundModule) {
85            boundModule = (IBoundModule) key;
86          } else {
87            Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
88            boundModule = lookupInstance(moduleClass, bindingContext);
89          }
90  
91          boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
92            assert assembly != null;
93            if (assembly.isRoot()) {
94              // force the binding matchers to load
95              registerBindingMatcher(assembly);
96            }
97          });
98  
99          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 }