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.configuration.IConfiguration;
9   import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
10  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
11  import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
13  import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
14  import gov.nist.secauto.metaschema.core.model.IBoundObject;
15  import gov.nist.secauto.metaschema.core.model.IModule;
16  import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
17  import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
18  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
19  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
20  import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
21  import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
22  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
23  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
24  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
25  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
26  import gov.nist.secauto.metaschema.databind.io.BindingException;
27  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
28  import gov.nist.secauto.metaschema.databind.io.Format;
29  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
30  import gov.nist.secauto.metaschema.databind.io.IDeserializer;
31  import gov.nist.secauto.metaschema.databind.io.ISerializer;
32  import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
33  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
34  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
35  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
36  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
37  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
38  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
39  
40  import org.json.JSONObject;
41  import org.json.JSONTokener;
42  import org.xml.sax.SAXException;
43  
44  import java.io.BufferedInputStream;
45  import java.io.FileNotFoundException;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.math.BigInteger;
49  import java.net.URI;
50  import java.net.URL;
51  import java.nio.file.Path;
52  import java.time.ZonedDateTime;
53  import java.util.List;
54  
55  import javax.xml.namespace.QName;
56  import javax.xml.transform.Source;
57  
58  import edu.umd.cs.findbugs.annotations.NonNull;
59  import edu.umd.cs.findbugs.annotations.Nullable;
60  
61  /**
62   * Provides information supporting a binding between a set of Module models and
63   * corresponding Java classes.
64   */
65  public interface IBindingContext {
66  
67    /**
68     * Get the singleton {@link IBindingContext} instance, which can be used to load
69     * information that binds a model to a set of Java classes.
70     *
71     * @return a new binding context
72     */
73    @NonNull
74    static IBindingContext instance() {
75      return DefaultBindingContext.instance();
76    }
77  
78    /**
79     * Register a matcher used to identify a bound class by the definition's root
80     * name.
81     *
82     * @param definition
83     *          the definition to match for
84     * @return the matcher
85     */
86    @NonNull
87    IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition);
88  
89    /**
90     * Register a matcher used to identify a bound class by the definition's root
91     * name.
92     *
93     * @param clazz
94     *          the definition class to match for, which must represent a root
95     *          assembly definition
96     * @return the matcher
97     */
98    @NonNull
99    IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz);
100 
101   /**
102    * Register a class binding for a given bound class.
103    *
104    * @param definition
105    *          the bound class information to register
106    * @return the old bound class information or {@code null} if no binding existed
107    *         for the associated class
108    */
109   @Nullable
110   IBoundDefinitionModelComplex registerClassBinding(@NonNull IBoundDefinitionModelComplex definition);
111 
112   /**
113    * Get the {@link IBoundDefinitionModel} instance associated with the provided
114    * Java class.
115    * <p>
116    * Typically the class will have a {@link MetaschemaAssembly} or
117    * {@link MetaschemaField} annotation.
118    *
119    * @param clazz
120    *          the class binding to load
121    * @return the associated class binding instance or {@code null} if the class is
122    *         not bound
123    */
124   @Nullable
125   IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);
126 
127   /**
128    * Determine the bound class for the provided XML {@link QName}.
129    *
130    * @param rootQName
131    *          the root XML element's QName
132    * @return the bound class or {@code null} if not recognized
133    * @see IBindingContext#registerBindingMatcher(Class)
134    */
135   @Nullable
136   Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName);
137 
138   /**
139    * Determine the bound class for the provided JSON/YAML property/item name using
140    * any registered matchers.
141    *
142    * @param rootName
143    *          the JSON/YAML property/item name
144    * @return the bound class or {@code null} if not recognized
145    * @see IBindingContext#registerBindingMatcher(Class)
146    */
147   @Nullable
148   Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName);
149 
150   /**
151    * Get's the {@link IDataTypeAdapter} associated with the specified Java class,
152    * which is used to read and write XML, JSON, and YAML data to and from
153    * instances of that class. Thus, this adapter supports a direct binding between
154    * the Java class and structured data in one of the supported formats. Adapters
155    * are used to support bindings for simple data objects (e.g., {@link String},
156    * {@link BigInteger}, {@link ZonedDateTime}, etc).
157    *
158    * @param <TYPE>
159    *          the class type of the adapter
160    * @param clazz
161    *          the Java {@link Class} for the bound type
162    * @return the adapter instance or {@code null} if the provided class is not
163    *         bound
164    */
165   @Nullable
166   <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz);
167 
168   /**
169    * Load a bound Metaschema module implemented by the provided class.
170    * <p>
171    * Also registers any associated bound classes.
172    * <p>
173    * Implementations are expected to return the same IModule instance for multiple
174    * calls to this method with the same class argument.
175    *
176    * @param clazz
177    *          the class implementing a bound Metaschema module
178    * @return the loaded module
179    */
180   @NonNull
181   IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz);
182 
183   /**
184    * Generate, compile, and load a set of generated Module annotated Java classes
185    * based on the provided Module {@code module}.
186    *
187    * @param module
188    *          the Module module to generate classes for
189    * @param compilePath
190    *          the path to the directory to generate classes in
191    * @return this instance
192    * @throws IOException
193    *           if an error occurred while generating or loading the classes
194    */
195   @NonNull
196   IBindingContext registerModule(
197       @NonNull IModule module,
198       @NonNull Path compilePath) throws IOException;
199 
200   /**
201    * Gets a data {@link ISerializer} which can be used to write Java instance data
202    * for the provided class in the requested format.
203    * <p>
204    * The provided class must be a bound Java class with a
205    * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
206    * {@link IBoundDefinitionModel} exists.
207    *
208    * @param <CLASS>
209    *          the Java type this serializer can write data from
210    * @param format
211    *          the format to serialize into
212    * @param clazz
213    *          the Java data object to serialize
214    * @return the serializer instance
215    * @throws NullPointerException
216    *           if any of the provided arguments, except the configuration, are
217    *           {@code null}
218    * @throws IllegalArgumentException
219    *           if the provided class is not bound to a Module assembly or field
220    * @throws UnsupportedOperationException
221    *           if the requested format is not supported by the implementation
222    * @see #getBoundDefinitionForClass(Class)
223    */
224   @NonNull
225   <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
226       @NonNull Format format,
227       @NonNull Class<CLASS> clazz);
228 
229   /**
230    * Gets a data {@link IDeserializer} which can be used to read Java instance
231    * data for the provided class from the requested format.
232    * <p>
233    * The provided class must be a bound Java class with a
234    * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
235    * {@link IBoundDefinitionModel} exists.
236    *
237    * @param <CLASS>
238    *          the Java type this deserializer can read data into
239    * @param format
240    *          the format to serialize into
241    * @param clazz
242    *          the Java data type to serialize
243    * @return the deserializer instance
244    * @throws NullPointerException
245    *           if any of the provided arguments, except the configuration, are
246    *           {@code null}
247    * @throws IllegalArgumentException
248    *           if the provided class is not bound to a Module assembly or field
249    * @throws UnsupportedOperationException
250    *           if the requested format is not supported by the implementation
251    * @see #getBoundDefinitionForClass(Class)
252    */
253   @NonNull
254   <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
255       @NonNull Format format,
256       @NonNull Class<CLASS> clazz);
257 
258   /**
259    * Get a new {@link IBoundLoader} instance.
260    *
261    * @return the instance
262    */
263   @NonNull
264   IBoundLoader newBoundLoader();
265 
266   /**
267    * Create a deep copy of the provided bound object.
268    *
269    * @param <CLASS>
270    *          the bound object type
271    * @param other
272    *          the object to copy
273    * @param parentInstance
274    *          the object's parent or {@code null}
275    * @return a deep copy of the provided object
276    * @throws BindingException
277    *           if an error occurred copying content between java instances
278    * @throws NullPointerException
279    *           if the provided object is {@code null}
280    * @throws IllegalArgumentException
281    *           if the provided class is not bound to a Module assembly or field
282    */
283   @NonNull
284   <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
285       throws BindingException;
286 
287   /**
288    * Get a new single use constraint validator.
289    *
290    * @param handler
291    *          the validation handler to use to process the validation results
292    * @param config
293    *          the validation configuration
294    *
295    * @return the validator
296    */
297   default IConstraintValidator newValidator(
298       @NonNull IConstraintValidationHandler handler,
299       @Nullable IConfiguration<ValidationFeature<?>> config) {
300     IBoundLoader loader = newBoundLoader();
301     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
302 
303     DynamicContext context = new DynamicContext();
304     context.setDocumentLoader(loader);
305 
306     DefaultConstraintValidator retval = new DefaultConstraintValidator(handler);
307     if (config != null) {
308       retval.applyConfiguration(config);
309     }
310     return retval;
311   }
312 
313   /**
314    * Perform constraint validation on the provided bound object represented as an
315    * {@link IDocumentNodeItem}.
316    *
317    * @param nodeItem
318    *          the node item to validate
319    * @param loader
320    *          a module loader used to load and resolve referenced resources
321    * @param config
322    *          the validation configuration
323    * @return the validation result
324    * @throws IllegalArgumentException
325    *           if the provided class is not bound to a Module assembly or field
326    */
327   default IValidationResult validate(
328       @NonNull IDocumentNodeItem nodeItem,
329       @NonNull IBoundLoader loader,
330       @Nullable IConfiguration<ValidationFeature<?>> config) {
331     IRootAssemblyNodeItem root = nodeItem.getRootAssemblyNodeItem();
332     return validate(root, loader, config);
333   }
334 
335   /**
336    * Perform constraint validation on the provided bound object represented as an
337    * {@link IDefinitionNodeItem}.
338    *
339    * @param nodeItem
340    *          the node item to validate
341    * @param loader
342    *          a module loader used to load and resolve referenced resources
343    * @param config
344    *          the validation configuration
345    * @return the validation result
346    * @throws IllegalArgumentException
347    *           if the provided class is not bound to a Module assembly or field
348    */
349   default IValidationResult validate(
350       @NonNull IDefinitionNodeItem<?, ?> nodeItem,
351       @NonNull IBoundLoader loader,
352       @Nullable IConfiguration<ValidationFeature<?>> config) {
353 
354     FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
355     IConstraintValidator validator = newValidator(handler, config);
356 
357     DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
358     dynamicContext.setDocumentLoader(loader);
359 
360     validator.validate(nodeItem, dynamicContext);
361     validator.finalizeValidation(dynamicContext);
362     return handler;
363   }
364 
365   /**
366    * Load and perform schema and constraint validation on the target. The
367    * constraint validation will only be performed if the schema validation passes.
368    *
369    * @param target
370    *          the target to validate
371    * @param asFormat
372    *          the schema format to use to validate the target
373    * @param schemaProvider
374    *          provides callbacks to get the appropriate schemas
375    * @param config
376    *          the validation configuration
377    * @return the validation result
378    * @throws IOException
379    *           if an error occurred while reading the target
380    */
381   default IValidationResult validate(
382       @NonNull URI target,
383       @NonNull Format asFormat,
384       @NonNull ISchemaValidationProvider schemaProvider,
385       @Nullable IConfiguration<ValidationFeature<?>> config) throws IOException {
386 
387     IValidationResult retval = schemaProvider.validateWithSchema(target, asFormat);
388 
389     if (retval.isPassing()) {
390       IValidationResult constraintValidationResult = validateWithConstraints(target, config);
391       retval = AggregateValidationResult.aggregate(retval, constraintValidationResult);
392     }
393     return retval;
394   }
395 
396   /**
397    * Load and validate the provided {@code target} using the associated Module
398    * module constraints.
399    *
400    * @param target
401    *          the file to load and validate
402    * @param config
403    *          the validation configuration
404    * @return the validation results
405    * @throws IOException
406    *           if an error occurred while parsing the target
407    */
408   default IValidationResult validateWithConstraints(
409       @NonNull URI target,
410       @Nullable IConfiguration<ValidationFeature<?>> config)
411       throws IOException {
412     IBoundLoader loader = newBoundLoader();
413     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
414     IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);
415 
416     return validate(nodeItem, loader, config);
417   }
418 
419   interface IModuleLoaderStrategy {
420     /**
421      * Load the bound Metaschema module represented by the provided class.
422      * <p>
423      * Implementations are allowed to return a cached instance if the module has
424      * already been loaded.
425      *
426      * @param clazz
427      *          the Module class
428      * @return the module
429      * @throws IllegalStateException
430      *           if an error occurred while processing the associated module
431      *           information
432      */
433     @NonNull
434     IBoundModule loadModule(@NonNull Class<? extends IBoundModule> clazz);
435 
436     /**
437      * Get the {@link IBoundDefinitionModel} instance associated with the provided
438      * Java class.
439      * <p>
440      * Typically the class will have a {@link MetaschemaAssembly} or
441      * {@link MetaschemaField} annotation.
442      *
443      * @param clazz
444      *          the class binding to load
445      * @return the associated class binding instance or {@code null} if the class is
446      *         not bound
447      */
448     @Nullable
449     IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);
450   }
451 
452   interface ISchemaValidationProvider {
453 
454     @NonNull
455     default IValidationResult validateWithSchema(@NonNull URI target, @NonNull Format asFormat)
456         throws FileNotFoundException, IOException {
457       URL targetResource = ObjectUtils.notNull(target.toURL());
458 
459       IValidationResult retval;
460       switch (asFormat) {
461       case JSON: {
462         JSONObject json;
463         try (@SuppressWarnings("resource") InputStream is
464             = new BufferedInputStream(ObjectUtils.notNull(targetResource.openStream()))) {
465           json = new JSONObject(new JSONTokener(is));
466         }
467         retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, target);
468         break;
469       }
470       case XML:
471         try {
472           List<Source> schemaSources = getXmlSchemas(targetResource);
473           retval = new XmlSchemaContentValidator(schemaSources).validate(target);
474         } catch (SAXException ex) {
475           throw new IOException(ex);
476         }
477         break;
478       case YAML: {
479         JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
480         assert json != null;
481         retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, ObjectUtils.notNull(target));
482         break;
483       }
484       default:
485         throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
486       }
487       return retval;
488     }
489 
490     /**
491      * Get a JSON schema to use for content validation.
492      *
493      * @param json
494      *          the JSON content to validate
495      *
496      * @return the JSON schema
497      * @throws IOException
498      *           if an error occurred while loading the schema
499      */
500     @NonNull
501     JSONObject getJsonSchema(@NonNull JSONObject json) throws IOException;
502 
503     /**
504      * Get a XML schema to use for content validation.
505      *
506      * @param targetResource
507      *          the URL for the XML content to validate
508      *
509      * @return the XML schema sources
510      * @throws IOException
511      *           if an error occurred while loading the schema
512      */
513     @NonNull
514     List<Source> getXmlSchemas(@NonNull URL targetResource) throws IOException;
515   }
516 
517   /**
518    * Implementations of this interface provide a means by which a bound class can
519    * be found that corresponds to an XML element, JSON property, or YAML item
520    * name.
521    */
522   interface IBindingMatcher {
523     @SuppressWarnings("PMD.ShortMethodName")
524     @NonNull
525     static IBindingMatcher of(IBoundDefinitionModelAssembly assembly) {
526       if (!assembly.isRoot()) {
527         throw new IllegalArgumentException(
528             String.format("The provided class '%s' is not a root assembly.", assembly.getBoundClass().getName()));
529       }
530       return new RootAssemblyBindingMatcher(assembly);
531     }
532 
533     /**
534      * Determine the bound class for the provided XML {@link QName}.
535      *
536      * @param rootQName
537      *          the root XML element's QName
538      * @return the bound class for the XML qualified name or {@code null} if not
539      *         recognized
540      */
541     Class<? extends IBoundObject> getBoundClassForXmlQName(QName rootQName);
542 
543     /**
544      * Determine the bound class for the provided JSON/YAML property/item name.
545      *
546      * @param rootName
547      *          the JSON/YAML property/item name
548      * @return the bound class for the JSON property name or {@code null} if not
549      *         recognized
550      */
551     Class<? extends IBoundObject> getBoundClassForJsonName(String rootName);
552   }
553 }