001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind; 007 008import org.eclipse.jdt.annotation.Owning; 009import org.json.JSONObject; 010import org.json.JSONTokener; 011import org.xml.sax.SAXException; 012 013import java.io.BufferedInputStream; 014import java.io.FileNotFoundException; 015import java.io.IOException; 016import java.io.InputStream; 017import java.math.BigInteger; 018import java.net.URI; 019import java.net.URL; 020import java.nio.file.Path; 021import java.time.ZonedDateTime; 022import java.util.Collection; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.function.Function; 026 027import javax.xml.namespace.QName; 028 029import dev.metaschema.core.configuration.IConfiguration; 030import dev.metaschema.core.datatype.DataTypeService; 031import dev.metaschema.core.datatype.IDataTypeAdapter; 032import dev.metaschema.core.metapath.DynamicContext; 033import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem; 034import dev.metaschema.core.metapath.item.node.IDocumentNodeItem; 035import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem; 036import dev.metaschema.core.model.IBoundObject; 037import dev.metaschema.core.model.IConstraintLoader; 038import dev.metaschema.core.model.IModule; 039import dev.metaschema.core.model.IModuleLoader; 040import dev.metaschema.core.model.MetaschemaException; 041import dev.metaschema.core.model.constraint.ConstraintValidationException; 042import dev.metaschema.core.model.constraint.DefaultConstraintValidator; 043import dev.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor; 044import dev.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler; 045import dev.metaschema.core.model.constraint.IConstraintSet; 046import dev.metaschema.core.model.constraint.IConstraintValidationHandler; 047import dev.metaschema.core.model.constraint.IConstraintValidator; 048import dev.metaschema.core.model.constraint.NoOpValidationEventListener; 049import dev.metaschema.core.model.constraint.ValidationConfig; 050import dev.metaschema.core.model.constraint.ValidationEventListener; 051import dev.metaschema.core.model.constraint.ValidationFeature; 052import dev.metaschema.core.model.validation.AggregateValidationResult; 053import dev.metaschema.core.model.validation.IValidationResult; 054import dev.metaschema.core.model.validation.JsonSchemaContentValidator; 055import dev.metaschema.core.model.validation.XmlSchemaContentValidator; 056import dev.metaschema.core.util.CollectionUtil; 057import dev.metaschema.core.util.ObjectUtils; 058import dev.metaschema.databind.codegen.DefaultModuleBindingGenerator; 059import dev.metaschema.databind.io.BindingException; 060import dev.metaschema.databind.io.DefaultBoundLoader; 061import dev.metaschema.databind.io.DeserializationFeature; 062import dev.metaschema.databind.io.Format; 063import dev.metaschema.databind.io.IBoundLoader; 064import dev.metaschema.databind.io.IDeserializer; 065import dev.metaschema.databind.io.ISerializer; 066import dev.metaschema.databind.io.yaml.YamlOperations; 067import dev.metaschema.databind.model.IBoundDefinitionModel; 068import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 069import dev.metaschema.databind.model.IBoundDefinitionModelComplex; 070import dev.metaschema.databind.model.IBoundModule; 071import dev.metaschema.databind.model.annotations.MetaschemaAssembly; 072import dev.metaschema.databind.model.annotations.MetaschemaField; 073import dev.metaschema.databind.model.metaschema.BindingConstraintLoader; 074import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule; 075import dev.metaschema.databind.model.metaschema.IBindingModuleLoader; 076import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor; 077import edu.umd.cs.findbugs.annotations.NonNull; 078import edu.umd.cs.findbugs.annotations.Nullable; 079 080/** 081 * Provides information supporting a binding between a set of Module models and 082 * corresponding Java classes. 083 */ 084public interface IBindingContext { 085 /** 086 * Get a new builder that can produce a new, configured binding context. 087 * 088 * @return the builder 089 * @since 2.0.0 090 */ 091 static BindingContextBuilder builder() { 092 return new BindingContextBuilder(); 093 } 094 095 /** 096 * Get a new {@link IBindingContext} instance, which can be used to load 097 * information that binds a model to a set of Java classes. 098 * 099 * @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}