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.datatype.DataTypeService;
9   import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
10  import gov.nist.secauto.metaschema.core.model.IBoundObject;
11  import gov.nist.secauto.metaschema.core.model.IModule;
12  import gov.nist.secauto.metaschema.core.model.IModuleLoader;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.databind.codegen.IProduction;
15  import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
16  import gov.nist.secauto.metaschema.databind.io.BindingException;
17  import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
18  import gov.nist.secauto.metaschema.databind.io.Format;
19  import gov.nist.secauto.metaschema.databind.io.IDeserializer;
20  import gov.nist.secauto.metaschema.databind.io.ISerializer;
21  import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonDeserializer;
22  import gov.nist.secauto.metaschema.databind.io.json.DefaultJsonSerializer;
23  import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlDeserializer;
24  import gov.nist.secauto.metaschema.databind.io.xml.DefaultXmlSerializer;
25  import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlDeserializer;
26  import gov.nist.secauto.metaschema.databind.io.yaml.DefaultYamlSerializer;
27  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
28  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
29  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
30  import gov.nist.secauto.metaschema.databind.model.binding.metaschema.METASCHEMA;
31  
32  import java.io.IOException;
33  import java.nio.file.Files;
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  
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  public class DefaultBindingContext implements IBindingContext {
61    private static DefaultBindingContext singleton;
62    @NonNull
63    private final IModuleLoaderStrategy moduleLoaderStrategy;
64    @NonNull
65    private final Map<Class<?>, IBoundDefinitionModelComplex> boundClassToStrategyMap = new ConcurrentHashMap<>();
66    @NonNull
67    private final Map<IBoundDefinitionModelAssembly, IBindingMatcher> bindingMatchers = new ConcurrentHashMap<>();
68  
69    /**
70     * Get the singleton instance of this binding context.
71     *
72     * @return the binding context
73     */
74    @NonNull
75    public static DefaultBindingContext instance() {
76      synchronized (DefaultBindingContext.class) {
77        if (singleton == null) {
78          singleton = new DefaultBindingContext();
79        }
80      }
81      return ObjectUtils.notNull(singleton);
82    }
83  
84    /**
85     * Construct a new binding context.
86     *
87     * @param modulePostProcessors
88     *          a list of module post processors to call after loading a module
89     */
90    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
91    public DefaultBindingContext(@NonNull List<IModuleLoader.IModulePostProcessor> modulePostProcessors) {
92      // only allow extended classes
93      moduleLoaderStrategy = new PostProcessingModuleLoaderStrategy(this, modulePostProcessors);
94      registerBindingMatcher(METASCHEMA.class);
95    }
96  
97    /**
98     * Construct a new binding context.
99     */
100   @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
101   public DefaultBindingContext() {
102     // only allow extended classes
103     moduleLoaderStrategy = new SimpleModuleLoaderStrategy(this);
104     registerBindingMatcher(METASCHEMA.class);
105   }
106 
107   @NonNull
108   protected IModuleLoaderStrategy getModuleLoaderStrategy() {
109     return moduleLoaderStrategy;
110   }
111 
112   /**
113    * Get the binding matchers that are associated with this class.
114    *
115    * @return the list of matchers
116    * @see #registerBindingMatcher(Class)
117    * @see #registerBindingMatcher(IBoundDefinitionModelAssembly)
118    */
119   @NonNull
120   protected Collection<IBindingMatcher> getBindingMatchers() {
121     return ObjectUtils.notNull(bindingMatchers.values());
122   }
123 
124   @Override
125   @NonNull
126   public final IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition) {
127     return ObjectUtils.notNull(bindingMatchers.computeIfAbsent(definition, key -> IBindingMatcher.of(definition)));
128   }
129 
130   @Override
131   public final IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz) {
132     IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(clazz);
133     if (definition == null) {
134       throw new IllegalArgumentException(String.format("Unable to find bound definition for class '%s'.",
135           clazz.getName()));
136     }
137 
138     try {
139       IBoundDefinitionModelAssembly assemblyDefinition = IBoundDefinitionModelAssembly.class.cast(definition);
140       return registerBindingMatcher(ObjectUtils.notNull(assemblyDefinition));
141     } catch (ClassCastException ex) {
142       throw new IllegalArgumentException(
143           String.format("The provided class '%s' is not a root assembly.", clazz.getName()), ex);
144     }
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);
156   }
157 
158   @Override
159   public <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz) {
160     return DataTypeService.getInstance().getJavaTypeAdapterByClass(clazz);
161   }
162 
163   /**
164    * {@inheritDoc}
165    * <p>
166    * A serializer returned by this method is thread-safe.
167    */
168   @Override
169   public <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
170       @NonNull Format format,
171       @NonNull Class<CLASS> clazz) {
172     Objects.requireNonNull(format, "format");
173     IBoundDefinitionModelAssembly definition;
174     try {
175       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
176     } catch (ClassCastException ex) {
177       throw new IllegalStateException(
178           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()), ex);
179     }
180     if (definition == null) {
181       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getClass().getName()));
182     }
183     ISerializer<CLASS> retval;
184     switch (format) {
185     case JSON:
186       retval = new DefaultJsonSerializer<>(definition);
187       break;
188     case XML:
189       retval = new DefaultXmlSerializer<>(definition);
190       break;
191     case YAML:
192       retval = new DefaultYamlSerializer<>(definition);
193       break;
194     default:
195       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
196     }
197     return retval;
198   }
199 
200   /**
201    * {@inheritDoc}
202    * <p>
203    * A deserializer returned by this method is thread-safe.
204    */
205   @Override
206   public <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
207       @NonNull Format format,
208       @NonNull Class<CLASS> clazz) {
209     IBoundDefinitionModelAssembly definition;
210     try {
211       definition = IBoundDefinitionModelAssembly.class.cast(getBoundDefinitionForClass(clazz));
212     } catch (ClassCastException ex) {
213       throw new IllegalStateException(
214           String.format("Class '%s' is not a bound assembly.", clazz.getClass().getName()),
215           ex);
216     }
217     if (definition == null) {
218       throw new IllegalStateException(String.format("Class '%s' is not bound", clazz.getName()));
219     }
220     IDeserializer<CLASS> retval;
221     switch (format) {
222     case JSON:
223       retval = new DefaultJsonDeserializer<>(definition);
224       break;
225     case XML:
226       retval = new DefaultXmlDeserializer<>(definition);
227       break;
228     case YAML:
229       retval = new DefaultYamlDeserializer<>(definition);
230       break;
231     default:
232       throw new UnsupportedOperationException(String.format("Unsupported format '%s'", format));
233     }
234 
235     return retval;
236   }
237 
238   @Override
239   @SuppressWarnings({ "PMD.UseProperClassLoader", "unchecked" }) // false positive
240   @NonNull
241   public IBindingContext registerModule(
242       @NonNull IModule module,
243       @NonNull Path compilePath) throws IOException {
244     if (!(module instanceof IBoundModule)) {
245       Files.createDirectories(compilePath);
246 
247       ClassLoader classLoader = ModuleCompilerHelper.newClassLoader(
248           compilePath,
249           ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
250 
251       IProduction production = ModuleCompilerHelper.compileMetaschema(module, compilePath);
252       production.getModuleProductions().stream()
253           .map(item -> {
254             try {
255               return (Class<? extends IBoundModule>) classLoader.loadClass(item.getClassName().reflectionName());
256             } catch (ClassNotFoundException ex) {
257               throw new IllegalStateException(ex);
258             }
259           })
260           .forEachOrdered(clazz -> {
261             IBoundModule boundModule = registerModule(ObjectUtils.notNull(clazz));
262             // force the binding matchers to load
263             boundModule.getRootAssemblyDefinitions();
264           });
265     }
266     return this;
267   }
268 
269   @Override
270   public IBoundModule registerModule(Class<? extends IBoundModule> clazz) {
271     return getModuleLoaderStrategy().loadModule(clazz);
272     // retval.getExportedAssemblyDefinitions().stream()
273     // .map(def -> (IBoundDefinitionModelAssembly) def)
274     // .filter(def -> def.isRoot())
275     // .forEachOrdered(def -> registerBindingMatcher(ObjectUtils.notNull(def)));
276     // return retval;
277   }
278 
279   @Override
280   public Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName) {
281     Class<? extends IBoundObject> retval = null;
282     for (IBindingMatcher matcher : getBindingMatchers()) {
283       retval = matcher.getBoundClassForXmlQName(rootQName);
284       if (retval != null) {
285         break;
286       }
287     }
288     return retval;
289   }
290 
291   @Override
292   public Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName) {
293     Class<? extends IBoundObject> retval = null;
294     for (IBindingMatcher matcher : getBindingMatchers()) {
295       retval = matcher.getBoundClassForJsonName(rootName);
296       if (retval != null) {
297         break;
298       }
299     }
300     return retval;
301   }
302 
303   @Override
304   public DefaultBoundLoader newBoundLoader() {
305     return new DefaultBoundLoader(this);
306   }
307 
308   @Override
309   public <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
310       throws BindingException {
311     IBoundDefinitionModelComplex definition = getBoundDefinitionForClass(other.getClass());
312     if (definition == null) {
313       throw new IllegalStateException(String.format("Class '%s' is not bound", other.getClass().getName()));
314     }
315     return ObjectUtils.asType(definition.deepCopyItem(other, parentInstance));
316   }
317 }