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