1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind;
7   
8   import gov.nist.secauto.metaschema.core.model.IBoundObject;
9   import gov.nist.secauto.metaschema.core.model.MetaschemaException;
10  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
11  import gov.nist.secauto.metaschema.databind.io.BindingException;
12  import gov.nist.secauto.metaschema.databind.io.Format;
13  import gov.nist.secauto.metaschema.databind.io.IDeserializer;
14  import gov.nist.secauto.metaschema.databind.io.ISerializer;
15  import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonDeserializer;
16  import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonSerializer;
17  import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlDeserializer;
18  import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlSerializer;
19  import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlDeserializer;
20  import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlSerializer;
21  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
22  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
23  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
24  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
25  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
26  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
27  import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
28  import gov.nist.secauto.metaschema.databind.model.metaschema.binding.METASCHEMA;
29  import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
30  
31  import java.io.IOException;
32  import java.net.URI;
33  import java.net.URL;
34  import java.nio.file.Path;
35  import java.util.Collection;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.concurrent.ConcurrentHashMap;
40  
41  import javax.xml.namespace.QName;
42  
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
45  import nl.talsmasoftware.lazy4j.Lazy;
46  
47  /**
48   * The implementation of a {@link IBindingContext} provided by this library.
49   * <p>
50   * This implementation caches Module information, which can dramatically improve
51   * read and write performance at the cost of some memory use. Thus, using the
52   * same singleton of this class across multiple I/O operations will improve
53   * overall read and write performance when processing the same types of data.
54   * <p>
55   * Serializers and deserializers provided by this class using the
56   * {@link #newSerializer(Format, Class)} and
57   * {@link #newDeserializer(Format, Class)} methods will
58   * <p>
59   * This class is synchronized and is thread-safe.
60   */
61  @SuppressWarnings("PMD.CouplingBetweenObjects")
62  public class DefaultBindingContext implements IBindingContext {
63    private static Lazy<DefaultBindingContext> singleton = Lazy.lazy(DefaultBindingContext::new);
64    @NonNull
65    private final IModuleLoaderStrategy moduleLoaderStrategy;
66    @NonNull
67    private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>();
68  
69    /**
70     * Get the singleton instance of this binding context.
71     * <p>
72     * Note: It is general a better practice to use a new {@link IBindingContext}
73     * and reuse that instance instead of this global instance.
74     *
75     * @return the binding context
76     * @see IBindingContext#newInstance()
77     */
78    @NonNull
79    static DefaultBindingContext instance() {
80      return ObjectUtils.notNull(singleton.get());
81    }
82  
83    /**
84     * Construct a new binding context.
85     */
86    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
87    public DefaultBindingContext() {
88      this(new SimpleModuleLoaderStrategy());
89    }
90  
91    /**
92     * Construct a new binding context.
93     *
94     * @param strategy
95     *          the behavior class to use for loading Metaschema modules
96     * @since 2.0.0
97     */
98    public DefaultBindingContext(@NonNull IBindingContext.IModuleLoaderStrategy strategy) {
99      // only allow extended classes
100     moduleLoaderStrategy = strategy;
101     registerModule(MetaschemaModelModule.class);
102   }
103 
104   @Override
105   @NonNull
106   public final IModuleLoaderStrategy getModuleLoaderStrategy() {
107     return moduleLoaderStrategy;
108   }
109 
110   @Override
111   public IBindingModuleLoader newModuleLoader() {
112     return new ModuleLoader(this, getModuleLoaderStrategy());
113   }
114 
115   @Override
116   @NonNull
117   public final IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz) {
118     IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
119     IBoundModule module = strategy.loadModule(clazz, this);
120     registerImportedModules(module);
121     return strategy.registerModule(module, this);
122   }
123 
124   private void registerImportedModules(@NonNull IBoundModule module) {
125     IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
126     module.getImportedModules().stream()
127         .forEachOrdered(parentModule -> {
128           registerImportedModules(ObjectUtils.notNull(parentModule));
129           strategy.registerModule(ObjectUtils.notNull(parentModule), this);
130         });
131   }
132 
133   /**
134    * Get the binding matchers that are associated with this class.
135    *
136    * @return the list of matchers
137    */
138   @NonNull
139   protected Collection<IBindingMatcher> getBindingMatchers() {
140     return getModuleLoaderStrategy().getBindingMatchers();
141   }
142 
143   @Override
144   public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) {
145     Class<?> clazz = definition.getBoundClass();
146     return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition);
147   }
148 
149   @Override
150   public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) {
151     return moduleLoaderStrategy.getBoundDefinitionForClass(clazz, this);
152   }
153 
154   /**
155    * {@inheritDoc}
156    * <p>
157    * A serializer returned by this method is thread-safe.
158    */
159   @Override
160   public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
161       @NonNull Format format,
162       @NonNull Class<CLASS> clazz) {
163     Objects.requireNonNull(format, "format");
164     IBoundDefinitionModelAssembly definition;
165     try {
166       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
167     } catch (ClassCastException ex) {
168       throw new IllegalStateException(
169           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
170     }
171     if (definition == null) {
172       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
173     }
174     ISerializer<CLASS> retval;
175     switch (format) {
176     case JSON:
177       retval = new DefaultJsonSerializer<>(definition);
178       break;
179     case XML:
180       retval = new DefaultXmlSerializer<>(definition);
181       break;
182     case YAML:
183       retval = new DefaultYamlSerializer<>(definition);
184       break;
185     default:
186       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
187     }
188     return retval;
189   }
190 
191   /**
192    * {@inheritDoc}
193    * <p>
194    * A deserializer returned by this method is thread-safe.
195    */
196   @Override
197   public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
198       @NonNull Format format,
199       @NonNull Class<CLASS> clazz) {
200     IBoundDefinitionModelAssembly definition;
201     try {
202       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
203     } catch (ClassCastException ex) {
204       throw new IllegalStateException(
205           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
206           ex);
207     }
208     if (definition == null) {
209       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
210     }
211     IDeserializer<CLASS> retval;
212     switch (format) {
213     case JSON:
214       retval = new DefaultJsonDeserializer<>(definition);
215       break;
216     case XML:
217       retval = new DefaultXmlDeserializer<>(definition);
218       break;
219     case YAML:
220       retval = new DefaultYamlDeserializer<>(definition);
221       break;
222     default:
223       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
224     }
225 
226     return retval;
227   }
228 
229   @Override
230   public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
231     Class<? extends IBoundObject> retval = null;
232     for (IBindingMatcher matcher : getBindingMatchers()) {
233       retval = matcher.getBoundClassForXmlQName(rootQName);
234       if (retval != null) {
235         break;
236       }
237     }
238     return retval;
239   }
240 
241   @Override
242   public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
243     Class<? extends IBoundObject> retval = null;
244     for (IBindingMatcher matcher : getBindingMatchers()) {
245       retval = matcher.getBoundClassForJsonName(rootName);
246       if (retval != null) {
247         break;
248       }
249     }
250     return retval;
251   }
252 
253   @Override
254   public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
255       throws BindingException {
256     IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
257     if (definition == null) {
258       throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
259     }
260     return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
261   }
262 
263   private static class ModuleLoader
264       extends BindingModuleLoader {
265 
266     public ModuleLoader(
267         @NonNull IBindingContext bindingContext,
268         @NonNull ModuleLoadingPostProcessor postProcessor) {
269       super(bindingContext, postProcessor);
270     }
271 
272     @Override
273     public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException {
274       IBindingMetaschemaModule module = super.load(resource);
275       getBindingContext().registerModule(module);
276       return module;
277     }
278 
279     @Override
280     public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException {
281       IBindingMetaschemaModule module = super.load(path);
282       getBindingContext().registerModule(module);
283       return module;
284     }
285 
286     @Override
287     public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException {
288       IBindingMetaschemaModule module = super.load(url);
289       getBindingContext().registerModule(module);
290       return module;
291     }
292 
293     @Override
294     protected IBindingMetaschemaModule newModule(
295         URI resource,
296         METASCHEMA binding,
297         List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException {
298       return super.newModule(resource, binding, importedModules);
299     }
300 
301   }
302 }