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.of(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     try {
102       registerModule(MetaschemaModelModule.class);
103     } catch (MetaschemaException ex) {
104       throw new IllegalStateException("Unable to register the builtin Metaschema module.", ex);
105     }
106   }
107 
108   @Override
109   @NonNull
110   public final IModuleLoaderStrategy getModuleLoaderStrategy() {
111     return moduleLoaderStrategy;
112   }
113 
114   @Override
115   public IBindingModuleLoader newModuleLoader() {
116     return new ModuleLoader(this, getModuleLoaderStrategy());
117   }
118 
119   @Override
120   @NonNull
121   public final IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz) throws MetaschemaException {
122     IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
123     IBoundModule module = strategy.loadModule(clazz, this);
124     registerImportedModules(module);
125     return strategy.registerModule(module, this);
126   }
127 
128   private void registerImportedModules(@NonNull IBoundModule module) throws MetaschemaException {
129     IModuleLoaderStrategy strategy = getModuleLoaderStrategy();
130     for (IBoundModule parentModule : module.getImportedModules()) {
131       assert parentModule != null;
132       registerImportedModules(parentModule);
133       strategy.registerModule(parentModule, this);
134     }
135   }
136 
137   /**
138    * Get the binding matchers that are associated with this class.
139    *
140    * @return the list of matchers
141    */
142   @NonNull
143   protected Collection<IBindingMatcher> getBindingMatchers() {
144     return getModuleLoaderStrategy().getBindingMatchers();
145   }
146 
147   @Override
148   public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) {
149     Class<?> clazz = definition.getBoundClass();
150     return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition);
151   }
152 
153   @Override
154   public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) {
155     return moduleLoaderStrategy.getBoundDefinitionForClass(clazz, this);
156   }
157 
158   /**
159    * {@inheritDoc}
160    * <p>
161    * A serializer returned by this method is thread-safe.
162    */
163   @Override
164   public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
165       @NonNull Format format,
166       @NonNull Class<CLASS> clazz) {
167     Objects.requireNonNull(format, "format");
168     IBoundDefinitionModelAssembly definition;
169     try {
170       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
171     } catch (ClassCastException ex) {
172       throw new IllegalStateException(
173           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
174     }
175     if (definition == null) {
176       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
177     }
178     ISerializer<CLASS> retval;
179     switch (format) {
180     case JSON:
181       retval = new DefaultJsonSerializer<>(definition);
182       break;
183     case XML:
184       retval = new DefaultXmlSerializer<>(definition);
185       break;
186     case YAML:
187       retval = new DefaultYamlSerializer<>(definition);
188       break;
189     default:
190       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
191     }
192     return retval;
193   }
194 
195   /**
196    * {@inheritDoc}
197    * <p>
198    * A deserializer returned by this method is thread-safe.
199    */
200   @Override
201   public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
202       @NonNull Format format,
203       @NonNull Class<CLASS> clazz) {
204     IBoundDefinitionModelAssembly definition;
205     try {
206       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
207     } catch (ClassCastException ex) {
208       throw new IllegalStateException(
209           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
210           ex);
211     }
212     if (definition == null) {
213       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
214     }
215     IDeserializer<CLASS> retval;
216     switch (format) {
217     case JSON:
218       retval = new DefaultJsonDeserializer<>(definition);
219       break;
220     case XML:
221       retval = new DefaultXmlDeserializer<>(definition);
222       break;
223     case YAML:
224       retval = new DefaultYamlDeserializer<>(definition);
225       break;
226     default:
227       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
228     }
229 
230     return retval;
231   }
232 
233   @Override
234   public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
235     Class<? extends IBoundObject> retval = null;
236     for (IBindingMatcher matcher : getBindingMatchers()) {
237       retval = matcher.getBoundClassForXmlQName(rootQName);
238       if (retval != null) {
239         break;
240       }
241     }
242     return retval;
243   }
244 
245   @Override
246   public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
247     Class<? extends IBoundObject> retval = null;
248     for (IBindingMatcher matcher : getBindingMatchers()) {
249       retval = matcher.getBoundClassForJsonName(rootName);
250       if (retval != null) {
251         break;
252       }
253     }
254     return retval;
255   }
256 
257   @Override
258   public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
259       throws BindingException {
260     IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
261     if (definition == null) {
262       throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
263     }
264     return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
265   }
266 
267   /**
268    * Used to prevent finalizer attacks as recommended in SEI CERT Rule OBJ-11.
269    * This is needed because the class is non-final and the constructor can throw.
270    */
271   @Override
272   @SuppressWarnings({
273       "PMD.EmptyFinalizer",
274       "checkstyle:NoFinalizer" })
275   protected final void finalize() {
276     // Do nothing
277   }
278 
279   private static class ModuleLoader
280       extends BindingModuleLoader {
281 
282     public ModuleLoader(
283         @NonNull IBindingContext bindingContext,
284         @NonNull ModuleLoadingPostProcessor postProcessor) {
285       super(bindingContext, postProcessor);
286     }
287 
288     @Override
289     public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException {
290       IBindingMetaschemaModule module = super.load(resource);
291       getBindingContext().registerModule(module);
292       return module;
293     }
294 
295     @Override
296     public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException {
297       IBindingMetaschemaModule module = super.load(path);
298       getBindingContext().registerModule(module);
299       return module;
300     }
301 
302     @Override
303     public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException {
304       IBindingMetaschemaModule module = super.load(url);
305       getBindingContext().registerModule(module);
306       return module;
307     }
308 
309     @Override
310     protected IBindingMetaschemaModule newModule(
311         URI resource,
312         METASCHEMA binding,
313         List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException {
314       return super.newModule(resource, binding, importedModules);
315     }
316   }
317 }