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