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}