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.IDataTypeAdapter; 010import gov.nist.secauto.metaschema.core.metapath.DynamicContext; 011import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem; 012import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem; 013import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem; 014import gov.nist.secauto.metaschema.core.model.IBoundObject; 015import gov.nist.secauto.metaschema.core.model.IModule; 016import gov.nist.secauto.metaschema.core.model.constraint.DefaultConstraintValidator; 017import gov.nist.secauto.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler; 018import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidationHandler; 019import gov.nist.secauto.metaschema.core.model.constraint.IConstraintValidator; 020import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature; 021import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult; 022import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; 023import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator; 024import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator; 025import gov.nist.secauto.metaschema.core.util.ObjectUtils; 026import gov.nist.secauto.metaschema.databind.io.BindingException; 027import gov.nist.secauto.metaschema.databind.io.DeserializationFeature; 028import gov.nist.secauto.metaschema.databind.io.Format; 029import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 030import gov.nist.secauto.metaschema.databind.io.IDeserializer; 031import gov.nist.secauto.metaschema.databind.io.ISerializer; 032import gov.nist.secauto.metaschema.databind.io.yaml.YamlOperations; 033import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModel; 034import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly; 035import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex; 036import gov.nist.secauto.metaschema.databind.model.IBoundModule; 037import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; 038import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField; 039 040import org.json.JSONObject; 041import org.json.JSONTokener; 042import org.xml.sax.SAXException; 043 044import java.io.BufferedInputStream; 045import java.io.FileNotFoundException; 046import java.io.IOException; 047import java.io.InputStream; 048import java.math.BigInteger; 049import java.net.URI; 050import java.net.URL; 051import java.nio.file.Path; 052import java.time.ZonedDateTime; 053import java.util.List; 054 055import javax.xml.namespace.QName; 056import javax.xml.transform.Source; 057 058import edu.umd.cs.findbugs.annotations.NonNull; 059import edu.umd.cs.findbugs.annotations.Nullable; 060 061/** 062 * Provides information supporting a binding between a set of Module models and 063 * corresponding Java classes. 064 */ 065public interface IBindingContext { 066 067 /** 068 * Get the singleton {@link IBindingContext} instance, which can be used to load 069 * information that binds a model to a set of Java classes. 070 * 071 * @return a new binding context 072 */ 073 @NonNull 074 static IBindingContext instance() { 075 return DefaultBindingContext.instance(); 076 } 077 078 /** 079 * Register a matcher used to identify a bound class by the definition's root 080 * name. 081 * 082 * @param definition 083 * the definition to match for 084 * @return the matcher 085 */ 086 @NonNull 087 IBindingMatcher registerBindingMatcher(@NonNull IBoundDefinitionModelAssembly definition); 088 089 /** 090 * Register a matcher used to identify a bound class by the definition's root 091 * name. 092 * 093 * @param clazz 094 * the definition class to match for, which must represent a root 095 * assembly definition 096 * @return the matcher 097 */ 098 @NonNull 099 IBindingMatcher registerBindingMatcher(@NonNull Class<? extends IBoundObject> clazz); 100 101 /** 102 * Register a class binding for a given bound class. 103 * 104 * @param definition 105 * the bound class information to register 106 * @return the old bound class information or {@code null} if no binding existed 107 * for the associated class 108 */ 109 @Nullable 110 IBoundDefinitionModelComplex registerClassBinding(@NonNull IBoundDefinitionModelComplex definition); 111 112 /** 113 * Get the {@link IBoundDefinitionModel} instance associated with the provided 114 * Java class. 115 * <p> 116 * Typically the class will have a {@link MetaschemaAssembly} or 117 * {@link MetaschemaField} annotation. 118 * 119 * @param clazz 120 * the class binding to load 121 * @return the associated class binding instance or {@code null} if the class is 122 * not bound 123 */ 124 @Nullable 125 IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz); 126 127 /** 128 * Determine the bound class for the provided XML {@link QName}. 129 * 130 * @param rootQName 131 * the root XML element's QName 132 * @return the bound class or {@code null} if not recognized 133 * @see IBindingContext#registerBindingMatcher(Class) 134 */ 135 @Nullable 136 Class<? extends IBoundObject> getBoundClassForRootXmlQName(@NonNull QName rootQName); 137 138 /** 139 * Determine the bound class for the provided JSON/YAML property/item name using 140 * any registered matchers. 141 * 142 * @param rootName 143 * the JSON/YAML property/item name 144 * @return the bound class or {@code null} if not recognized 145 * @see IBindingContext#registerBindingMatcher(Class) 146 */ 147 @Nullable 148 Class<? extends IBoundObject> getBoundClassForRootJsonName(@NonNull String rootName); 149 150 /** 151 * Get's the {@link IDataTypeAdapter} associated with the specified Java class, 152 * which is used to read and write XML, JSON, and YAML data to and from 153 * instances of that class. Thus, this adapter supports a direct binding between 154 * the Java class and structured data in one of the supported formats. Adapters 155 * are used to support bindings for simple data objects (e.g., {@link String}, 156 * {@link BigInteger}, {@link ZonedDateTime}, etc). 157 * 158 * @param <TYPE> 159 * the class type of the adapter 160 * @param clazz 161 * the Java {@link Class} for the bound type 162 * @return the adapter instance or {@code null} if the provided class is not 163 * bound 164 */ 165 @Nullable 166 <TYPE extends IDataTypeAdapter<?>> TYPE getJavaTypeAdapterInstance(@NonNull Class<TYPE> clazz); 167 168 /** 169 * Load a bound Metaschema module implemented by the provided class. 170 * <p> 171 * Also registers any associated bound classes. 172 * <p> 173 * Implementations are expected to return the same IModule instance for multiple 174 * calls to this method with the same class argument. 175 * 176 * @param clazz 177 * the class implementing a bound Metaschema module 178 * @return the loaded module 179 */ 180 @NonNull 181 IBoundModule registerModule(@NonNull Class<? extends IBoundModule> clazz); 182 183 /** 184 * Generate, compile, and load a set of generated Module annotated Java classes 185 * based on the provided Module {@code module}. 186 * 187 * @param module 188 * the Module module to generate classes for 189 * @param compilePath 190 * the path to the directory to generate classes in 191 * @return this instance 192 * @throws IOException 193 * if an error occurred while generating or loading the classes 194 */ 195 @NonNull 196 IBindingContext registerModule( 197 @NonNull IModule module, 198 @NonNull Path compilePath) throws IOException; 199 200 /** 201 * Gets a data {@link ISerializer} which can be used to write Java instance data 202 * for the provided class in the requested format. 203 * <p> 204 * The provided class must be a bound Java class with a 205 * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a 206 * {@link IBoundDefinitionModel} exists. 207 * 208 * @param <CLASS> 209 * the Java type this serializer can write data from 210 * @param format 211 * the format to serialize into 212 * @param clazz 213 * the Java data object to serialize 214 * @return the serializer instance 215 * @throws NullPointerException 216 * if any of the provided arguments, except the configuration, are 217 * {@code null} 218 * @throws IllegalArgumentException 219 * if the provided class is not bound to a Module assembly or field 220 * @throws UnsupportedOperationException 221 * if the requested format is not supported by the implementation 222 * @see #getBoundDefinitionForClass(Class) 223 */ 224 @NonNull 225 <CLASS extends IBoundObject> ISerializer<CLASS> newSerializer( 226 @NonNull Format format, 227 @NonNull Class<CLASS> clazz); 228 229 /** 230 * Gets a data {@link IDeserializer} which can be used to read Java instance 231 * data for the provided class from the requested format. 232 * <p> 233 * The provided class must be a bound Java class with a 234 * {@link MetaschemaAssembly} or {@link MetaschemaField} annotation for which a 235 * {@link IBoundDefinitionModel} exists. 236 * 237 * @param <CLASS> 238 * the Java type this deserializer can read data into 239 * @param format 240 * the format to serialize into 241 * @param clazz 242 * the Java data type to serialize 243 * @return the deserializer instance 244 * @throws NullPointerException 245 * if any of the provided arguments, except the configuration, are 246 * {@code null} 247 * @throws IllegalArgumentException 248 * if the provided class is not bound to a Module assembly or field 249 * @throws UnsupportedOperationException 250 * if the requested format is not supported by the implementation 251 * @see #getBoundDefinitionForClass(Class) 252 */ 253 @NonNull 254 <CLASS extends IBoundObject> IDeserializer<CLASS> newDeserializer( 255 @NonNull Format format, 256 @NonNull Class<CLASS> clazz); 257 258 /** 259 * Get a new {@link IBoundLoader} instance. 260 * 261 * @return the instance 262 */ 263 @NonNull 264 IBoundLoader newBoundLoader(); 265 266 /** 267 * Create a deep copy of the provided bound object. 268 * 269 * @param <CLASS> 270 * the bound object type 271 * @param other 272 * the object to copy 273 * @param parentInstance 274 * the object's parent or {@code null} 275 * @return a deep copy of the provided object 276 * @throws BindingException 277 * if an error occurred copying content between java instances 278 * @throws NullPointerException 279 * if the provided object is {@code null} 280 * @throws IllegalArgumentException 281 * if the provided class is not bound to a Module assembly or field 282 */ 283 @NonNull 284 <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject parentInstance) 285 throws BindingException; 286 287 /** 288 * Get a new single use constraint validator. 289 * 290 * @param handler 291 * the validation handler to use to process the validation results 292 * @param config 293 * the validation configuration 294 * 295 * @return the validator 296 */ 297 default IConstraintValidator newValidator( 298 @NonNull IConstraintValidationHandler handler, 299 @Nullable IConfiguration<ValidationFeature<?>> config) { 300 IBoundLoader loader = newBoundLoader(); 301 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS); 302 303 DynamicContext context = new DynamicContext(); 304 context.setDocumentLoader(loader); 305 306 DefaultConstraintValidator retval = new DefaultConstraintValidator(handler); 307 if (config != null) { 308 retval.applyConfiguration(config); 309 } 310 return retval; 311 } 312 313 /** 314 * Perform constraint validation on the provided bound object represented as an 315 * {@link IDocumentNodeItem}. 316 * 317 * @param nodeItem 318 * the node item to validate 319 * @param loader 320 * a module loader used to load and resolve referenced resources 321 * @param config 322 * the validation configuration 323 * @return the validation result 324 * @throws IllegalArgumentException 325 * if the provided class is not bound to a Module assembly or field 326 */ 327 default IValidationResult validate( 328 @NonNull IDocumentNodeItem nodeItem, 329 @NonNull IBoundLoader loader, 330 @Nullable IConfiguration<ValidationFeature<?>> config) { 331 IRootAssemblyNodeItem root = nodeItem.getRootAssemblyNodeItem(); 332 return validate(root, loader, config); 333 } 334 335 /** 336 * Perform constraint validation on the provided bound object represented as an 337 * {@link IDefinitionNodeItem}. 338 * 339 * @param nodeItem 340 * the node item to validate 341 * @param loader 342 * a module loader used to load and resolve referenced resources 343 * @param config 344 * the validation configuration 345 * @return the validation result 346 * @throws IllegalArgumentException 347 * if the provided class is not bound to a Module assembly or field 348 */ 349 default IValidationResult validate( 350 @NonNull IDefinitionNodeItem<?, ?> nodeItem, 351 @NonNull IBoundLoader loader, 352 @Nullable IConfiguration<ValidationFeature<?>> config) { 353 354 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); 355 IConstraintValidator validator = newValidator(handler, config); 356 357 DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext()); 358 dynamicContext.setDocumentLoader(loader); 359 360 validator.validate(nodeItem, dynamicContext); 361 validator.finalizeValidation(dynamicContext); 362 return handler; 363 } 364 365 /** 366 * Load and perform schema and constraint validation on the target. The 367 * constraint validation will only be performed if the schema validation passes. 368 * 369 * @param target 370 * the target to validate 371 * @param asFormat 372 * the schema format to use to validate the target 373 * @param schemaProvider 374 * provides callbacks to get the appropriate schemas 375 * @param config 376 * the validation configuration 377 * @return the validation result 378 * @throws IOException 379 * if an error occurred while reading the target 380 */ 381 default IValidationResult validate( 382 @NonNull URI target, 383 @NonNull Format asFormat, 384 @NonNull ISchemaValidationProvider schemaProvider, 385 @Nullable IConfiguration<ValidationFeature<?>> config) throws IOException { 386 387 IValidationResult retval = schemaProvider.validateWithSchema(target, asFormat); 388 389 if (retval.isPassing()) { 390 IValidationResult constraintValidationResult = validateWithConstraints(target, config); 391 retval = AggregateValidationResult.aggregate(retval, constraintValidationResult); 392 } 393 return retval; 394 } 395 396 /** 397 * Load and validate the provided {@code target} using the associated Module 398 * module constraints. 399 * 400 * @param target 401 * the file to load and validate 402 * @param config 403 * the validation configuration 404 * @return the validation results 405 * @throws IOException 406 * if an error occurred while parsing the target 407 */ 408 default IValidationResult validateWithConstraints( 409 @NonNull URI target, 410 @Nullable IConfiguration<ValidationFeature<?>> config) 411 throws IOException { 412 IBoundLoader loader = newBoundLoader(); 413 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS); 414 IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target); 415 416 return validate(nodeItem, loader, config); 417 } 418 419 interface IModuleLoaderStrategy { 420 /** 421 * Load the bound Metaschema module represented by the provided class. 422 * <p> 423 * Implementations are allowed to return a cached instance if the module has 424 * already been loaded. 425 * 426 * @param clazz 427 * the Module class 428 * @return the module 429 * @throws IllegalStateException 430 * if an error occurred while processing the associated module 431 * information 432 */ 433 @NonNull 434 IBoundModule loadModule(@NonNull Class<? extends IBoundModule> clazz); 435 436 /** 437 * Get the {@link IBoundDefinitionModel} instance associated with the provided 438 * Java class. 439 * <p> 440 * Typically the class will have a {@link MetaschemaAssembly} or 441 * {@link MetaschemaField} annotation. 442 * 443 * @param clazz 444 * the class binding to load 445 * @return the associated class binding instance or {@code null} if the class is 446 * not bound 447 */ 448 @Nullable 449 IBoundDefinitionModelComplex getBoundDefinitionForClass(@NonNull Class<? extends IBoundObject> clazz); 450 } 451 452 interface ISchemaValidationProvider { 453 454 @NonNull 455 default IValidationResult validateWithSchema(@NonNull URI target, @NonNull Format asFormat) 456 throws FileNotFoundException, IOException { 457 URL targetResource = ObjectUtils.notNull(target.toURL()); 458 459 IValidationResult retval; 460 switch (asFormat) { 461 case JSON: { 462 JSONObject json; 463 try (@SuppressWarnings("resource") InputStream is 464 = new BufferedInputStream(ObjectUtils.notNull(targetResource.openStream()))) { 465 json = new JSONObject(new JSONTokener(is)); 466 } 467 retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, target); 468 break; 469 } 470 case XML: 471 try { 472 List<Source> schemaSources = getXmlSchemas(targetResource); 473 retval = new XmlSchemaContentValidator(schemaSources).validate(target); 474 } catch (SAXException ex) { 475 throw new IOException(ex); 476 } 477 break; 478 case YAML: { 479 JSONObject json = YamlOperations.yamlToJson(YamlOperations.parseYaml(target)); 480 assert json != null; 481 retval = new JsonSchemaContentValidator(getJsonSchema(json)).validate(json, ObjectUtils.notNull(target)); 482 break; 483 } 484 default: 485 throw new UnsupportedOperationException("Unsupported format: " + asFormat.name()); 486 } 487 return retval; 488 } 489 490 /** 491 * Get a JSON schema to use for content validation. 492 * 493 * @param json 494 * the JSON content to validate 495 * 496 * @return the JSON schema 497 * @throws IOException 498 * if an error occurred while loading the schema 499 */ 500 @NonNull 501 JSONObject getJsonSchema(@NonNull JSONObject json) throws IOException; 502 503 /** 504 * Get a XML schema to use for content validation. 505 * 506 * @param targetResource 507 * the URL for the XML content to validate 508 * 509 * @return the XML schema sources 510 * @throws IOException 511 * if an error occurred while loading the schema 512 */ 513 @NonNull 514 List<Source> getXmlSchemas(@NonNull URL targetResource) throws IOException; 515 } 516 517 /** 518 * Implementations of this interface provide a means by which a bound class can 519 * be found that corresponds to an XML element, JSON property, or YAML item 520 * name. 521 */ 522 interface IBindingMatcher { 523 @SuppressWarnings("PMD.ShortMethodName") 524 @NonNull 525 static IBindingMatcher of(IBoundDefinitionModelAssembly assembly) { 526 if (!assembly.isRoot()) { 527 throw new IllegalArgumentException( 528 String.format("The provided class '%s' is not a root assembly.", assembly.getBoundClass().getName())); 529 } 530 return new RootAssemblyBindingMatcher(assembly); 531 } 532 533 /** 534 * Determine the bound class for the provided XML {@link QName}. 535 * 536 * @param rootQName 537 * the root XML element's QName 538 * @return the bound class for the XML qualified name or {@code null} if not 539 * recognized 540 */ 541 Class<? extends IBoundObject> getBoundClassForXmlQName(QName rootQName); 542 543 /** 544 * Determine the bound class for the provided JSON/YAML property/item name. 545 * 546 * @param rootName 547 * the JSON/YAML property/item name 548 * @return the bound class for the JSON property name or {@code null} if not 549 * recognized 550 */ 551 Class<? extends IBoundObject> getBoundClassForJsonName(String rootName); 552 } 553}