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