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