1 /*
2 * SPDX-FileCopyrightText: none
3 * SPDX-License-Identifier: CC0-1.0
4 */
5
6 package gov.nist.secauto.metaschema.databind;
7
8 import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
9 import gov.nist.secauto.metaschema.core.datatype.DataTypeService;
10 import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
11 import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
12 import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
13 import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
14 import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
15 import gov.nist.secauto.metaschema.core.model.IBoundObject;
16 import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
17 import gov.nist.secauto.metaschema.core.model.IModule;
18 import gov.nist.secauto.metaschema.core.model.IModuleLoader;
19 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
20 import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationException;
21 import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator;
22 import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
23 import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
24 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
25 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
26 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
27 import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
28 import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
29 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
30 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
31 import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
32 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
33 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
34 import gov.nist.secauto.metaschema.databind.codegen.DefaultModuleBindingGenerator;
35 import gov.nist.secauto.metaschema.databind.io.BindingException;
36 import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
37 import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
38 import gov.nist.secauto.metaschema.databind.io.Format;
39 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
40 import gov.nist.secauto.metaschema.databind.io.IDeserializer;
41 import gov.nist.secauto.metaschema.databind.io.ISerializer;
42 import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
43 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
44 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
45 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
46 import gov.nist.secauto.metaschema.databind.model.IBoundModule;
47 import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
48 import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
49 import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
50 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
51 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
52 import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
53
54 import org.json.JSONObject;
55 import org.json.JSONTokener;
56 import org.xml.sax.SAXException;
57
58 import java.io.BufferedInputStream;
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.math.BigInteger;
63 import java.net.URI;
64 import java.net.URL;
65 import java.nio.file.Path;
66 import java.time.ZonedDateTime;
67 import java.util.Collection;
68 import java.util.LinkedList;
69 import java.util.List;
70 import java.util.function.Function;
71
72 import javax.xml.namespace.QName;
73
74 import edu.umd.cs.findbugs.annotations.NonNull;
75 import edu.umd.cs.findbugs.annotations.Nullable;
76
77 /**
78 * Provides information supporting a binding between a set of Module models and
79 * corresponding Java classes.
80 */
81 public interface IBindingContext {
82 /**
83 * Get a new builder that can produce a new, configured binding context.
84 *
85 * @return the builder
86 * @since 2.0.0
87 */
88 static BindingContextBuilder builder() {
89 return new BindingContextBuilder();
90 }
91
92 /**
93 * Get a new {@link IBindingContext} instance, which can be used to load
94 * information that binds a model to a set of Java classes.
95 *
96 * @return a new binding context
97 * @since 2.0.0
98 */
99 @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 }