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         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 }