001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind;
007
008import org.eclipse.jdt.annotation.Owning;
009import org.json.JSONObject;
010import org.json.JSONTokener;
011import org.xml.sax.SAXException;
012
013import java.io.BufferedInputStream;
014import java.io.FileNotFoundException;
015import java.io.IOException;
016import java.io.InputStream;
017import java.math.BigInteger;
018import java.net.URI;
019import java.net.URL;
020import java.nio.file.Path;
021import java.time.ZonedDateTime;
022import java.util.Collection;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.function.Function;
026
027import javax.xml.namespace.QName;
028
029import dev.metaschema.core.configuration.IConfiguration;
030import dev.metaschema.core.datatype.DataTypeService;
031import dev.metaschema.core.datatype.IDataTypeAdapter;
032import dev.metaschema.core.metapath.DynamicContext;
033import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
034import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
035import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
036import dev.metaschema.core.model.IBoundObject;
037import dev.metaschema.core.model.IConstraintLoader;
038import dev.metaschema.core.model.IModule;
039import dev.metaschema.core.model.IModuleLoader;
040import dev.metaschema.core.model.MetaschemaException;
041import dev.metaschema.core.model.constraint.ConstraintValidationException;
042import dev.metaschema.core.model.constraint.DefaultConstraintValidator;
043import dev.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
044import dev.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
045import dev.metaschema.core.model.constraint.IConstraintSet;
046import dev.metaschema.core.model.constraint.IConstraintValidationHandler;
047import dev.metaschema.core.model.constraint.IConstraintValidator;
048import dev.metaschema.core.model.constraint.ParallelValidationConfig;
049import dev.metaschema.core.model.constraint.ValidationFeature;
050import dev.metaschema.core.model.validation.AggregateValidationResult;
051import dev.metaschema.core.model.validation.IValidationResult;
052import dev.metaschema.core.model.validation.JsonSchemaContentValidator;
053import dev.metaschema.core.model.validation.XmlSchemaContentValidator;
054import dev.metaschema.core.util.CollectionUtil;
055import dev.metaschema.core.util.ObjectUtils;
056import dev.metaschema.databind.codegen.DefaultModuleBindingGenerator;
057import dev.metaschema.databind.io.BindingException;
058import dev.metaschema.databind.io.DefaultBoundLoader;
059import dev.metaschema.databind.io.DeserializationFeature;
060import dev.metaschema.databind.io.Format;
061import dev.metaschema.databind.io.IBoundLoader;
062import dev.metaschema.databind.io.IDeserializer;
063import dev.metaschema.databind.io.ISerializer;
064import dev.metaschema.databind.io.yaml.YamlOperations;
065import dev.metaschema.databind.model.IBoundDefinitionModel;
066import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
067import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
068import dev.metaschema.databind.model.IBoundModule;
069import dev.metaschema.databind.model.annotations.MetaschemaAssembly;
070import dev.metaschema.databind.model.annotations.MetaschemaField;
071import dev.metaschema.databind.model.metaschema.BindingConstraintLoader;
072import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
073import dev.metaschema.databind.model.metaschema.IBindingModuleLoader;
074import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
075import edu.umd.cs.findbugs.annotations.NonNull;
076import edu.umd.cs.findbugs.annotations.Nullable;
077
078/**
079 * Provides information supporting a binding between a set of Module models and
080 * corresponding Java classes.
081 */
082public interface IBindingContext {
083  /**
084   * Get a new builder that can produce a new, configured binding context.
085   *
086   * @return the builder
087   * @since 2.0.0
088   */
089  static BindingContextBuilder builder() {
090    return new BindingContextBuilder();
091  }
092
093  /**
094   * Get a new {@link IBindingContext} instance, which can be used to load
095   * information that binds a model to a set of Java classes.
096   *
097   * @return a new binding context
098   * @since 2.0.0
099   */
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}