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}