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     strategy.registerModule(module, this);
121     return module;
122   }
123 
124   /**
125    * Get the binding matchers that are associated with this class.
126    *
127    * @return the list of matchers
128    */
129   @NonNull
130   protected Collection<IBindingMatcher> getBindingMatchers() {
131     return getModuleLoaderStrategy().getBindingMatchers();
132   }
133 
134   @Override
135   public final IBoundDefinitionModelComplex registerClassBinding(IBoundDefinitionModelComplex definition) {
136     Class<?> clazz = definition.getBoundClass();
137     return boundClassToStrategyMap.computeIfAbsent(clazz, k -> definition);
138   }
139 
140   @Override
141   public final IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz) {
142     return moduleLoaderStrategy.getBoundDefinitionForClass(clazz, this);
143   }
144 
145   /**
146    * {@inheritDoc}
147    * <p>
148    * A serializer returned by this method is thread-safe.
149    */
150   @Override
151   public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
152       @NonNull Format format,
153       @NonNull Class<CLASS> clazz) {
154     Objects.requireNonNull(format, "format");
155     IBoundDefinitionModelAssembly definition;
156     try {
157       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
158     } catch (ClassCastException ex) {
159       throw new IllegalStateException(
160           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
161     }
162     if (definition == null) {
163       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
164     }
165     ISerializer<CLASS> retval;
166     switch (format) {
167     case JSON:
168       retval = new DefaultJsonSerializer<>(definition);
169       break;
170     case XML:
171       retval = new DefaultXmlSerializer<>(definition);
172       break;
173     case YAML:
174       retval = new DefaultYamlSerializer<>(definition);
175       break;
176     default:
177       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
178     }
179     return retval;
180   }
181 
182   /**
183    * {@inheritDoc}
184    * <p>
185    * A deserializer returned by this method is thread-safe.
186    */
187   @Override
188   public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
189       @NonNull Format format,
190       @NonNull Class<CLASS> clazz) {
191     IBoundDefinitionModelAssembly definition;
192     try {
193       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
194     } catch (ClassCastException ex) {
195       throw new IllegalStateException(
196           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
197           ex);
198     }
199     if (definition == null) {
200       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
201     }
202     IDeserializer<CLASS> retval;
203     switch (format) {
204     case JSON:
205       retval = new DefaultJsonDeserializer<>(definition);
206       break;
207     case XML:
208       retval = new DefaultXmlDeserializer<>(definition);
209       break;
210     case YAML:
211       retval = new DefaultYamlDeserializer<>(definition);
212       break;
213     default:
214       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
215     }
216 
217     return retval;
218   }
219 
220   @Override
221   public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
222     Class<? extends IBoundObject> retval = null;
223     for (IBindingMatcher matcher : getBindingMatchers()) {
224       retval = matcher.getBoundClassForXmlQName(rootQName);
225       if (retval != null) {
226         break;
227       }
228     }
229     return retval;
230   }
231 
232   @Override
233   public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
234     Class<? extends IBoundObject> retval = null;
235     for (IBindingMatcher matcher : getBindingMatchers()) {
236       retval = matcher.getBoundClassForJsonName(rootName);
237       if (retval != null) {
238         break;
239       }
240     }
241     return retval;
242   }
243 
244   @Override
245   public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
246       throws BindingException {
247     IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
248     if (definition == null) {
249       throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
250     }
251     return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
252   }
253 
254   private static class ModuleLoader
255       extends BindingModuleLoader {
256 
257     public ModuleLoader(
258         @NonNull IBindingContext bindingContext,
259         @NonNull ModuleLoadingPostProcessor postProcessor) {
260       super(bindingContext, postProcessor);
261     }
262 
263     @Override
264     public IBindingMetaschemaModule load(URI resource) throws MetaschemaException, IOException {
265       IBindingMetaschemaModule module = super.load(resource);
266       getBindingContext().registerModule(module);
267       return module;
268     }
269 
270     @Override
271     public IBindingMetaschemaModule load(Path path) throws MetaschemaException, IOException {
272       IBindingMetaschemaModule module = super.load(path);
273       getBindingContext().registerModule(module);
274       return module;
275     }
276 
277     @Override
278     public IBindingMetaschemaModule load(URL url) throws MetaschemaException, IOException {
279       IBindingMetaschemaModule module = super.load(url);
280       getBindingContext().registerModule(module);
281       return module;
282     }
283 
284     @Override
285     protected IBindingMetaschemaModule newModule(
286         URI resource,
287         METASCHEMA binding,
288         List<? extends IBindingMetaschemaModule> importedModules) throws MetaschemaException {
289       return super.newModule(resource, binding, importedModules);
290     }
291 
292   }
293 }