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