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