1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind;
7   
8   import java.io.IOException;
9   import java.net.URI;
10  import java.net.URL;
11  import java.nio.file.Path;
12  import java.util.Collection;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Objects;
16  import java.util.concurrent.ConcurrentHashMap;
17  
18  import javax.xml.namespace.QName;
19  
20  import dev.metaschema.core.model.IBoundObject;
21  import dev.metaschema.core.model.MetaschemaException;
22  import dev.metaschema.core.util.ObjectUtils;
23  import dev.metaschema.databind.io.BindingException;
24  import dev.metaschema.databind.io.Format;
25  import dev.metaschema.databind.io.IDeserializer;
26  import dev.metaschema.databind.io.ISerializer;
27  import dev.metaschema.databind.io.json.DefaultJsonDeserializer;
28  import dev.metaschema.databind.io.json.DefaultJsonSerializer;
29  import dev.metaschema.databind.io.xml.DefaultXmlDeserializer;
30  import dev.metaschema.databind.io.xml.DefaultXmlSerializer;
31  import dev.metaschema.databind.io.yaml.DefaultYamlDeserializer;
32  import dev.metaschema.databind.io.yaml.DefaultYamlSerializer;
33  import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
34  import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
35  import dev.metaschema.databind.model.IBoundModule;
36  import dev.metaschema.databind.model.metaschema.BindingModuleLoader;
37  import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
38  import dev.metaschema.databind.model.metaschema.IBindingModuleLoader;
39  import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
40  import dev.metaschema.databind.model.metaschema.binding.METASCHEMA;
41  import dev.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
42  import edu.umd.cs.findbugs.annotations.NonNull;
43  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44  import nl.talsmasoftware.lazy4j.Lazy;
45  
46  /**
47   * The implementation of a {@link IBindingContext} provided by this library.
48   * <p>
49   * This implementation caches Module information, which can dramatically improve
50   * read and write performance at the cost of some memory use. Thus, using the
51   * same singleton of this class across multiple I/O operations will improve
52   * overall read and write performance when processing the same types of data.
53   * <p>
54   * Serializers and deserializers provided by this class using the
55   * {@link #newSerializer(Format, Class)} and
56   * {@link #newDeserializer(Format, Class)} methods will
57   * <p>
58   * This class is synchronized and is thread-safe.
59   */
60  @SuppressWarnings("PMD.CouplingBetweenObjects")
61  public class DefaultBindingContext implements IBindingContext {
62    private static Lazy<DefaultBindingContext> singleton = Lazy.of(DefaultBindingContext::new);
63    @NonNull
64    private final IModuleLoaderStrategy moduleLoaderStrategy;
65    @NonNull
66    private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>();
67  
68    /**
69     * Get the singleton instance of this binding context.
70     * <p>
71     * Note: It is general a better practice to use a new {@link IBindingContext}
72     * and reuse that instance instead of this global instance.
73     *
74     * @return the binding context
75     * @see IBindingContext#newInstance()
76     */
77    @NonNull
78    static DefaultBindingContext instance() {
79      return ObjectUtils.notNull(singleton.get());
80    }
81  
82    /**
83     * Construct a new binding context.
84     */
85    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
86    public DefaultBindingContext() {
87      this(new SimpleModuleLoaderStrategy());
88    }
89  
90    /**
91     * Construct a new binding context.
92     *
93     * @param strategy
94     *          the behavior class to use for loading Metaschema modules
95     * @since 2.0.0
96     */
97    public DefaultBindingContext(@NonNull IBindingContext.IModuleLoaderStrategy strategy) {
98      // only allow extended classes
99      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 }