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.util.ObjectUtils;
11  import gov.nist.secauto.metaschema.databind.IBindingContext.IBindingMatcher;
12  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
13  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
14  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
15  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
16  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
17  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaModule;
18  import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil;
19  import gov.nist.secauto.metaschema.databind.model.impl.DefinitionAssembly;
20  import gov.nist.secauto.metaschema.databind.model.impl.DefinitionField;
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.locks.Lock;
29  import java.util.concurrent.locks.ReentrantLock;
30  import java.util.stream.Collectors;
31  
32  import edu.umd.cs.findbugs.annotations.NonNull;
33  
34  /**
35   * Provides basic module loading capabilities.
36   *
37   * @since 2.0.0
38   */
39  public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
40    @SuppressWarnings("PMD.UseConcurrentHashMap")
41    @NonNull
42    private final Map<IBoundDefinitionModelAssembly, IBindingMatcher> bindingMatchers = new HashMap<>();
43    @NonNull
44    private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
45    @SuppressWarnings("PMD.UseConcurrentHashMap")
46    @NonNull
47    private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();
48  
49    @NonNull
50    private final Lock modulesLock = new ReentrantLock();
51    @SuppressWarnings("PMD.UseConcurrentHashMap")
52    @NonNull
53    private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
54        = new HashMap<>();
55    @NonNull
56    private final Lock definitionsLock = new ReentrantLock();
57  
58    @Override
59    public IBoundModule loadModule(
60        @NonNull Class<? extends IBoundModule> clazz,
61        @NonNull IBindingContext bindingContext) {
62      return lookupInstance(clazz, bindingContext);
63    }
64  
65    @Override
66    public IBoundModule registerModule(
67        IModule module,
68        IBindingContext bindingContext) {
69      modulesLock.lock();
70      try {
71        return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
72          assert key != null;
73  
74          IBoundModule boundModule;
75          if (key instanceof IBoundModule) {
76            boundModule = (IBoundModule) key;
77          } else {
78            Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
79            boundModule = lookupInstance(moduleClass, bindingContext);
80          }
81  
82          boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
83            assert assembly != null;
84            if (assembly.isRoot()) {
85              // force the binding matchers to load
86              registerBindingMatcher(assembly);
87            }
88          });
89  
90          return boundModule;
91        }));
92      } finally {
93        modulesLock.unlock();
94      }
95    }
96  
97    @NonNull
98    protected abstract Class<? extends IBoundModule> handleUnboundModule(@NonNull IModule key);
99  
100   /**
101    * Get the Module instance for a given class annotated by the
102    * {@link MetaschemaModule} annotation, instantiating it if needed.
103    * <p>
104    * Will also load any imported Metaschemas.
105    *
106    *
107    * @param moduleClass
108    *          the Module class
109    * @param bindingContext
110    *          the Metaschema binding context used to lookup binding information
111    * @return the new Module instance
112    */
113   @NonNull
114   protected IBoundModule lookupInstance(
115       @NonNull Class<? extends IBoundModule> moduleClass,
116       @NonNull IBindingContext bindingContext) {
117     IBoundModule retval;
118     modulesLock.lock();
119     try {
120       retval = modulesByClass.get(moduleClass);
121       if (retval == null) {
122         if (!moduleClass.isAnnotationPresent(MetaschemaModule.class)) {
123           throw new IllegalStateException(String.format("The class '%s' is missing the '%s' annotation",
124               moduleClass.getCanonicalName(), MetaschemaModule.class.getCanonicalName()));
125         }
126 
127         retval = IBoundModule.newInstance(moduleClass, bindingContext, getImportedModules(moduleClass, bindingContext));
128         modulesByClass.put(moduleClass, retval);
129       }
130     } finally {
131       modulesLock.unlock();
132     }
133     return retval;
134   }
135 
136   @NonNull
137   protected IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
138     IBindingMatcher retval;
139     modulesLock.lock();
140     try {
141       retval = bindingMatchers.get(definition);
142       if (retval == null) {
143         if (!definition.isRoot()) {
144           throw new IllegalArgumentException(
145               String.format("The provided definition '%s' is not a root assembly.",
146                   definition.getBoundClass().getName()));
147         }
148 
149         retval = IBindingMatcher.of(definition);
150         bindingMatchers.put(definition, retval);
151       }
152     } finally {
153       modulesLock.unlock();
154     }
155     return retval;
156   }
157 
158   @Override
159   public final List<IBindingMatcher> getBindingMatchers() {
160     modulesLock.lock();
161     try {
162       // make a defensive copy
163       return new ArrayList<>(bindingMatchers.values());
164     } finally {
165       modulesLock.unlock();
166     }
167   }
168 
169   @NonNull
170   private List<IBoundModule> getImportedModules(
171       @NonNull Class<? extends IBoundModule> moduleClass,
172       @NonNull IBindingContext bindingContext) {
173     MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class);
174 
175     return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports())
176         .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext))
177         .collect(Collectors.toUnmodifiableList()));
178   }
179 
180   @Override
181   public IBoundDefinitionModelComplex getBoundDefinitionForClass(
182       @NonNull Class<? extends IBoundObject> clazz,
183       @NonNull IBindingContext bindingContext) {
184 
185     IBoundDefinitionModelComplex retval;
186     definitionsLock.lock();
187     try {
188       retval = definitionsByClass.get(clazz);
189       if (retval == null) {
190         retval = newBoundDefinition(clazz, bindingContext);
191         definitionsByClass.put(clazz, retval);
192       }
193 
194       // // force loading of metaschema information to apply constraints
195       // IModule module = retval.getContainingModule();
196       // registerModule(module, bindingContext);
197       return retval;
198     } finally {
199       definitionsLock.unlock();
200     }
201   }
202 
203   @NonNull
204   private IBoundDefinitionModelComplex newBoundDefinition(
205       @NonNull Class<? extends IBoundObject> clazz,
206       @NonNull IBindingContext bindingContext) {
207     IBoundDefinitionModelComplex retval;
208     if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) {
209       MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class);
210       Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
211       IBoundModule module = loadModule(moduleClass, bindingContext);
212       retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext);
213     } else if (clazz.isAnnotationPresent(MetaschemaField.class)) {
214       MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class);
215       Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
216       IBoundModule module = loadModule(moduleClass, bindingContext);
217       retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext);
218     } else {
219       throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
220           clazz.getName()));
221     }
222     return retval;
223   }
224 }