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.NoOpValidationEventListener;
049import dev.metaschema.core.model.constraint.ValidationConfig;
050import dev.metaschema.core.model.constraint.ValidationEventListener;
051import dev.metaschema.core.model.constraint.ValidationFeature;
052import dev.metaschema.core.model.validation.AggregateValidationResult;
053import dev.metaschema.core.model.validation.IValidationResult;
054import dev.metaschema.core.model.validation.JsonSchemaContentValidator;
055import dev.metaschema.core.model.validation.XmlSchemaContentValidator;
056import dev.metaschema.core.util.CollectionUtil;
057import dev.metaschema.core.util.ObjectUtils;
058import dev.metaschema.databind.codegen.DefaultModuleBindingGenerator;
059import dev.metaschema.databind.io.BindingException;
060import dev.metaschema.databind.io.DefaultBoundLoader;
061import dev.metaschema.databind.io.DeserializationFeature;
062import dev.metaschema.databind.io.Format;
063import dev.metaschema.databind.io.IBoundLoader;
064import dev.metaschema.databind.io.IDeserializer;
065import dev.metaschema.databind.io.ISerializer;
066import dev.metaschema.databind.io.yaml.YamlOperations;
067import dev.metaschema.databind.model.IBoundDefinitionModel;
068import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
069import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
070import dev.metaschema.databind.model.IBoundModule;
071import dev.metaschema.databind.model.annotations.MetaschemaAssembly;
072import dev.metaschema.databind.model.annotations.MetaschemaField;
073import dev.metaschema.databind.model.metaschema.BindingConstraintLoader;
074import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
075import dev.metaschema.databind.model.metaschema.IBindingModuleLoader;
076import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
077import edu.umd.cs.findbugs.annotations.NonNull;
078import edu.umd.cs.findbugs.annotations.Nullable;
079
080/**
081 * Provides information supporting a binding between a set of Module models and
082 * corresponding Java classes.
083 */
084public interface IBindingContext {
085  /**
086   * Get a new builder that can produce a new, configured binding context.
087   *
088   * @return the builder
089   * @since 2.0.0
090   */
091  static BindingContextBuilder builder() {
092    return new BindingContextBuilder();
093  }
094
095  /**
096   * Get a new {@link IBindingContext} instance, which can be used to load
097   * information that binds a model to a set of Java classes.
098   *
099   * @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}