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 LOGGER.atWarn().log("Replacing matcher for QName: {}", qname); 162 } 163 164 // retval = bindingMatchers.get(definition); 165 // if (retval == null) { 166 // if (!definition.isRoot()) { 167 // throw new IllegalArgumentException( 168 // String.format("The provided definition '%s' is not a root assembly.", 169 // definition.getBoundClass().getName())); 170 // } 171 // 172 // retval = IBindingMatcher.of(definition); 173 // bindingMatchers.put(definition, retval); 174 // } 175 } finally { 176 modulesLock.unlock(); 177 } 178 return retval; 179 } 180 181 @Override 182 public final List<IBindingMatcher> getBindingMatchers() { 183 modulesLock.lock(); 184 try { 185 // make a defensive copy 186 return new ArrayList<>(bindingMatchers.values()); 187 } finally { 188 modulesLock.unlock(); 189 } 190 } 191 192 @NonNull 193 private List<IBoundModule> getImportedModules( 194 @NonNull Class<? extends IBoundModule> moduleClass, 195 @NonNull IBindingContext bindingContext) { 196 MetaschemaModule moduleAnnotation = moduleClass.getAnnotation(MetaschemaModule.class); 197 198 return ObjectUtils.notNull(Arrays.stream(moduleAnnotation.imports()) 199 .map(clazz -> lookupInstance(ObjectUtils.requireNonNull(clazz), bindingContext)) 200 .collect(Collectors.toUnmodifiableList())); 201 } 202 203 @Override 204 public IBoundDefinitionModelComplex getBoundDefinitionForClass( 205 @NonNull Class<? extends IBoundObject> clazz, 206 @NonNull IBindingContext bindingContext) { 207 208 IBoundDefinitionModelComplex retval; 209 definitionsLock.lock(); 210 try { 211 retval = definitionsByClass.get(clazz); 212 if (retval == null) { 213 retval = newBoundDefinition(clazz, bindingContext); 214 definitionsByClass.put(clazz, retval); 215 } 216 217 // // force loading of metaschema information to apply constraints 218 // IModule module = retval.getContainingModule(); 219 // registerModule(module, bindingContext); 220 return retval; 221 } finally { 222 definitionsLock.unlock(); 223 } 224 } 225 226 @NonNull 227 private IBoundDefinitionModelComplex newBoundDefinition( 228 @NonNull Class<? extends IBoundObject> clazz, 229 @NonNull IBindingContext bindingContext) { 230 IBoundDefinitionModelComplex retval; 231 if (clazz.isAnnotationPresent(MetaschemaAssembly.class)) { 232 MetaschemaAssembly annotation = ModelUtil.getAnnotation(clazz, MetaschemaAssembly.class); 233 Class<? extends IBoundModule> moduleClass = annotation.moduleClass(); 234 IBoundModule module = loadModule(moduleClass, bindingContext); 235 retval = DefinitionAssembly.newInstance(clazz, annotation, module, bindingContext); 236 } else if (clazz.isAnnotationPresent(MetaschemaField.class)) { 237 MetaschemaField annotation = ModelUtil.getAnnotation(clazz, MetaschemaField.class); 238 Class<? extends IBoundModule> moduleClass = annotation.moduleClass(); 239 IBoundModule module = loadModule(moduleClass, bindingContext); 240 retval = DefinitionField.newInstance(clazz, annotation, module, bindingContext); 241 } else { 242 throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.", 243 clazz.getName())); 244 } 245 return retval; 246 } 247}