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.DataTypeService;
10  import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
11  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
13  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
14  import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
15  import gov.nist.secauto.metaschema.core.model.IBoundObject;
16  import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
17  import gov.nist.secauto.metaschema.core.model.IModule;
18  import gov.nist.secauto.metaschema.core.model.IModuleLoader;
19  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
20  import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
21  import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
22  import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
23  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
24  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
25  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
26  import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
27  import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
28  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
29  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
30  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
31  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
32  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
33  import gov.nist.secauto.metaschema.databind.codegen.DefaultModuleBindingGenerator;
34  import gov.nist.secauto.metaschema.databind.io.BindingException;
35  import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
36  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
37  import gov.nist.secauto.metaschema.databind.io.Format;
38  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
39  import gov.nist.secauto.metaschema.databind.io.IDeserializer;
40  import gov.nist.secauto.metaschema.databind.io.ISerializer;
41  import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
42  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
43  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
44  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
45  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
46  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
47  import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
48  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
49  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
50  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
51  import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
52  
53  import org.json.JSONObject;
54  import org.json.JSONTokener;
55  import org.xml.sax.SAXException;
56  
57  import java.io.BufferedInputStream;
58  import java.io.FileNotFoundException;
59  import java.io.IOException;
60  import java.io.InputStream;
61  import java.math.BigInteger;
62  import java.net.URI;
63  import java.net.URL;
64  import java.nio.file.Path;
65  import java.time.ZonedDateTime;
66  import java.util.Collection;
67  import java.util.LinkedList;
68  import java.util.List;
69  import java.util.function.Function;
70  
71  import javax.xml.namespace.QName;
72  
73  import edu.umd.cs.findbugs.annotations.NonNull;
74  import edu.umd.cs.findbugs.annotations.Nullable;
75  
76  /**
77   * Provides information supporting a binding between a set of Module models and
78   * corresponding Java classes.
79   */
80  public interface IBindingContext {
81    /**
82     * Get a new builder that can produce a new, configured binding context.
83     *
84     * @return the builder
85     * @since 2.0.0
86     */
87    static BindingContextBuilder builder() {
88      return new BindingContextBuilder();
89    }
90  
91    /**
92     * Get a new {@link IBindingContext} instance, which can be used to load
93     * information that binds a model to a set of Java classes.
94     *
95     * @return a new binding context
96     * @since 2.0.0
97     */
98    @NonNull
99    static IBindingContext newInstance() {
100     return new DefaultBindingContext();
101   }
102 
103   /**
104    * Get a new {@link IBindingContext} instance, which can be used to load
105    * information that binds a model to a set of Java classes.
106    *
107    * @param strategy
108    *          the loader strategy to use when loading Metaschema modules
109    * @return a new binding context
110    * @since 2.0.0
111    */
112   @NonNull
113   static IBindingContext newInstance(@NonNull IBindingContext.IModuleLoaderStrategy strategy) {
114     return new DefaultBindingContext(strategy);
115   }
116 
117   /**
118    * Get the Metaschema module loader strategy used by this binding context to
119    * load modules.
120    *
121    * @return the strategy instance
122    * @since 2.0.0
123    */
124   @NonNull
125   IModuleLoaderStrategy getModuleLoaderStrategy();
126 
127   /**
128    * Get a loader that supports loading a Metaschema module from a specified
129    * resource.
130    * <p>
131    * Modules loaded with this loader are automatically registered with this
132    * binding context.
133    * <p>
134    * Use of this method requires that the binding context is initialized using a
135    * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
136    * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
137    * initialized using the {@link DefaultModuleBindingGenerator}. * @return the
138    * loader
139    *
140    * @return the loader
141    * @since 2.0.0
142    */
143   @NonNull
144   IBindingModuleLoader newModuleLoader();
145 
146   /**
147    * Loads a Metaschema module from the specified path.
148    * <p>
149    * This method automatically registers the module with this binding context.
150    * <p>
151    * Use of this method requires that the binding context is initialized using a
152    * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
153    * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
154    * initialized using the {@link DefaultModuleBindingGenerator}.
155    *
156    * @param path
157    *          the path to load the module from
158    * @return the loaded Metaschema module
159    * @throws MetaschemaException
160    *           if an error occurred while processing the resource
161    * @throws IOException
162    *           if an error occurred parsing the resource
163    * @throws UnsupportedOperationException
164    *           if this binding context is not configured to support dynamic bound
165    *           module loading
166    * @since 2.0.0
167    */
168   @NonNull
169   default IBindingMetaschemaModule loadMetaschema(@NonNull Path path) throws MetaschemaException, IOException {
170     return newModuleLoader().load(path);
171   }
172 
173   /**
174    * Loads a Metaschema module from the specified URL.
175    * <p>
176    * This method automatically registers the module with this binding context.
177    * <p>
178    * Use of this method requires that the binding context is initialized using a
179    * {@link IModuleLoaderStrategy} that supports dynamic bound module loading.
180    * This can be accomplished using the {@link SimpleModuleLoaderStrategy}
181    * initialized using the {@link DefaultModuleBindingGenerator}.
182    *
183    * @param url
184    *          the URL to load the module from
185    * @return the loaded Metaschema module
186    * @throws MetaschemaException
187    *           if an error occurred while processing the resource
188    * @throws IOException
189    *           if an error occurred parsing the resource
190    * @throws UnsupportedOperationException
191    *           if this binding context is not configured to support dynamic bound
192    *           module loading
193    * @since 2.0.0
194    */
195   @NonNull
196   default IBindingMetaschemaModule loadMetaschema(@NonNull URL url) throws MetaschemaException, IOException {
197     return newModuleLoader().load(url);
198   }
199 
200   /**
201    * Get a loader that supports loading Metaschema module constraints from a
202    * specified resource.
203    * <p>
204    * Metaschema module constraints loaded this need to be used with a new
205    * {@link IBindingContext} instance to be applied to loaded modules. The new
206    * binding context must initialized using the
207    * {@link PostProcessingModuleLoaderStrategy} that is initialized with a
208    * {@link ExternalConstraintsModulePostProcessor} instance.
209    *
210    * @return the loader
211    * @since 2.0.0
212    */
213   @NonNull
214   static IConstraintLoader getConstraintLoader() {
215     return new BindingConstraintLoader(DefaultBindingContext.instance());
216   }
217 
218   /**
219    * Get a loader that supports loading Metaschema module constraints from a
220    * specified resource.
221    * <p>
222    * Metaschema module constraints loaded this need to be used with a new
223    * {@link IBindingContext} instance to be applied to loaded modules. The new
224    * binding context must initialized using the
225    * {@link PostProcessingModuleLoaderStrategy} that is initialized with a
226    * {@link ExternalConstraintsModulePostProcessor} instance.
227    *
228    * @return the loader
229    * @since 2.0.0
230    */
231   @NonNull
232   default IConstraintLoader newConstraintLoader() {
233     return new BindingConstraintLoader(this);
234   }
235 
236   /**
237    * Load a bound Metaschema module implemented by the provided class.
238    * <p>
239    * Also registers any associated bound classes.
240    * <p>
241    * Implementations are expected to return the same IModule instance for multiple
242    * calls to this method with the same class argument.
243    *
244    * @param clazz
245    *          the class implementing a bound Metaschema module
246    * @return the loaded module
247    */
248   @NonNull
249   IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz);
250 
251   /**
252    * Registers the provided Metaschema module with this binding context.
253    * <p>
254    * If the provided instance is not an instance of {@link IBoundModule}, then
255    * annotated Java classes for this module will be generated, compiled, and
256    * loaded based on the provided Module.
257    *
258    * @param module
259    *          the Module module to generate classes for
260    * @return the registered module, which may be a different instance than what
261    *         was provided if dynamic compilation was performed
262    * @throws UnsupportedOperationException
263    *           if this binding context is not configured to support dynamic bound
264    *           module loading and the module instance is not a subclass of
265    *           {@link IBoundModule}
266    * @since 2.0.0
267    */
268   @NonNull
269   default IBoundModule registerModule(@NonNull IModule module) {
270     return getModuleLoaderStrategy().registerModule(module, this);
271   }
272 
273   /**
274    * Register a class binding for a given bound class.
275    *
276    * @param definition
277    *          the bound class information to register
278    * @return the old bound class information or {@code null} if no binding existed
279    *         for the associated class
280    */
281   @Nullable
282   IBoundDefinitionModelComplex registerClassBinding(@NonNull IBoundDefinitionModelComplex definition);
283 
284   /**
285    * Get the {@link IBoundDefinitionModel} instance associated with the provided
286    * Java class.
287    * <p>
288    * Typically the class will have a {@link MetaschemaAssembly} or
289    * {@link MetaschemaField} annotation.
290    *
291    * @param clazz
292    *          the class binding to load
293    * @return the associated class binding instance or {@code null} if the class is
294    *         not bound
295    */
296   @Nullable
297   IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz);
298 
299   /**
300    * Determine the bound class for the provided XML {@link QName}.
301    *
302    * @param rootQName
303    *          the root XML element's QName
304    * @return the bound class or {@code null} if not recognized
305    */
306   @Nullable
307   Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName);
308 
309   /**
310    * Determine the bound class for the provided JSON/YAML property/item name using
311    * any registered matchers.
312    *
313    * @param rootName
314    *          the JSON/YAML property/item name
315    * @return the bound class or {@code null} if not recognized
316    */
317   @Nullable
318   Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName);
319 
320   /**
321    * Get's the {@link IDataTypeAdapter} associated with the specified Java class,
322    * which is used to read and write XML, JSON, and YAML data to and from
323    * instances of that class. Thus, this adapter supports a direct binding between
324    * the Java class and structured data in one of the supported formats. Adapters
325    * are used to support bindings for simple data objects (e.g., {@link String},
326    * {@link BigInteger}, {@link ZonedDateTime}, etc).
327    *
328    * @param <TYPE>
329    *          the class type of the adapter
330    * @param clazz
331    *          the Java {@link Class} for the bound type
332    * @return the adapter instance or {@code null} if the provided class is not
333    *         bound
334    */
335   @Nullable
336   default <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz) {
337     return DataTypeService.getInstance().getJavaTypeAdapterByClass(clazz);
338   }
339 
340   /**
341    * Gets a data {@link ISerializer} which can be used to write Java instance data
342    * for the provided class in the requested format.
343    * <p>
344    * The provided class must be a bound Java class with a
345    * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
346    * {@link IBoundDefinitionModel} exists.
347    *
348    * @param <CLASS>
349    *          the Java type this serializer can write data from
350    * @param format
351    *          the format to serialize into
352    * @param clazz
353    *          the Java data object to serialize
354    * @return the serializer instance
355    * @throws NullPointerException
356    *           if any of the provided arguments, except the configuration, are
357    *           {@code null}
358    * @throws IllegalArgumentException
359    *           if the provided class is not bound to a Module assembly or field
360    * @throws UnsupportedOperationException
361    *           if the requested format is not supported by the implementation
362    * @see #getBoundDefinitionForClass(Class)
363    */
364   @NonNull
365   <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer(
366       @NonNull Format format,
367       @NonNull Class<CLASS> clazz);
368 
369   /**
370    * Gets a data {@link IDeserializer} which can be used to read Java instance
371    * data for the provided class from the requested format.
372    * <p>
373    * The provided class must be a bound Java class with a
374    * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a
375    * {@link IBoundDefinitionModel} exists.
376    *
377    * @param <CLASS>
378    *          the Java type this deserializer can read data into
379    * @param format
380    *          the format to serialize into
381    * @param clazz
382    *          the Java data type to serialize
383    * @return the deserializer instance
384    * @throws NullPointerException
385    *           if any of the provided arguments, except the configuration, are
386    *           {@code null}
387    * @throws IllegalArgumentException
388    *           if the provided class is not bound to a Module assembly or field
389    * @throws UnsupportedOperationException
390    *           if the requested format is not supported by the implementation
391    * @see #getBoundDefinitionForClass(Class)
392    */
393   @NonNull
394   <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer(
395       @NonNull Format format,
396       @NonNull Class<CLASS> clazz);
397 
398   /**
399    * Get a new {@link IBoundLoader} instance to load bound content instances.
400    *
401    * @return the instance
402    */
403   @NonNull
404   default IBoundLoader newBoundLoader() {
405     return new DefaultBoundLoader(this);
406   }
407 
408   /**
409    * Create a deep copy of the provided bound object.
410    *
411    * @param <CLASS>
412    *          the bound object type
413    * @param other
414    *          the object to copy
415    * @param parentInstance
416    *          the object's parent or {@code null}
417    * @return a deep copy of the provided object
418    * @throws BindingException
419    *           if an error occurred copying content between java instances
420    * @throws NullPointerException
421    *           if the provided object is {@code null}
422    * @throws IllegalArgumentException
423    *           if the provided class is not bound to a Module assembly or field
424    */
425   @NonNull
426   <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance)
427       throws BindingException;
428 
429   /**
430    * Get a new single use constraint validator.
431    *
432    * @param handler
433    *          the validation handler to use to process the validation results
434    * @param config
435    *          the validation configuration
436    *
437    * @return the validator
438    */
439   default IConstraintValidator newValidator(
440       @NonNull IConstraintValidationHandler handler,
441       @Nullable IConfiguration<ValidationFeature<?>> config) {
442     IBoundLoader loader = newBoundLoader();
443     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
444 
445     DynamicContext context = new DynamicContext();
446     context.setDocumentLoader(loader);
447 
448     DefaultConstraintValidator retval = new DefaultConstraintValidator(handler);
449     if (config != null) {
450       retval.applyConfiguration(config);
451     }
452     return retval;
453   }
454 
455   /**
456    * Perform constraint validation on the provided bound object represented as an
457    * {@link IDocumentNodeItem}.
458    *
459    * @param nodeItem
460    *          the node item to validate
461    * @param loader
462    *          a module loader used to load and resolve referenced resources
463    * @param config
464    *          the validation configuration
465    * @return the validation result
466    * @throws IllegalArgumentException
467    *           if the provided class is not bound to a Module assembly or field
468    */
469   default IValidationResult validate(
470       @NonNull IDocumentNodeItem nodeItem,
471       @NonNull IBoundLoader loader,
472       @Nullable IConfiguration<ValidationFeature<?>> config) {
473     IRootAssemblyNodeItem root = nodeItem.getRootAssemblyNodeItem();
474     return validate(root, loader, config);
475   }
476 
477   /**
478    * Perform constraint validation on the provided bound object represented as an
479    * {@link IDefinitionNodeItem}.
480    *
481    * @param nodeItem
482    *          the node item to validate
483    * @param loader
484    *          a module loader used to load and resolve referenced resources
485    * @param config
486    *          the validation configuration
487    * @return the validation result
488    * @throws IllegalArgumentException
489    *           if the provided class is not bound to a Module assembly or field
490    */
491   default IValidationResult validate(
492       @NonNull IDefinitionNodeItem<?, ?> nodeItem,
493       @NonNull IBoundLoader loader,
494       @Nullable IConfiguration<ValidationFeature<?>> config) {
495 
496     FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
497     IConstraintValidator validator = newValidator(handler, config);
498 
499     DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
500     dynamicContext.setDocumentLoader(loader);
501 
502     validator.validate(nodeItem, dynamicContext);
503     validator.finalizeValidation(dynamicContext);
504     return handler;
505   }
506 
507   /**
508    * Load and perform schema and constraint validation on the target. The
509    * constraint validation will only be performed if the schema validation passes.
510    *
511    * @param target
512    *          the target to validate
513    * @param asFormat
514    *          the schema format to use to validate the target
515    * @param schemaProvider
516    *          provides callbacks to get the appropriate schemas
517    * @param config
518    *          the validation configuration
519    * @return the validation result
520    * @throws IOException
521    *           if an error occurred while reading the target
522    */
523   default IValidationResult validate(
524       @NonNull URI target,
525       @NonNull Format asFormat,
526       @NonNull ISchemaValidationProvider schemaProvider,
527       @Nullable IConfiguration<ValidationFeature<?>> config) throws IOException {
528 
529     IValidationResult retval = schemaProvider.validateWithSchema(target, asFormat, this);
530 
531     if (retval.isPassing()) {
532       IValidationResult constraintValidationResult = validateWithConstraints(target, config);
533       retval = AggregateValidationResult.aggregate(retval, constraintValidationResult);
534     }
535     return retval;
536   }
537 
538   /**
539    * Load and validate the provided {@code target} using the associated Module
540    * module constraints.
541    *
542    * @param target
543    *          the file to load and validate
544    * @param config
545    *          the validation configuration
546    * @return the validation results
547    * @throws IOException
548    *           if an error occurred while parsing the target
549    */
550   default IValidationResult validateWithConstraints(
551       @NonNull URI target,
552       @Nullable IConfiguration<ValidationFeature<?>> config)
553       throws IOException {
554     IBoundLoader loader = newBoundLoader();
555     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
556     IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);
557 
558     return validate(nodeItem, loader, config);
559   }
560 
561   /**
562    * A behavioral class used by the binding context to load Metaschema modules.
563    * <p>
564    * A module will flow through the following process.
565    * <ol>
566    * <li><b>Loading:</b> The module is read from its source.</li>
567    * <li><b>Post Processing:</b> The module is prepared for use.</li>
568    * <li><b>Registration:</b> The module is registered for use.</li>
569    * </ol>
570    * <p>
571    * A module will be loaded when either the module or one of its global
572    * definitions is accessed the first time.
573    */
574   interface IModuleLoaderStrategy extends ModuleLoadingPostProcessor {
575     /**
576      * Load the bound Metaschema module represented by the provided class.
577      * <p>
578      * This is the primary entry point for loading an already bound module. This
579      * method must ensure that the loaded module is post-processed and registered.
580      * <p>
581      * Implementations are allowed to return a cached instance if the module has
582      * already been loaded by this method.
583      *
584      * @param clazz
585      *          the Module class
586      * @param bindingContext
587      *          the Metaschema binding context used to load bound resources
588      * @return the module
589      * @throws IllegalStateException
590      *           if an error occurred while processing the associated module
591      *           information
592      * @since 2.0.0
593      */
594     @NonNull
595     IBoundModule loadModule(
596         @NonNull Class<? extends IBoundModule> clazz,
597         @NonNull IBindingContext bindingContext);
598 
599     /**
600      * Perform post-processing on the module.
601      *
602      * @param module
603      *          the Metaschema module to post-process
604      * @param bindingContext
605      *          the Metaschema binding context used to load bound resources
606      * @since 2.0.0
607      */
608     @Override
609     default void postProcessModule(
610         @NonNull IModule module,
611         @NonNull IBindingContext bindingContext) {
612       // do nothing by default
613     }
614 
615     /**
616      * Registers the provided Metaschema module.
617      * <p>
618      * If this module has not been post-processed, this method is expected to drive
619      * post-processing first.
620      * <p>
621      * If the provided instance is not an instance of {@link IBoundModule}, then
622      * annotated Java classes for this module will be generated, compiled, and
623      * loaded based on the provided Module.
624      *
625      * @param module
626      *          the Module module to generate classes for
627      * @param bindingContext
628      *          the Metaschema binding context used to load bound resources
629      * @return the registered module, which may be a different instance than what
630      *         was provided if dynamic compilation was performed
631      * @throws UnsupportedOperationException
632      *           if this binding context is not configured to support dynamic bound
633      *           module loading and the module instance is not a subclass of
634      *           {@link IBoundModule}
635      * @since 2.0.0
636      */
637     @NonNull
638     IBoundModule registerModule(
639         @NonNull IModule module,
640         @NonNull IBindingContext bindingContext);
641     //
642     // /**
643     // * Register a matcher used to identify a bound class by the definition's root
644     // * name.
645     // *
646     // * @param definition
647     // * the definition to match for
648     // * @return the matcher
649     // */
650     // @NonNull
651     // IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly
652     // definition);
653 
654     /**
655      * Get the matchers used to identify the bound class associated with the
656      * definition's root name.
657      *
658      * @return the matchers
659      */
660     @NonNull
661     Collection<IBindingMatcher> getBindingMatchers();
662 
663     /**
664      * Get the {@link IBoundDefinitionModel} instance associated with the provided
665      * Java class.
666      * <p>
667      * Typically the class will have a {@link MetaschemaAssembly} or
668      * {@link MetaschemaField} annotation.
669      *
670      * @param clazz
671      *          the class binding to load
672      * @param bindingContext
673      *          the Metaschema binding context used to load bound resources
674      * @return the associated class binding instance
675      * @throws IllegalArgumentException
676      *           if the class is not a bound definition with a
677      *           {@link MetaschemaAssembly} or {@link MetaschemaField} annotation
678      */
679     @NonNull
680     IBoundDefinitionModelComplex getBoundDefinitionForClass(
681         @NonNull Class<? extends IBoundObject> clazz,
682         @NonNull IBindingContext bindingContext);
683   }
684 
685   /**
686    * Enables building a {@link IBindingContext} using common configuration options
687    * based on the builder pattern.
688    *
689    * @since 2.0.0
690    */
691   final class BindingContextBuilder {
692     private Path compilePath;
693     private final List<IModuleLoader.IModulePostProcessor> postProcessors = new LinkedList<>();
694     private final List<IConstraintSet> constraintSets = new LinkedList<>();
695     @NonNull
696     private final Function<IBindingContext.IModuleLoaderStrategy, IBindingContext> initializer;
697 
698     private BindingContextBuilder() {
699       this(DefaultBindingContext::new);
700     }
701 
702     /**
703      * Construct a new builder.
704      *
705      * @param initializer
706      *          the callback to use to get a new binding context instance
707      */
708     public BindingContextBuilder(
709         @NonNull Function<IBindingContext.IModuleLoaderStrategy, IBindingContext> initializer) {
710       this.initializer = initializer;
711     }
712 
713     /**
714      * Enable dynamic code generation and compilation for Metaschema module-based
715      * classes.
716      *
717      * @param path
718      *          the path to use to generate and compile Metaschema module-based
719      *          classes
720      * @return this builder
721      */
722     @NonNull
723     public BindingContextBuilder compilePath(@NonNull Path path) {
724       compilePath = path;
725       return this;
726     }
727 
728     /**
729      * Configure a Metaschema module post processor.
730      *
731      * @param processor
732      *          the post processor to configure
733      * @return this builder
734      */
735     @NonNull
736     public BindingContextBuilder postProcessor(@NonNull IModuleLoader.IModulePostProcessor processor) {
737       postProcessors.add(processor);
738       return this;
739     }
740 
741     /**
742      * Configure a set of constraints targeting Metaschema modules.
743      *
744      * @param set
745      *          the constraint set to configure
746      * @return this builder
747      */
748     @NonNull
749     public BindingContextBuilder constraintSet(@NonNull IConstraintSet set) {
750       constraintSets.add(set);
751       return this;
752     }
753 
754     /**
755      * Configure a collection of constraint sets targeting Metaschema modules.
756      *
757      * @param set
758      *          the constraint sets to configure
759      * @return this builder
760      */
761     @NonNull
762     public BindingContextBuilder constraintSet(@NonNull Collection<IConstraintSet> set) {
763       constraintSets.addAll(set);
764       return this;
765     }
766 
767     /**
768      * Build a {@link IBindingContext} using the configuration options provided to
769      * the builder.
770      *
771      * @return a new, configured binding context
772      */
773     @NonNull
774     public IBindingContext build() {
775       // get loader strategy based on if code generation is configured
776       IBindingContext.IModuleLoaderStrategy strategy = compilePath == null
777           ? new SimpleModuleLoaderStrategy()
778           : new SimpleModuleLoaderStrategy(new DefaultModuleBindingGenerator(compilePath));
779 
780       // determine if any post processors are configured or need to be
781       List<IModuleLoader.IModulePostProcessor> processors = new LinkedList<>(postProcessors);
782       if (!constraintSets.isEmpty()) {
783         processors.add(new ExternalConstraintsModulePostProcessor(constraintSets));
784       }
785 
786       if (!processors.isEmpty()) {
787         // post processors are configured, configure the loader strategy to handle them
788         strategy = new PostProcessingModuleLoaderStrategy(
789             CollectionUtil.unmodifiableList(processors),
790             strategy);
791       }
792 
793       return ObjectUtils.notNull(initializer.apply(strategy));
794     }
795   }
796 
797   /**
798    * Provides schema validation capabilities.
799    */
800   interface ISchemaValidationProvider {
801 
802     /**
803      * Validate the target resource.
804      *
805      * @param target
806      *          the resource to validate
807      * @param asFormat
808      *          the format to validate the content as
809      * @param bindingContext
810      *          the Metaschema binding context used to load bound resources
811      * @return the validation result
812      * @throws FileNotFoundException
813      *           if the resource was not found
814      * @throws IOException
815      *           if an error occurred while reading the resource
816      */
817     @NonNull
818     default IValidationResult validateWithSchema(
819         @NonNull URI target,
820         @NonNull Format asFormat,
821         @NonNull IBindingContext bindingContext)
822         throws FileNotFoundException, IOException {
823       URL targetResource = ObjectUtils.notNull(target.toURL());
824 
825       IValidationResult retval;
826       switch (asFormat) {
827       case JSON: {
828         JSONObject json;
829         try (@SuppressWarnings("resource")
830         InputStream is
831             = new BufferedInputStream(ObjectUtils.notNull(targetResource.openStream()))) {
832           json = new JSONObject(new JSONTokener(is));
833         }
834         retval = getJsonSchema(json, bindingContext).validate(json, target);
835         break;
836       }
837       case XML:
838         try {
839           retval = getXmlSchemas(targetResource, bindingContext).validate(target);
840         } catch (SAXException ex) {
841           throw new IOException(ex);
842         }
843         break;
844       case YAML: {
845         JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target));
846         assert json != null;
847         retval = getJsonSchema(json, bindingContext).validate(json, ObjectUtils.notNull(target));
848         break;
849       }
850       default:
851         throw new UnsupportedOperationException("Unsupported format: " + asFormat.name());
852       }
853       return retval;
854     }
855 
856     /**
857      * Get a JSON schema to use for content validation.
858      *
859      * @param json
860      *          the JSON content to validate
861      * @param bindingContext
862      *          the Metaschema binding context used to load bound resources
863      * @return the JSON schema validator
864      * @throws IOException
865      *           if an error occurred while loading the schema
866      * @since 2.0.0
867      */
868     @NonNull
869     JsonSchemaContentValidator getJsonSchema(@NonNull JSONObject json, @NonNull IBindingContext bindingContext)
870         throws IOException;
871 
872     /**
873      * Get a XML schema to use for content validation.
874      *
875      * @param targetResource
876      *          the URL for the XML content to validate
877      * @param bindingContext
878      *          the Metaschema binding context used to load bound resources
879      * @return the XML schema validator
880      * @throws IOException
881      *           if an error occurred while loading the schema
882      * @throws SAXException
883      *           if an error occurred while parsing the schema
884      * @since 2.0.0
885      */
886     @NonNull
887     XmlSchemaContentValidator getXmlSchemas(@NonNull URL targetResource, @NonNull IBindingContext bindingContext)
888         throws IOException, SAXException;
889   }
890 
891   /**
892    * Implementations of this interface provide a means by which a bound class can
893    * be found that corresponds to an XML element, JSON property, or YAML item
894    * name.
895    */
896   interface IBindingMatcher {
897     /**
898      * Construct a new binding matcher for the provided assembly definition.
899      *
900      * @param assembly
901      *          the assembly definition that matcher is for
902      * @return the matcher
903      */
904     @SuppressWarnings("PMD.ShortMethodName")
905     @NonNull
906     static IBindingMatcher of(IBoundDefinitionModelAssembly assembly) {
907       if (!assembly.isRoot()) {
908         throw new IllegalArgumentException(
909             String.format("The provided class '%s' is not a root assembly.", assembly.getBoundClass().getName()));
910       }
911       return new RootAssemblyBindingMatcher(assembly);
912     }
913 
914     /**
915      * Determine the bound class for the provided XML {@link QName}.
916      *
917      * @param rootQName
918      *          the root XML element's QName
919      * @return the bound class for the XML qualified name or {@code null} if not
920      *         recognized
921      */
922     Class<? extends IBoundObject> getBoundClassForXmlQName(QName rootQName);
923 
924     /**
925      * Determine the bound class for the provided JSON/YAML property/item name.
926      *
927      * @param rootName
928      *          the JSON/YAML property/item name
929      * @return the bound class for the JSON property name or {@code null} if not
930      *         recognized
931      */
932     Class<? extends IBoundObject> getBoundClassForJsonName(String rootName);
933   }
934 }