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