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.DefaultConstraintValidator;
21 import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
22 import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
23 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
24 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler;
25 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator;
26 import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
27 import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
28 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
29 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator;
30 import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator;
31 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
32 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
33 import gov.nist.secauto.metaschema.databind.codegen.DefaultModuleBindingGenerator;
34 import gov.nist.secauto.metaschema.databind.io.BindingException;
35 import gov.nist.secauto.metaschema.databind.io.DefaultBoundLoader;
36 import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
37 import gov.nist.secauto.metaschema.databind.io.Format;
38 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
39 import gov.nist.secauto.metaschema.databind.io.IDeserializer;
40 import gov.nist.secauto.metaschema.databind.io.ISerializer;
41 import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations;
42 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel;
43 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
44 import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
45 import gov.nist.secauto.metaschema.databind.model.IBoundModule;
46 import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
47 import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
48 import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
49 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
50 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
51 import gov.nist.secauto.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor;
52
53 import org.json.JSONObject;
54 import org.json.JSONTokener;
55 import org.xml.sax.SAXException;
56
57 import java.io.BufferedInputStream;
58 import java.io.FileNotFoundException;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.math.BigInteger;
62 import java.net.URI;
63 import java.net.URL;
64 import java.nio.file.Path;
65 import java.time.ZonedDateTime;
66 import java.util.Collection;
67 import java.util.LinkedList;
68 import java.util.List;
69 import java.util.function.Function;
70
71 import javax.xml.namespace.QName;
72
73 import edu.umd.cs.findbugs.annotations.NonNull;
74 import edu.umd.cs.findbugs.annotations.Nullable;
75
76 /**
77 * Provides information supporting a binding between a set of Module models and
78 * corresponding Java classes.
79 */
80 public interface IBindingContext {
81 /**
82 * Get a new builder that can produce a new, configured binding context.
83 *
84 * @return the builder
85 * @since 2.0.0
86 */
87 static BindingContextBuilder builder() {
88 return new BindingContextBuilder();
89 }
90
91 /**
92 * Get a new {@link IBindingContext} instance, which can be used to load
93 * information that binds a model to a set of Java classes.
94 *
95 * @return a new binding context
96 * @since 2.0.0
97 */
98 @NonNull
99 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 getDataTypeAdapterInstance(@NonNull Class<TYPE> clazz) {
337 return DataTypeService.instance().getDataTypeByAdapterClass(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.
567 * <li><b>Post Processing:</b> The module is prepared for use.
568 * <li><b>Registration:</b> The module is registered for use.
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 }