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