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