001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package dev.metaschema.databind.codegen.config; 007 008import org.apache.logging.log4j.LogManager; 009import org.apache.logging.log4j.Logger; 010 011import java.io.File; 012import java.io.IOException; 013import java.net.MalformedURLException; 014import java.net.URI; 015import java.net.URISyntaxException; 016import java.net.URL; 017import java.nio.file.Path; 018import java.util.Collection; 019import java.util.List; 020import java.util.Map; 021import java.util.Objects; 022import java.util.concurrent.ConcurrentHashMap; 023import java.util.function.Function; 024 025import dev.metaschema.core.model.IAssemblyDefinition; 026import dev.metaschema.core.model.IFieldDefinition; 027import dev.metaschema.core.model.IModelDefinition; 028import dev.metaschema.core.model.IModule; 029import dev.metaschema.core.model.INamedInstance; 030import dev.metaschema.core.util.CollectionUtil; 031import dev.metaschema.core.util.ObjectUtils; 032import dev.metaschema.databind.IBindingContext; 033import dev.metaschema.databind.codegen.ClassUtils; 034import dev.metaschema.databind.config.binding.MetaschemaBindings; 035import dev.metaschema.databind.io.BindingException; 036import dev.metaschema.databind.io.Format; 037import dev.metaschema.databind.io.IDeserializer; 038import edu.umd.cs.findbugs.annotations.NonNull; 039import edu.umd.cs.findbugs.annotations.Nullable; 040 041/** 042 * Default implementation of {@link IBindingConfiguration} that provides binding 043 * configuration for Java class generation from Metaschema modules. 044 * <p> 045 * This implementation supports loading configuration from XML files and 046 * provides namespace-to-package mappings and definition-specific binding 047 * configurations. 048 */ 049public class DefaultBindingConfiguration implements IBindingConfiguration { 050 private static final Logger LOGGER = LogManager.getLogger(DefaultBindingConfiguration.class); 051 052 private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>(); 053 // metaschema location -> ModelType -> Definition name -> IBindingConfiguration 054 private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap 055 = new ConcurrentHashMap<>(); 056 057 @Override 058 public String getPackageNameForModule(IModule module) { 059 URI namespace = module.getXmlNamespace(); 060 return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString())); 061 } 062 063 /** 064 * Retrieve the binding configuration for the provided {@code definition}. 065 * <p> 066 * This method first checks for a binding by the definition's name. If not found 067 * and the definition is inline, it also checks for a binding by the 068 * definition's path (using "/" separated ancestor names). 069 * 070 * @param definition 071 * the definition to get the config for 072 * @return the binding configuration or {@code null} if there is not 073 * configuration 074 */ 075 @Override 076 @Nullable 077 public IDefinitionBindingConfiguration getBindingConfigurationForDefinition( 078 @NonNull IModelDefinition definition) { 079 String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString()); 080 String definitionName = definition.getName(); 081 082 MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri); 083 084 IDefinitionBindingConfiguration retval = null; 085 if (metaschemaConfig != null) { 086 switch (definition.getModelType()) { 087 case ASSEMBLY: 088 // First try by name 089 retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName); 090 // If not found and inline, try by path 091 if (retval == null && definition.isInline()) { 092 String path = computeDefinitionPath(definition); 093 retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(path); 094 } 095 break; 096 case FIELD: 097 // First try by name 098 retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName); 099 // If not found and inline, try by path 100 if (retval == null && definition.isInline()) { 101 String path = computeDefinitionPath(definition); 102 retval = metaschemaConfig.getFieldDefinitionBindingConfig(path); 103 } 104 break; 105 default: 106 throw new UnsupportedOperationException( 107 String.format("Unsupported definition type '%s'", definition.getModelType())); 108 } 109 } 110 return retval; 111 } 112 113 /** 114 * Compute the path for an inline definition. 115 * <p> 116 * The path is constructed by walking up the definition hierarchy and 117 * concatenating ancestor definition names with "/" separators. For example, an 118 * inline assembly "assembly" within "scope" within 119 * "metaschema-module-constraints" would have path "scope/assembly". 120 * 121 * @param definition 122 * the definition to compute the path for 123 * @return the computed path, or just the definition name if not inline 124 */ 125 @NonNull 126 private static String computeDefinitionPath(@NonNull IModelDefinition definition) { 127 StringBuilder path = new StringBuilder(); 128 IModelDefinition current = definition; 129 130 while (current.isInline()) { 131 if (path.length() > 0) { 132 path.insert(0, "/"); 133 } 134 path.insert(0, current.getName()); 135 136 // Walk up to the parent definition 137 INamedInstance inlineInstance = current.getInlineInstance(); 138 if (inlineInstance != null) { 139 current = inlineInstance.getContainingDefinition(); 140 } else { 141 break; 142 } 143 } 144 145 return ObjectUtils.notNull(path.toString()); 146 } 147 148 @Override 149 public String getQualifiedBaseClassName(IModelDefinition definition) { 150 IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition); 151 return config == null 152 ? null 153 : config.getQualifiedBaseClassName(); 154 } 155 156 @Override 157 public String getClassName(IModelDefinition definition) { 158 IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition); 159 160 String retval = null; 161 if (config != null) { 162 retval = config.getClassName(); 163 } 164 165 if (retval == null) { 166 retval = ClassUtils.toClassName(definition.getName()); 167 } 168 return retval; 169 } 170 171 @NonNull 172 @Override 173 public String getClassName(@NonNull IModule module) { 174 // TODO: make this configurable 175 return ClassUtils.toClassName(module.getShortName() + "Module"); 176 } 177 178 @Override 179 public List<String> getQualifiedSuperinterfaceClassNames(IModelDefinition definition) { 180 IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition); 181 return config == null 182 ? CollectionUtil.emptyList() 183 : config.getInterfacesToImplement(); 184 } 185 186 /** 187 * Get the property binding configuration for a specific property within a 188 * definition. 189 * 190 * @param definition 191 * the containing definition 192 * @param propertyName 193 * the name of the property 194 * @return the property binding configuration, or {@code null} if none is 195 * configured 196 */ 197 @Nullable 198 public IPropertyBindingConfiguration getPropertyBindingConfiguration( 199 @NonNull IModelDefinition definition, 200 @NonNull String propertyName) { 201 String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString()); 202 String definitionName = definition.getName(); 203 204 MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri); 205 if (metaschemaConfig == null) { 206 return null; 207 } 208 209 return metaschemaConfig.getPropertyBindingConfig(definitionName, propertyName); 210 } 211 212 /** 213 * Binds an XML namespace, which is normally associated with one or more Module, 214 * with a provided Java package name. 215 * 216 * @param namespace 217 * an XML namespace URI 218 * @param packageName 219 * the package name to associate with the namespace 220 * @throws IllegalStateException 221 * if the binding configuration is changing a previously changed 222 * namespace to package binding 223 */ 224 public void addModelBindingConfig(String namespace, String packageName) { 225 if (namespaceToPackageNameMap.containsKey(namespace)) { 226 String oldPackageName = namespaceToPackageNameMap.get(namespace); 227 if (!oldPackageName.equals(packageName)) { 228 throw new IllegalStateException( 229 String.format("Attempt to redefine existing package name '%s' to '%s' for namespace '%s'", 230 oldPackageName, 231 packageName, 232 namespace)); 233 } // else the same package name, so do nothing 234 } else { 235 namespaceToPackageNameMap.put(namespace, packageName); 236 } 237 } 238 239 /** 240 * Based on the current binding configuration, generate a Java package name for 241 * the provided namespace. If the namespace is already mapped, such as through 242 * the use of {@link #addModelBindingConfig(String, String)}, then the provided 243 * package name will be used. If the namespace is not mapped, then the namespace 244 * URI will be translated into a Java package name. 245 * 246 * @param namespace 247 * the namespace to generate a Java package name for 248 * @return a Java package name 249 */ 250 @NonNull 251 protected String getPackageNameForNamespace(@NonNull String namespace) { 252 String packageName = namespaceToPackageNameMap.get(namespace); 253 if (packageName == null) { 254 packageName = ClassUtils.toPackageName(namespace); 255 } 256 return packageName; 257 } 258 259 /** 260 * Get the binding configuration for the provided Module. 261 * 262 * @param module 263 * the Module module 264 * @return the configuration for the Module or {@code null} if there is no 265 * configuration 266 */ 267 protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull IModule module) { 268 String moduleUri = ObjectUtils.notNull(module.getLocation().toString()); 269 return getMetaschemaBindingConfiguration(moduleUri); 270 271 } 272 273 /** 274 * Get the binding configuration for the Module modulke located at the provided 275 * {@code moduleUri}. 276 * 277 * @param moduleUri 278 * the location of the Module module 279 * @return the configuration for the Module module or {@code null} if there is 280 * no configuration 281 */ 282 @Nullable 283 protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull String moduleUri) { 284 return moduleUrlToMetaschemaBindingConfigurationMap.get(moduleUri); 285 } 286 287 /** 288 * Set the binding configuration for the Module module located at the provided 289 * {@code moduleUri}. 290 * 291 * @param moduleUri 292 * the location of the Module module 293 * @param config 294 * the Module binding configuration 295 * @return the old configuration for the Module module or {@code null} if there 296 * was no previous configuration 297 */ 298 public MetaschemaBindingConfiguration addMetaschemaBindingConfiguration( 299 @NonNull String moduleUri, 300 @NonNull MetaschemaBindingConfiguration config) { 301 Objects.requireNonNull(moduleUri, "moduleUri"); 302 Objects.requireNonNull(config, "config"); 303 return moduleUrlToMetaschemaBindingConfigurationMap.put(moduleUri, config); 304 } 305 306 /** 307 * Load the binding configuration from the provided {@code file}. 308 * 309 * @param file 310 * the configuration resource 311 * @throws IOException 312 * if an error occurred while reading the {@code file} 313 * @throws BindingException 314 * if an error occurred while processing the binding configuration 315 */ 316 public void load(Path file) throws IOException, BindingException { 317 URL resource = ObjectUtils.notNull(file.toAbsolutePath().normalize().toUri().toURL()); 318 load(resource); 319 } 320 321 /** 322 * Load the binding configuration from the provided {@code file}. 323 * 324 * @param file 325 * the configuration resource 326 * @throws IOException 327 * if an error occurred while reading the {@code file} 328 * @throws BindingException 329 * if an error occurred while processing the binding configuration 330 */ 331 public void load(File file) throws IOException, BindingException { 332 load(file.toPath()); 333 } 334 335 /** 336 * Load the binding configuration from the provided {@code resource}. 337 * 338 * @param resource 339 * the configuration resource 340 * @throws IOException 341 * if an error occurred while reading the {@code resource} 342 * @throws BindingException 343 * if an error occurred while processing the binding configuration 344 */ 345 public void load(@NonNull URL resource) throws IOException, BindingException { 346 IBindingContext context = IBindingContext.newInstance(); 347 IDeserializer<MetaschemaBindings> deserializer = context.newDeserializer(Format.XML, MetaschemaBindings.class); 348 349 MetaschemaBindings bindings; 350 try { 351 bindings = deserializer.deserialize(resource); 352 } catch (IOException | URISyntaxException ex) { 353 throw new IOException("Failed to parse binding configuration: " + resource, ex); 354 } 355 356 List<MetaschemaBindings.ModelBinding> modelBindings = bindings.getModelBindings(); 357 for (MetaschemaBindings.ModelBinding model : modelBindings) { 358 processModelBindingConfig(model); 359 } 360 361 List<MetaschemaBindings.MetaschemaBinding> metaschemaBindings = bindings.getMetaschemaBindings(); 362 for (MetaschemaBindings.MetaschemaBinding metaschema : metaschemaBindings) { 363 try { 364 processMetaschemaBindingConfig(resource, metaschema); 365 } catch (MalformedURLException | URISyntaxException ex) { 366 throw new IOException(ex); 367 } 368 } 369 } 370 371 private void processModelBindingConfig(MetaschemaBindings.ModelBinding model) { 372 String namespace = model.getNamespace().toString(); 373 374 MetaschemaBindings.ModelBinding.Java java = model.getJava(); 375 if (java != null) { 376 String packageName = java.getUsePackageName(); 377 if (packageName != null) { 378 addModelBindingConfig(namespace, packageName); 379 } 380 } 381 } 382 383 private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindings.MetaschemaBinding metaschema) 384 throws MalformedURLException, URISyntaxException, BindingException { 385 String href = metaschema.getHref().toString(); 386 URL moduleUrl = new URL(configResource, href); 387 String moduleUri = ObjectUtils.notNull(moduleUrl.toURI().normalize().toString()); 388 389 MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri); 390 if (metaschemaConfig == null) { 391 metaschemaConfig = new MetaschemaBindingConfiguration(); 392 addMetaschemaBindingConfiguration(moduleUri, metaschemaConfig); 393 } 394 395 List<MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding> assemblyBindings 396 = metaschema.getDefineAssemblyBindings(); 397 for (MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding assemblyBinding : assemblyBindings) { 398 String name = assemblyBinding.getName(); 399 String target = assemblyBinding.getTarget(); 400 401 // Determine the lookup key - use name if provided, otherwise use target 402 String lookupKey = name != null ? name : target; 403 if (lookupKey != null) { 404 IDefinitionBindingConfiguration config = metaschemaConfig.getAssemblyDefinitionBindingConfig(lookupKey); 405 config = processDefinitionBindingConfiguration(config, assemblyBinding.getJava()); 406 metaschemaConfig.addAssemblyDefinitionBindingConfig(lookupKey, config); 407 408 // Process property bindings for this assembly 409 processAssemblyPropertyBindings(metaschemaConfig, lookupKey, assemblyBinding.getPropertyBindings()); 410 411 // Process choice group bindings for this assembly 412 processChoiceGroupBindings(config, assemblyBinding.getChoiceGroupBindings()); 413 } else { 414 LOGGER.warn("Assembly binding in metaschema '{}' has neither 'name' nor 'target' attribute; skipping", 415 moduleUri); 416 } 417 } 418 419 List<MetaschemaBindings.MetaschemaBinding.DefineFieldBinding> fieldBindings 420 = metaschema.getDefineFieldBindings(); 421 for (MetaschemaBindings.MetaschemaBinding.DefineFieldBinding fieldBinding : fieldBindings) { 422 String name = fieldBinding.getName(); 423 String target = fieldBinding.getTarget(); 424 425 // Determine the lookup key - use name if provided, otherwise use target 426 String lookupKey = name != null ? name : target; 427 if (lookupKey != null) { 428 IDefinitionBindingConfiguration config = metaschemaConfig.getFieldDefinitionBindingConfig(lookupKey); 429 config = processDefinitionBindingConfiguration(config, fieldBinding.getJava()); 430 metaschemaConfig.addFieldDefinitionBindingConfig(lookupKey, config); 431 432 // Process property bindings for this field 433 processFieldPropertyBindings(metaschemaConfig, lookupKey, fieldBinding.getPropertyBindings()); 434 } else { 435 LOGGER.warn("Field binding in metaschema '{}' has neither 'name' nor 'target' attribute; skipping", 436 moduleUri); 437 } 438 } 439 } 440 441 /** 442 * Process property bindings from a definition binding element. 443 * <p> 444 * This generic helper method consolidates the common logic for processing 445 * property bindings from both assembly and field definition bindings. 446 * 447 * @param <P> 448 * the property binding type 449 * @param <J> 450 * the Java configuration type 451 * @param metaschemaConfig 452 * the metaschema binding configuration to add property bindings to 453 * @param definitionName 454 * the name of the containing definition 455 * @param propertyBindings 456 * the list of property bindings to process 457 * @param nameAccessor 458 * function to extract the property name from a binding 459 * @param javaAccessor 460 * function to extract the Java config from a binding 461 * @param collectionClassAccessor 462 * function to extract the collection class name from a Java config 463 * @throws BindingException 464 * if the collection class is invalid or cannot be found 465 */ 466 private static <P, J> void processPropertyBindings( 467 @NonNull MetaschemaBindingConfiguration metaschemaConfig, 468 @NonNull String definitionName, 469 @Nullable List<P> propertyBindings, 470 @NonNull Function<P, String> nameAccessor, 471 @NonNull Function<P, J> javaAccessor, 472 @NonNull Function<J, String> collectionClassAccessor) throws BindingException { 473 if (propertyBindings == null) { 474 return; 475 } 476 477 for (P propertyBinding : propertyBindings) { 478 String propertyName = nameAccessor.apply(propertyBinding); 479 if (propertyName == null) { 480 continue; 481 } 482 483 J java = javaAccessor.apply(propertyBinding); 484 if (java == null) { 485 continue; 486 } 487 488 String collectionClassName = collectionClassAccessor.apply(java); 489 if (collectionClassName != null) { 490 // Validate the collection class 491 validateCollectionClass(collectionClassName, definitionName, propertyName); 492 493 IMutablePropertyBindingConfiguration config = new DefaultPropertyBindingConfiguration(); 494 config.setCollectionClassName(collectionClassName); 495 metaschemaConfig.addPropertyBindingConfig(definitionName, propertyName, config); 496 } 497 } 498 } 499 500 /** 501 * Validate that the specified collection class exists and implements a 502 * supported collection interface (Collection or Map). 503 * 504 * @param collectionClassName 505 * the fully qualified class name to validate 506 * @param definitionName 507 * the name of the containing definition (for error messages) 508 * @param propertyName 509 * the name of the property (for error messages) 510 * @throws BindingException 511 * if the class cannot be found or does not implement a supported 512 * collection interface 513 */ 514 private static void validateCollectionClass( 515 @NonNull String collectionClassName, 516 @NonNull String definitionName, 517 @NonNull String propertyName) throws BindingException { 518 Class<?> collectionClass; 519 try { 520 collectionClass = Class.forName(collectionClassName); 521 } catch (ClassNotFoundException ex) { 522 throw new BindingException(String.format( 523 "Collection class '%s' for property '%s' in definition '%s' could not be found", 524 collectionClassName, propertyName, definitionName), ex); 525 } 526 527 // Check if the class implements Collection or Map 528 if (!Collection.class.isAssignableFrom(collectionClass) && !Map.class.isAssignableFrom(collectionClass)) { 529 throw new BindingException(String.format( 530 "Collection class '%s' for property '%s' in definition '%s' must implement " 531 + "java.util.Collection or java.util.Map", 532 collectionClassName, propertyName, definitionName)); 533 } 534 } 535 536 /** 537 * Process property bindings from a define-assembly-binding element. 538 * 539 * @param metaschemaConfig 540 * the metaschema binding configuration to add property bindings to 541 * @param definitionName 542 * the name of the containing definition 543 * @param propertyBindings 544 * the list of property bindings to process 545 * @throws BindingException 546 * if the collection class is invalid or cannot be found 547 */ 548 private static void processAssemblyPropertyBindings( 549 @NonNull MetaschemaBindingConfiguration metaschemaConfig, 550 @NonNull String definitionName, 551 @Nullable List<MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.PropertyBinding> propertyBindings) 552 throws BindingException { 553 processPropertyBindings( 554 metaschemaConfig, 555 definitionName, 556 propertyBindings, 557 MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.PropertyBinding::getName, 558 MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.PropertyBinding::getJava, 559 MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.PropertyBinding.Java::getCollectionClass); 560 } 561 562 /** 563 * Process property bindings from a define-field-binding element. 564 * 565 * @param metaschemaConfig 566 * the metaschema binding configuration to add property bindings to 567 * @param definitionName 568 * the name of the containing definition 569 * @param propertyBindings 570 * the list of property bindings to process 571 * @throws BindingException 572 * if the collection class is invalid or cannot be found 573 */ 574 private static void processFieldPropertyBindings( 575 @NonNull MetaschemaBindingConfiguration metaschemaConfig, 576 @NonNull String definitionName, 577 @Nullable List<MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.PropertyBinding> propertyBindings) 578 throws BindingException { 579 processPropertyBindings( 580 metaschemaConfig, 581 definitionName, 582 propertyBindings, 583 MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.PropertyBinding::getName, 584 MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.PropertyBinding::getJava, 585 MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.PropertyBinding.Java::getCollectionClass); 586 } 587 588 /** 589 * Process choice group bindings from a define-assembly-binding element. 590 * 591 * @param config 592 * the definition binding configuration to add choice group bindings to 593 * @param choiceGroupBindings 594 * the list of choice group bindings to process 595 */ 596 private static void processChoiceGroupBindings( 597 @NonNull IDefinitionBindingConfiguration config, 598 @Nullable List< 599 MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding> choiceGroupBindings) { 600 if (choiceGroupBindings == null || !(config instanceof DefaultDefinitionBindingConfiguration)) { 601 return; 602 } 603 604 DefaultDefinitionBindingConfiguration mutableConfig = (DefaultDefinitionBindingConfiguration) config; 605 for (MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.ChoiceGroupBinding choiceGroupBinding : choiceGroupBindings) { 606 String groupAsName = choiceGroupBinding.getName(); 607 IChoiceGroupBindingConfiguration choiceGroupConfig 608 = new DefaultChoiceGroupBindingConfiguration(choiceGroupBinding); 609 mutableConfig.addChoiceGroupBinding(groupAsName, choiceGroupConfig); 610 } 611 } 612 613 @NonNull 614 private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration( 615 @Nullable IDefinitionBindingConfiguration oldConfig, 616 @Nullable MetaschemaBindings.MetaschemaBinding.DefineAssemblyBinding.Java java) { 617 IMutableDefinitionBindingConfiguration config = oldConfig == null 618 ? new DefaultDefinitionBindingConfiguration() 619 : new DefaultDefinitionBindingConfiguration(oldConfig); 620 621 if (java != null) { 622 String className = java.getUseClassName(); 623 if (className != null) { 624 config.setClassName(ObjectUtils.notNull(className)); 625 } 626 627 String baseClass = java.getExtendBaseClass(); 628 if (baseClass != null) { 629 config.setQualifiedBaseClassName(ObjectUtils.notNull(baseClass)); 630 } 631 632 List<String> interfaces = java.getImplementInterfaces(); 633 for (String interfaceName : interfaces) { 634 config.addInterfaceToImplement(Objects.requireNonNull(interfaceName, 635 "interface name cannot be null in implement-interfaces configuration")); 636 } 637 } 638 return config; 639 } 640 641 @NonNull 642 private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration( 643 @Nullable IDefinitionBindingConfiguration oldConfig, 644 @Nullable MetaschemaBindings.MetaschemaBinding.DefineFieldBinding.Java java) { 645 IMutableDefinitionBindingConfiguration config = oldConfig == null 646 ? new DefaultDefinitionBindingConfiguration() 647 : new DefaultDefinitionBindingConfiguration(oldConfig); 648 649 if (java != null) { 650 String className = java.getUseClassName(); 651 if (className != null) { 652 config.setClassName(ObjectUtils.notNull(className)); 653 } 654 655 String baseClass = java.getExtendBaseClass(); 656 if (baseClass != null) { 657 config.setQualifiedBaseClassName(ObjectUtils.notNull(baseClass)); 658 } 659 660 List<String> interfaces = java.getImplementInterfaces(); 661 for (String interfaceName : interfaces) { 662 config.addInterfaceToImplement(Objects.requireNonNull(interfaceName, 663 "interface name cannot be null in implement-interfaces configuration")); 664 } 665 } 666 return config; 667 } 668 669 /** 670 * Holds binding configurations for a specific Metaschema module. 671 * <p> 672 * This class maintains mappings from definition names to their binding 673 * configurations for both assembly and field definitions. 674 */ 675 public static final class MetaschemaBindingConfiguration { 676 private final Map<String, IDefinitionBindingConfiguration> assemblyBindingConfigs = new ConcurrentHashMap<>(); 677 private final Map<String, IDefinitionBindingConfiguration> fieldBindingConfigs = new ConcurrentHashMap<>(); 678 // Map structure: definition name -> property name -> property binding config 679 private final Map<String, Map<String, IPropertyBindingConfiguration>> propertyBindingConfigs 680 = new ConcurrentHashMap<>(); 681 682 private MetaschemaBindingConfiguration() { 683 } 684 685 /** 686 * Get the binding configuration for the {@link IAssemblyDefinition} with the 687 * provided {@code name}. 688 * 689 * @param name 690 * the definition name 691 * @return the definition's binding configuration or {@code null} if no 692 * configuration is provided 693 */ 694 @Nullable 695 public IDefinitionBindingConfiguration getAssemblyDefinitionBindingConfig(@NonNull String name) { 696 return assemblyBindingConfigs.get(name); 697 } 698 699 /** 700 * Get the binding configuration for the {@link IFieldDefinition} with the 701 * provided {@code name}. 702 * 703 * @param name 704 * the definition name 705 * @return the definition's binding configuration or {@code null} if no 706 * configuration is provided 707 */ 708 @Nullable 709 public IDefinitionBindingConfiguration getFieldDefinitionBindingConfig(@NonNull String name) { 710 return fieldBindingConfigs.get(name); 711 } 712 713 /** 714 * Set the binding configuration for the {@link IAssemblyDefinition} with the 715 * provided {@code name}. 716 * 717 * @param name 718 * the definition name 719 * @param config 720 * the new binding configuration for the definition 721 * @return the definition's old binding configuration or {@code null} if no 722 * configuration was previously provided 723 */ 724 @Nullable 725 public IDefinitionBindingConfiguration addAssemblyDefinitionBindingConfig(@NonNull String name, 726 @NonNull IDefinitionBindingConfiguration config) { 727 return assemblyBindingConfigs.put(name, config); 728 } 729 730 /** 731 * Set the binding configuration for the {@link IFieldDefinition} with the 732 * provided {@code name}. 733 * 734 * @param name 735 * the definition name 736 * @param config 737 * the new binding configuration for the definition 738 * @return the definition's old binding configuration or {@code null} if no 739 * configuration was previously provided 740 */ 741 @Nullable 742 public IDefinitionBindingConfiguration addFieldDefinitionBindingConfig(@NonNull String name, 743 @NonNull IDefinitionBindingConfiguration config) { 744 return fieldBindingConfigs.put(name, config); 745 } 746 747 /** 748 * Get the property binding configuration for a specific property within a 749 * definition. 750 * 751 * @param definitionName 752 * the name of the containing definition 753 * @param propertyName 754 * the name of the property 755 * @return the property binding configuration, or {@code null} if none is 756 * configured 757 */ 758 @Nullable 759 public IPropertyBindingConfiguration getPropertyBindingConfig( 760 @NonNull String definitionName, 761 @NonNull String propertyName) { 762 Map<String, IPropertyBindingConfiguration> defProps = propertyBindingConfigs.get(definitionName); 763 return defProps == null ? null : defProps.get(propertyName); 764 } 765 766 /** 767 * Set the property binding configuration for a specific property within a 768 * definition. 769 * 770 * @param definitionName 771 * the name of the containing definition 772 * @param propertyName 773 * the name of the property 774 * @param config 775 * the property binding configuration 776 * @return the old property binding configuration, or {@code null} if none was 777 * previously configured 778 */ 779 @Nullable 780 public IPropertyBindingConfiguration addPropertyBindingConfig( 781 @NonNull String definitionName, 782 @NonNull String propertyName, 783 @NonNull IPropertyBindingConfiguration config) { 784 return propertyBindingConfigs 785 .computeIfAbsent(definitionName, k -> new ConcurrentHashMap<>()) 786 .put(propertyName, config); 787 } 788 } 789}