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.ParallelValidationConfig; 049import dev.metaschema.core.model.constraint.ValidationFeature; 050import dev.metaschema.core.model.validation.AggregateValidationResult; 051import dev.metaschema.core.model.validation.IValidationResult; 052import dev.metaschema.core.model.validation.JsonSchemaContentValidator; 053import dev.metaschema.core.model.validation.XmlSchemaContentValidator; 054import dev.metaschema.core.util.CollectionUtil; 055import dev.metaschema.core.util.ObjectUtils; 056import dev.metaschema.databind.codegen.DefaultModuleBindingGenerator; 057import dev.metaschema.databind.io.BindingException; 058import dev.metaschema.databind.io.DefaultBoundLoader; 059import dev.metaschema.databind.io.DeserializationFeature; 060import dev.metaschema.databind.io.Format; 061import dev.metaschema.databind.io.IBoundLoader; 062import dev.metaschema.databind.io.IDeserializer; 063import dev.metaschema.databind.io.ISerializer; 064import dev.metaschema.databind.io.yaml.YamlOperations; 065import dev.metaschema.databind.model.IBoundDefinitionModel; 066import dev.metaschema.databind.model.IBoundDefinitionModelAssembly; 067import dev.metaschema.databind.model.IBoundDefinitionModelComplex; 068import dev.metaschema.databind.model.IBoundModule; 069import dev.metaschema.databind.model.annotations.MetaschemaAssembly; 070import dev.metaschema.databind.model.annotations.MetaschemaField; 071import dev.metaschema.databind.model.metaschema.BindingConstraintLoader; 072import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule; 073import dev.metaschema.databind.model.metaschema.IBindingModuleLoader; 074import dev.metaschema.databind.model.metaschema.ModuleLoadingPostProcessor; 075import edu.umd.cs.findbugs.annotations.NonNull; 076import edu.umd.cs.findbugs.annotations.Nullable; 077 078/** 079 * Provides information supporting a binding between a set of Module models and 080 * corresponding Java classes. 081 */ 082public interface IBindingContext { 083 /** 084 * Get a new builder that can produce a new, configured binding context. 085 * 086 * @return the builder 087 * @since 2.0.0 088 */ 089 static BindingContextBuilder builder() { 090 return new BindingContextBuilder(); 091 } 092 093 /** 094 * Get a new {@link IBindingContext} instance, which can be used to load 095 * information that binds a model to a set of Java classes. 096 * 097 * @return a new binding context 098 * @since 2.0.0 099 */ 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}