001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.databind; 007 008import gov.nist.secauto.metaschema.core.model.IBoundObject; 009import gov.nist.secauto.metaschema.core.model.IModule; 010import gov.nist.secauto.metaschema.core.model.MetaschemaException; 011import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator; 012import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; 013import gov.nist.secauto.metaschema.core.util.ExceptionUtils; 014import gov.nist.secauto.metaschema.core.util.ExceptionUtils.WrappedException; 015import gov.nist.secauto.metaschema.core.util.ObjectUtils; 016import gov.nist.secauto.metaschema.databind.IBindingContext.IBindingMatcher; 017import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly; 018import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex; 019import gov.nist.secauto.metaschema.databind.model.IBoundModule; 020import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; 021import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField; 022import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaModule; 023import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil; 024import gov.nist.secauto.metaschema.databind.model.impl.DefinitionAssembly; 025import gov.nist.secauto.metaschema.databind.model.impl.DefinitionField; 026import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule; 027 028import org.apache.logging.log4j.LogManager; 029import org.apache.logging.log4j.Logger; 030 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.HashMap; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.concurrent.ConcurrentHashMap; 038import java.util.concurrent.locks.Lock; 039import java.util.concurrent.locks.ReentrantLock; 040import java.util.stream.Collectors; 041 042import edu.umd.cs.findbugs.annotations.NonNull; 043 044/** 045 * Provides basic module loading capabilities. 046 * 047 * @since 2.0.0 048 */ 049public abstract class AbstractModuleLoaderStrategy implements IBindingContext.IModuleLoaderStrategy { 050 private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class); 051 052 @SuppressWarnings("PMD.UseConcurrentHashMap") 053 @NonNull 054 private final Map<IEnhancedQName, IBindingMatcher> bindingMatchers = new LinkedHashMap<>(); 055 @NonNull 056 private final Map<IModule, IBoundModule> moduleToBoundModuleMap = new ConcurrentHashMap<>(); 057 @SuppressWarnings("PMD.UseConcurrentHashMap") 058 @NonNull 059 private final Map<Class<? extends IBoundModule>, IBoundModule> modulesByClass = new HashMap<>(); 060 061 @NonNull 062 private final Lock modulesLock = new ReentrantLock(); 063 @SuppressWarnings("PMD.UseConcurrentHashMap") 064 @NonNull 065 private final Map<Class<? extends IBoundObject>, IBoundDefinitionModelComplex> definitionsByClass 066 = new HashMap<>(); 067 @NonNull 068 private final Lock definitionsLock = new ReentrantLock(); 069 070 @Override 071 public IBoundModule loadModule( 072 @NonNull Class<? extends IBoundModule> clazz, 073 @NonNull IBindingContext bindingContext) { 074 return lookupInstance(clazz, bindingContext); 075 } 076 077 @Override 078 @SuppressWarnings("PMD.ExceptionAsFlowControl") 079 public IBoundModule registerModule( 080 IModule module, 081 IBindingContext bindingContext) throws MetaschemaException { 082 modulesLock.lock(); 083 try { 084 return ObjectUtils.notNull(moduleToBoundModuleMap.computeIfAbsent(module, key -> { 085 assert key != null; 086 087 IBoundModule boundModule; 088 if (key instanceof IBoundModule) { 089 boundModule = (IBoundModule) key; 090 } else { 091 try { 092 Class<? extends IBoundModule> moduleClass = handleUnboundModule(key); 093 boundModule = lookupInstance(moduleClass, bindingContext); 094 } catch (MetaschemaException ex) { 095 throw ExceptionUtils.wrap(ex); 096 } 097 } 098 099 boundModule.getExportedAssemblyDefinitions().forEach(assembly -> { 100 assert assembly != null; 101 if (assembly.isRoot()) { 102 // force the binding matchers to load 103 registerBindingMatcher(assembly); 104 } 105 }); 106 107 return boundModule; 108 })); 109 } catch (WrappedException ex) { 110 throw ExceptionUtils.unwrap(ex, MetaschemaException.class); 111 } finally { 112 modulesLock.unlock(); 113 } 114 } 115 116 @NonNull 117 protected abstract Class<? extends IBoundModule> handleUnboundModule(@NonNull IModule key) throws MetaschemaException; 118 119 /** 120 * Get the Module instance for a given class annotated by the 121 * {@link MetaschemaModule} annotation, instantiating it if needed. 122 * <p> 123 * Will also load any imported Metaschemas. 124 * 125 * 126 * @param moduleClass 127 * the Module class 128 * @param bindingContext 129 * the Metaschema binding context used to lookup binding information 130 * @return the new Module instance 131 */ 132 @NonNull 133 protected IBoundModule lookupInstance( 134 @NonNull Class<? extends IBoundModule> moduleClass, 135 @NonNull IBindingContext bindingContext) { 136 IBoundModule retval; 137 modulesLock.lock(); 138 try { 139 retval = modulesByClass.get(moduleClass); 140 if (retval == null) { 141 if (!moduleClass.isAnnotationPresent(MetaschemaModule.class)) { 142 throw new IllegalStateException(String.format("The class '%s' is missing the '%s' annotation", 143 moduleClass.getCanonicalName(), MetaschemaModule.class.getCanonicalName())); 144 } 145 146 retval = IBoundModule.newInstance(moduleClass, bindingContext, getImportedModules(moduleClass, bindingContext)); 147 modulesByClass.put(moduleClass, retval); 148 } 149 } finally { 150 modulesLock.unlock(); 151 } 152 return retval; 153 } 154 155 @NonNull 156 protected IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) { 157 IBindingMatcher retval; 158 modulesLock.lock(); 159 try { 160 if (!definition.isRoot()) { 161 throw new IllegalArgumentException( 162 String.format("The provided definition '%s' is not a root assembly.", 163 definition.getBoundClass().getName())); 164 } 165 IEnhancedQName qname = definition.getRootQName(); 166 retval = IBindingMatcher.of(definition); 167 // always replace the existing matcher to ensure the last loaded module wins 168 IBindingMatcher old = bindingMatchers.put(qname, retval); 169 if (old != null && !(definition.getContainingModule() instanceof MetaschemaModelModule)) { 170 // FIXME: find existing causes of this in unit tests 171 if (LOGGER.isDebugEnabled()) { 172 LOGGER.atDebug().log("Replacing matcher for QName: {}", qname); 173 } 174 } 175 176 // retval = bindingMatchers.get(definition); 177 // if (retval == null) { 178 // if (!definition.isRoot()) { 179 // throw new IllegalArgumentException( 180 // String.format("The provided definition '%s' is not a root assembly.", 181 // definition.getBoundClass().getName())); 182 // } 183 // 184 // retval = IBindingMatcher.of(definition); 185 // bindingMatchers.put(definition, retval); 186 // } 187 } finally { 188 modulesLock.unlock(); 189 } 190 return retval; 191 } 192 193 @Override 194 public final List<IBindingMatcher> getBindingMatchers() { 195 modulesLock.lock(); 196 try { 197 // make a defensive copy 198 return new ArrayList<>(bindingMatchers.values()); 199 } finally { 200 modulesLock.unlock(); 201 } 202 } 203 204 @NonNull 205 private List<IBoundModule> getImportedModules( 206 @NonNull Class<? extends IBoundModule> moduleClass, 207 @NonNull IBindingContext bindingContext) { 208 MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class); 209 210 return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports()) 211 .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext)) 212 .collect(Collectors.toUnmodifiableList())); 213 } 214 215 @Override 216 public IBoundDefinitionModelComplex getBoundDefinitionForClass( 217 @NonNull Class<? extends IBoundObject> clazz, 218 @NonNull IBindingContext bindingContext) { 219 220 IBoundDefinitionModelComplex retval; 221 definitionsLock.lock(); 222 try { 223 retval = definitionsByClass.get(clazz); 224 if (retval == null) { 225 retval = newBoundDefinition(clazz, bindingContext); 226 definitionsByClass.put(clazz, retval); 227 } 228 229 // // force loading of metaschema information to apply constraints 230 // IModule module = retval.getContainingModule(); 231 // registerModule(module, bindingContext); 232 return retval; 233 } finally { 234 definitionsLock.unlock(); 235 } 236 } 237 238 @NonNull 239 private IBoundDefinitionModelComplex newBoundDefinition( 240 @NonNull Class<? extends IBoundObject> clazz, 241 @NonNull IBindingContext bindingContext) { 242 IBoundDefinitionModelComplex retval; 243 if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) { 244 MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class); 245 Class<? extends IBoundModule> moduleClass = annotation.moduleClass(); 246 IBoundModule module = loadModule(moduleClass, bindingContext); 247 retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext); 248 } else if (clazz.isAnnotationPresent(MetaschemaField.class)) { 249 MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class); 250 Class<? extends IBoundModule> moduleClass = annotation.moduleClass(); 251 IBoundModule module = loadModule(moduleClass, bindingContext); 252 retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext); 253 } else { 254 throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.", 255 clazz.getName())); 256 } 257 return retval; 258 } 259}