1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen.config;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.io.File;
12  import java.io.IOException;
13  import java.net.MalformedURLException;
14  import java.net.URI;
15  import java.net.URISyntaxException;
16  import java.net.URL;
17  import java.nio.file.Path;
18  import java.util.Collection;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.Objects;
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.function.Function;
24  
25  import dev.metaschema.core.model.IAssemblyDefinition;
26  import dev.metaschema.core.model.IFieldDefinition;
27  import dev.metaschema.core.model.IModelDefinition;
28  import dev.metaschema.core.model.IModule;
29  import dev.metaschema.core.model.INamedInstance;
30  import dev.metaschema.core.util.CollectionUtil;
31  import dev.metaschema.core.util.ObjectUtils;
32  import dev.metaschema.databind.IBindingContext;
33  import dev.metaschema.databind.codegen.ClassUtils;
34  import dev.metaschema.databind.config.binding.MetaschemaBindings;
35  import dev.metaschema.databind.io.BindingException;
36  import dev.metaschema.databind.io.Format;
37  import dev.metaschema.databind.io.IDeserializer;
38  import edu.umd.cs.findbugs.annotations.NonNull;
39  import edu.umd.cs.findbugs.annotations.Nullable;
40  
41  /**
42   * Default implementation of {@link IBindingConfiguration} that provides binding
43   * configuration for Java class generation from Metaschema modules.
44   * <p>
45   * This implementation supports loading configuration from XML files and
46   * provides namespace-to-package mappings and definition-specific binding
47   * configurations.
48   */
49  public class DefaultBindingConfiguration implements IBindingConfiguration {
50    private static final Logger LOGGER = LogManager.getLogger(DefaultBindingConfiguration.class);
51  
52    private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>();
53    // metaschema location -> ModelType -> Definition name -> IBindingConfiguration
54    private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap
55        = new ConcurrentHashMap<>();
56  
57    @Override
58    public String getPackageNameForModule(IModule module) {
59      URI namespace = module.getXmlNamespace();
60      return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString()));
61    }
62  
63    /**
64     * Retrieve the binding configuration for the provided {@code definition}.
65     * <p>
66     * This method first checks for a binding by the definition's name. If not found
67     * and the definition is inline, it also checks for a binding by the
68     * definition's path (using "/" separated ancestor names).
69     *
70     * @param definition
71     *          the definition to get the config for
72     * @return the binding configuration or {@code null} if there is not
73     *         configuration
74     */
75    @Override
76    @Nullable
77    public IDefinitionBindingConfiguration getBindingConfigurationForDefinition(
78        @NonNull IModelDefinition definition) {
79      String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString());
80      String definitionName = definition.getName();
81  
82      MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
83  
84      IDefinitionBindingConfiguration retval = null;
85      if (metaschemaConfig != null) {
86        switch (definition.getModelType()) {
87        case ASSEMBLY:
88          // First try by name
89          retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName);
90          // If not found and inline, try by path
91          if (retval == null && definition.isInline()) {
92            String path = computeDefinitionPath(definition);
93            retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(path);
94          }
95          break;
96        case FIELD:
97          // First try by name
98          retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName);
99          // 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 }