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