001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind; 007 008import java.io.IOException; 009import java.net.URI; 010import java.net.URL; 011import java.nio.file.Path; 012import java.util.Collection; 013import java.util.List; 014import java.util.Map; 015import java.util.Objects; 016import java.util.concurrent.ConcurrentHashMap; 017 018import javax.xml.namespace.QName; 019 020import dev.metaschema.core.model.IBoundObject; 021import dev.metaschema.core.model.MetaschemaException; 022import dev.metaschema.core.util.ObjectUtils; 023import dev.metaschema.databind.io.BindingException; 024import dev.metaschema.databind.io.Format; 025import dev.metaschema.databind.io.IDeserializer; 026import dev.metaschema.databind.io.ISerializer; 027import dev.metaschema.databind.io.json.DefaultJsonDeserializer; 028import dev.metaschema.databind.io.json.DefaultJsonSerializer; 029import dev.metaschema.databind.io.xml.DefaultXmlDeserializer; 030import dev.metaschema.databind.io.xml.DefaultXmlSerializer; 031import dev.metaschema.databind.io.yaml.DefaultYamlDeserializer; 032import dev.metaschema.databind.io.yaml.DefaultYamlSerializer; 033import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 034import dev.metaschema.databind.model.IBoundDefinitionModelComplex; 035import dev.metaschema.databind.model.IBoundModule; 036import dev.metaschema.databind.model.metaschema.BindingModuleLoader; 037import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule; 038import dev.metaschema.databind.model.metaschema.IBindingModuleLoader; 039import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor; 040import dev.metaschema.databind.model.metaschema.binding.METASCHEMA; 041import dev.metaschema.databind.model.metaschema.binding.MetaschemaModelModule; 042import edu.umd.cs.findbugs.annotations.NonNull; 043import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 044import nl.talsmasoftware.lazy4j.Lazy; 045 046/** 047 * The implementation of a {@link IBindingContext} provided by this library. 048 * <p> 049 * This implementation caches Module information, which can dramatically improve 050 * read and write performance at the cost of some memory use. Thus, using the 051 * same singleton of this class across multiple I/O operations will improve 052 * overall read and write performance when processing the same types of data. 053 * <p> 054 * Serializers and deserializers provided by this class using the 055 * {@link #newSerializer(Format, Class)} and 056 * {@link #newDeserializer(Format, Class)} methods will 057 * <p> 058 * This class is synchronized and is thread-safe. 059 */ 060@SuppressWarnings("PMD.CouplingBetweenObjects") 061public class DefaultBindingContext implements IBindingContext { 062 private static Lazy<DefaultBindingContext> singleton = Lazy.of(DefaultBindingContext::new); 063 @NonNull 064 private final IModuleLoaderStrategy moduleLoaderStrategy; 065 @NonNull 066 private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>(); 067 068 /** 069 * Get the singleton instance of this binding context. 070 * <p> 071 * Note: It is general a better practice to use a new {@link IBindingContext} 072 * and reuse that instance instead of this global instance. 073 * 074 * @return the binding context 075 * @see IBindingContext#newInstance() 076 */ 077 @NonNull 078 static DefaultBindingContext instance() { 079 return ObjectUtils.notNull(singleton.get()); 080 } 081 082 /** 083 * Construct a new binding context. 084 */ 085 @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") 086 public DefaultBindingContext() { 087 this(new SimpleModuleLoaderStrategy()); 088 } 089 090 /** 091 * Construct a new binding context. 092 * 093 * @param strategy 094 * the behavior class to use for loading Metaschema modules 095 * @since 2.0.0 096 */ 097 public DefaultBindingContext(@NonNull IBindingContext.IModuleLoaderStrategy strategy) { 098 // only allow extended classes 099 moduleLoaderStrategy = strategy; 100 try { 101 registerModule(MetaschemaModelModule.class); 102 } catch (MetaschemaException ex) { 103 throw new IllegalStateException("Unable to register the builtin Metaschema module.", ex); 104 } 105 } 106 107 @Override 108 @NonNull 109 public final IModuleLoaderStrategy getModuleLoaderStrategy() { 110 return moduleLoaderStrategy; 111 } 112 113 @Override 114 public IBindingModuleLoader newModuleLoader() { 115 return new ModuleLoader(this, getModuleLoaderStrategy()); 116 } 117 118 @Override 119 @NonNull 120 public final IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz) throws MetaschemaException { 121 IModuleLoaderStrategy strategy = getModuleLoaderStrategy(); 122 IBoundModule module = strategy.loadModule(clazz, this); 123 registerImportedModules(module); 124 return strategy.registerModule(module, this); 125 } 126 127 private void registerImportedModules(@NonNull IBoundModule module) throws MetaschemaException { 128 IModuleLoaderStrategy strategy = getModuleLoaderStrategy(); 129 for (IBoundModule parentModule : module.getImportedModules()) { 130 assert parentModule != null; 131 registerImportedModules(parentModule); 132 strategy.registerModule(parentModule, this); 133 } 134 } 135 136 /** 137 * Get the binding matchers that are associated with this class. 138 * 139 * @return the list of matchers 140 */ 141 @NonNull 142 protected Collection<IBindingMatcher> getBindingMatchers() { 143 return getModuleLoaderStrategy().getBindingMatchers(); 144 } 145 146 @Override 147 public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) { 148 Class<?> clazz = definition.getBoundClass(); 149 return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition); 150 } 151 152 @Override 153 public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) { 154 return moduleLoaderStrategy.getBoundDefinitionForClass(clazz, this); 155 } 156 157 /** 158 * {@inheritDoc} 159 * <p> 160 * A serializer returned by this method is thread-safe. 161 */ 162 @Override 163 public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer( 164 @NonNull Format format, 165 @NonNull Class<CLASS> clazz) { 166 Objects.requireNonNull(format, "format"); 167 IBoundDefinitionModelAssembly definition; 168 try { 169 definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz)); 170 } catch (ClassCastException ex) { 171 throw new IllegalStateException( 172 String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex); 173 } 174 if (definition == null) { 175 throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName())); 176 } 177 ISerializer<CLASS> retval; 178 switch (format) { 179 case JSON: 180 retval = new DefaultJsonSerializer<>(definition); 181 break; 182 case XML: 183 retval = new DefaultXmlSerializer<>(definition); 184 break; 185 case YAML: 186 retval = new DefaultYamlSerializer<>(definition); 187 break; 188 default: 189 throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format)); 190 } 191 return retval; 192 } 193 194 /** 195 * {@inheritDoc} 196 * <p> 197 * A deserializer returned by this method is thread-safe. 198 */ 199 @Override 200 public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer( 201 @NonNull Format format, 202 @NonNull Class<CLASS> clazz) { 203 IBoundDefinitionModelAssembly definition; 204 try { 205 definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz)); 206 } catch (ClassCastException ex) { 207 throw new IllegalStateException( 208 String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), 209 ex); 210 } 211 if (definition == null) { 212 throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName())); 213 } 214 IDeserializer<CLASS> retval; 215 switch (format) { 216 case JSON: 217 retval = new DefaultJsonDeserializer<>(definition); 218 break; 219 case XML: 220 retval = new DefaultXmlDeserializer<>(definition); 221 break; 222 case YAML: 223 retval = new DefaultYamlDeserializer<>(definition); 224 break; 225 default: 226 throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format)); 227 } 228 229 return retval; 230 } 231 232 @Override 233 public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) { 234 Class<? extends IBoundObject> retval = null; 235 for (IBindingMatcher matcher : getBindingMatchers()) { 236 retval = matcher.getBoundClassForXmlQName(rootQName); 237 if (retval != null) { 238 break; 239 } 240 } 241 return retval; 242 } 243 244 @Override 245 public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) { 246 Class<? extends IBoundObject> retval = null; 247 for (IBindingMatcher matcher : getBindingMatchers()) { 248 retval = matcher.getBoundClassForJsonName(rootName); 249 if (retval != null) { 250 break; 251 } 252 } 253 return retval; 254 } 255 256 @Override 257 public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance) 258 throws BindingException { 259 IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass()); 260 if (definition == null) { 261 throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName())); 262 } 263 return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance)); 264 } 265 266 /** 267 * Used to prevent finalizer attacks as recommended in SEI CERT Rule OBJ-11. 268 * This is needed because the class is non-final and the constructor can throw. 269 */ 270 @Override 271 @SuppressWarnings({ 272 "deprecation", 273 "checkstyle:NoFinalizer" }) 274 protected final void finalize() { 275 // Do nothing 276 } 277 278 private static class ModuleLoader 279 extends BindingModuleLoader { 280 281 public ModuleLoader( 282 @NonNull IBindingContext bindingContext, 283 @NonNull ModuleLoadingPostProcessor postProcessor) { 284 super(bindingContext, postProcessor); 285 } 286 287 @Override 288 public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException { 289 IBindingMetaschemaModule module = super.load(resource); 290 getBindingContext().registerModule(module); 291 return module; 292 } 293 294 @Override 295 public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException { 296 IBindingMetaschemaModule module = super.load(path); 297 getBindingContext().registerModule(module); 298 return module; 299 } 300 301 @Override 302 public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException { 303 IBindingMetaschemaModule module = super.load(url); 304 getBindingContext().registerModule(module); 305 return module; 306 } 307 308 @Override 309 protected IBindingMetaschemaModule newModule( 310 URI resource, 311 METASCHEMA binding, 312 List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException { 313 return super.newModule(resource, binding, importedModules); 314 } 315 } 316}