001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind;
007
008import org.apache.logging.log4j.LogManager;
009import org.apache.logging.log4j.Logger;
010
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.HashMap;
014import java.util.LinkedHashMap;
015import java.util.List;
016import java.util.Map;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.concurrent.locks.Lock;
019import java.util.concurrent.locks.ReentrantLock;
020import java.util.stream.Collectors;
021
022import dev.metaschema.core.model.IBoundObject;
023import dev.metaschema.core.model.IModule;
024import dev.metaschema.core.model.MetaschemaException;
025import dev.metaschema.core.model.constraint.DefaultConstraintValidator;
026import dev.metaschema.core.qname.IEnhancedQName;
027import dev.metaschema.core.util.ExceptionUtils;
028import dev.metaschema.core.util.ExceptionUtils.WrappedException;
029import dev.metaschema.core.util.ObjectUtils;
030import dev.metaschema.databind.IBindingContext.IBindingMatcher;
031import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
032import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
033import dev.metaschema.databind.model.IBoundModule;
034import dev.metaschema.databind.model.annotations.MetaschemaAssembly;
035import dev.metaschema.databind.model.annotations.MetaschemaField;
036import dev.metaschema.databind.model.annotations.MetaschemaModule;
037import dev.metaschema.databind.model.annotations.ModelUtil;
038import dev.metaschema.databind.model.impl.DefinitionAssembly;
039import dev.metaschema.databind.model.impl.DefinitionField;
040import dev.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
041import edu.umd.cs.findbugs.annotations.NonNull;
042
043/**
044 * Provides basic module loading capabilities.
045 *
046 * @since 2.0.0
047 */
048public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy {
049  private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);
050
051  @SuppressWarnings("PMD.UseConcurrentHashMap")
052  @NonNull
053  private final Map<IEnhancedQName, IBindingMatcher> bindingMatchers = new LinkedHashMap<>();
054  @NonNull
055  private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>();
056  @SuppressWarnings("PMD.UseConcurrentHashMap")
057  @NonNull
058  private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>();
059
060  @NonNull
061  private final Lock modulesLock = new ReentrantLock();
062  @SuppressWarnings("PMD.UseConcurrentHashMap")
063  @NonNull
064  private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass
065      = new HashMap<>();
066  @NonNull
067  private final Lock definitionsLock = new ReentrantLock();
068
069  @Override
070  public IBoundModule loadModule(
071      @NonNull Class<? extends IBoundModule> clazz,
072      @NonNull IBindingContext bindingContext) {
073    return lookupInstance(clazz, bindingContext);
074  }
075
076  @Override
077  @SuppressWarnings("PMD.ExceptionAsFlowControl")
078  public IBoundModule registerModule(
079      IModule module,
080      IBindingContext bindingContext) throws MetaschemaException {
081    modulesLock.lock();
082    try {
083      return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> {
084        assert key != null;
085
086        IBoundModule boundModule;
087        if (key instanceof IBoundModule) {
088          boundModule = (IBoundModule) key;
089        } else {
090          try {
091            Class<? extends IBoundModule> moduleClass = handleUnboundModule(key);
092            boundModule = lookupInstance(moduleClass, bindingContext);
093          } catch (MetaschemaException ex) {
094            throw ExceptionUtils.wrap(ex);
095          }
096        }
097
098        boundModule.getExportedAssemblyDefinitions().forEach(assembly -> {
099          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}