1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.util.ArrayList;
12  import java.util.Arrays;
13  import java.util.HashMap;
14  import java.util.LinkedHashMap;
15  import java.util.List;
16  import java.util.Map;
17  import java.util.concurrent.ConcurrentHashMap;
18  import java.util.concurrent.locks.Lock;
19  import java.util.concurrent.locks.ReentrantLock;
20  import java.util.stream.Collectors;
21  
22  import dev.metaschema.core.model.IBoundObject;
23  import dev.metaschema.core.model.IModule;
24  import dev.metaschema.core.model.MetaschemaException;
25  import dev.metaschema.core.model.constraint.DefaultConstraintValidator;
26  import dev.metaschema.core.qname.IEnhancedQName;
27  import dev.metaschema.core.util.ExceptionUtils;
28  import dev.metaschema.core.util.ExceptionUtils.WrappedException;
29  import dev.metaschema.core.util.ObjectUtils;
30  import dev.metaschema.databind.IBindingContext.IBindingMatcher;
31  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
32  import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
33  import dev.metaschema.databind.model.IBoundModule;
34  import dev.metaschema.databind.model.annotations.MetaschemaAssembly;
35  import dev.metaschema.databind.model.annotations.MetaschemaField;
36  import dev.metaschema.databind.model.annotations.MetaschemaModule;
37  import dev.metaschema.databind.model.annotations.ModelUtil;
38  import dev.metaschema.databind.model.impl.DefinitionAssembly;
39  import dev.metaschema.databind.model.impl.DefinitionField;
40  import dev.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  
43  /**
44   * Provides basic module loading capabilities.
45   *
46   * @since 2.0.0
47   */
48  public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
49    private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);
50  
51    @SuppressWarnings("PMD.UseConcurrentHashMap")
52    @NonNull
53    private final Map<IEnhancedQName, IBindingMatcher> bindingMatchers = new LinkedHashMap<>();
54    @NonNull
55    private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
56    @SuppressWarnings("PMD.UseConcurrentHashMap")
57    @NonNull
58    private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();
59  
60    @NonNull
61    private final Lock modulesLock = new ReentrantLock();
62    @SuppressWarnings("PMD.UseConcurrentHashMap")
63    @NonNull
64    private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
65        = new HashMap<>();
66    @NonNull
67    private final Lock definitionsLock = new ReentrantLock();
68  
69    @Override
70    public IBoundModule loadModule(
71        @NonNull Class<? extends IBoundModule> clazz,
72        @NonNull IBindingContext bindingContext) {
73      return lookupInstance(clazz, bindingContext);
74    }
75  
76    @Override
77    @SuppressWarnings("PMD.ExceptionAsFlowControl")
78    public IBoundModule registerModule(
79        IModule module,
80        IBindingContext bindingContext) throws MetaschemaException {
81      modulesLock.lock();
82      try {
83        return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
84          assert key != null;
85  
86          IBoundModule boundModule;
87          if (key instanceof IBoundModule) {
88            boundModule = (IBoundModule) key;
89          } else {
90            try {
91              Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
92              boundModule = lookupInstance(moduleClass, bindingContext);
93            } catch (MetaschemaException ex) {
94              throw ExceptionUtils.wrap(ex);
95            }
96          }
97  
98          boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
99            assert assembly != null;
100           if (assembly.isRoot()) {
101             // force the binding matchers to load
102             registerBindingMatcher(assembly);
103           }
104         });
105 
106         return boundModule;
107       }));
108     } catch (WrappedException ex) {
109       throw ExceptionUtils.unwrap(ex, MetaschemaException.class);
110     } finally {
111       modulesLock.unlock();
112     }
113   }
114 
115   /**
116    * Handle a module that is not already bound to a Java class.
117    * <p>
118    * This method is called when a module is encountered that does not implement
119    * {@link IBoundModule}. Implementations must determine the appropriate bound
120    * module class to use for the given module.
121    *
122    * @param key
123    *          the unbound module to handle
124    * @return the class of the bound module to use
125    * @throws MetaschemaException
126    *           if an error occurs while determining the bound module class
127    */
128   @NonNull
129   protected abstract Class<? extends IBoundModule> handleUnboundModule(@NonNull IModule key) throws MetaschemaException;
130 
131   /**
132    * Get the Module instance for a given class annotated by the
133    * {@link MetaschemaModule} annotation, instantiating it if needed.
134    * <p>
135    * Will also load any imported Metaschemas.
136    *
137    *
138    * @param moduleClass
139    *          the Module class
140    * @param bindingContext
141    *          the Metaschema binding context used to lookup binding information
142    * @return the new Module instance
143    */
144   @NonNull
145   protected IBoundModule lookupInstance(
146       @NonNull Class<? extends IBoundModule> moduleClass,
147       @NonNull IBindingContext bindingContext) {
148     IBoundModule retval;
149     modulesLock.lock();
150     try {
151       retval = modulesByClass.get(moduleClass);
152       if (retval == null) {
153         if (!moduleClass.isAnnotationPresent(MetaschemaModule.class)) {
154           throw new IllegalStateException(String.format("The class '%s' is missing the '%s' annotation",
155               moduleClass.getCanonicalName(), MetaschemaModule.class.getCanonicalName()));
156         }
157 
158         retval = IBoundModule.newInstance(moduleClass, bindingContext, getImportedModules(moduleClass, bindingContext));
159         modulesByClass.put(moduleClass, retval);
160       }
161     } finally {
162       modulesLock.unlock();
163     }
164     return retval;
165   }
166 
167   /**
168    * Register a binding matcher for a root assembly definition.
169    * <p>
170    * The binding matcher is used to match document root elements to their
171    * corresponding assembly definitions during deserialization.
172    *
173    * @param definition
174    *          the root assembly definition to register a matcher for
175    * @return the registered binding matcher
176    * @throws IllegalArgumentException
177    *           if the provided definition is not a root assembly
178    */
179   @NonNull
180   protected IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
181     IBindingMatcher retval;
182     modulesLock.lock();
183     try {
184       if (!definition.isRoot()) {
185         throw new IllegalArgumentException(
186             String.format("The provided definition '%s' is not a root assembly.",
187                 definition.getBoundClass().getName()));
188       }
189       IEnhancedQName qname = definition.getRootQName();
190       retval = IBindingMatcher.of(definition);
191       // always replace the existing matcher to ensure the last loaded module wins
192       IBindingMatcher old = bindingMatchers.put(qname, retval);
193       // FIXME: find existing causes of this in unit tests
194       if (old != null && !(definition.getContainingModule() instanceof MetaschemaModelModule)) {
195         LOGGER.atDebug().log("Replacing matcher for QName: {}", qname);
196       }
197 
198       // retval = bindingMatchers.get(definition);
199       // if (retval == null) {
200       // if (!definition.isRoot()) {
201       // throw new IllegalArgumentException(
202       // String.format("The provided definition '%s' is not a root assembly.",
203       // definition.getBoundClass().getName()));
204       // }
205       //
206       // retval = IBindingMatcher.of(definition);
207       // bindingMatchers.put(definition, retval);
208       // }
209     } finally {
210       modulesLock.unlock();
211     }
212     return retval;
213   }
214 
215   @Override
216   public final List<IBindingMatcher> getBindingMatchers() {
217     modulesLock.lock();
218     try {
219       // make a defensive copy
220       return new ArrayList<>(bindingMatchers.values());
221     } finally {
222       modulesLock.unlock();
223     }
224   }
225 
226   @NonNull
227   private List<IBoundModule> getImportedModules(
228       @NonNull Class<? extends IBoundModule> moduleClass,
229       @NonNull IBindingContext bindingContext) {
230     MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class);
231 
232     return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports())
233         .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext))
234         .collect(Collectors.toUnmodifiableList()));
235   }
236 
237   @Override
238   public IBoundDefinitionModelComplex getBoundDefinitionForClass(
239       @NonNull Class<? extends IBoundObject> clazz,
240       @NonNull IBindingContext bindingContext) {
241 
242     IBoundDefinitionModelComplex retval;
243     definitionsLock.lock();
244     try {
245       retval = definitionsByClass.get(clazz);
246       if (retval == null) {
247         retval = newBoundDefinition(clazz, bindingContext);
248         definitionsByClass.put(clazz, retval);
249       }
250 
251       // // force loading of metaschema information to apply constraints
252       // IModule module = retval.getContainingModule();
253       // registerModule(module, bindingContext);
254       return retval;
255     } finally {
256       definitionsLock.unlock();
257     }
258   }
259 
260   @NonNull
261   private IBoundDefinitionModelComplex newBoundDefinition(
262       @NonNull Class<? extends IBoundObject> clazz,
263       @NonNull IBindingContext bindingContext) {
264     IBoundDefinitionModelComplex retval;
265     if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) {
266       MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class);
267       Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
268       IBoundModule module = loadModule(moduleClass, bindingContext);
269       retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext);
270     } else if (clazz.isAnnotationPresent(MetaschemaField.class)) {
271       MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class);
272       Class<? extends IBoundModule> moduleClass = annotation.moduleClass();
273       IBoundModule module = loadModule(moduleClass, bindingContext);
274       retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext);
275     } else {
276       throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
277           clazz.getName()));
278     }
279     return retval;
280   }
281 }