DefaultBindingConfiguration.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.databind.codegen.config;

import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
import gov.nist.secauto.metaschema.core.model.IModelDefinition;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.codegen.ClassUtils;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaModelBindingType;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaObjectDefinitionBindingType;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingType;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsDocument;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsType;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ModelBindingType;
import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ObjectDefinitionBindingType;

import org.apache.xmlbeans.XmlException;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

public class DefaultBindingConfiguration implements IBindingConfiguration {
  private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>();
  // metaschema location -> ModelType -> Definition name -> IBindingConfiguration
  private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap
      = new ConcurrentHashMap<>();

  @Override
  public String getPackageNameForModule(IModule module) {
    URI namespace = module.getXmlNamespace();
    return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString()));
  }

  /**
   * Retrieve the binding configuration for the provided {@code definition}.
   *
   * @param definition
   *          the definition to get the config for
   * @return the binding configuration or {@code null} if there is not
   *         configuration
   */
  @Nullable
  public IDefinitionBindingConfiguration getBindingConfigurationForDefinition(
      @NonNull IModelDefinition definition) {
    String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString());
    String definitionName = definition.getName();

    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);

    IDefinitionBindingConfiguration retval = null;
    if (metaschemaConfig != null) {
      switch (definition.getModelType()) {
      case ASSEMBLY:
        retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName);
        break;
      case FIELD:
        retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName);
        break;
      default:
        throw new UnsupportedOperationException(
            String.format("Unsupported definition type '%s'", definition.getModelType()));
      }
    }
    return retval;
  }

  @Override
  public String getQualifiedBaseClassName(IModelDefinition definition) {
    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
    return config == null
        ? null
        : config.getQualifiedBaseClassName();
  }

  @Override
  public String getClassName(IModelDefinition definition) {
    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);

    String retval = null;
    if (config != null) {
      retval = config.getClassName();
    }

    if (retval == null) {
      retval = ClassUtils.toClassName(definition.getName());
    }
    return retval;
  }

  @Override
  public @NonNull
  String getClassName(@NonNull IModule module) {
    // TODO: make this configurable
    return ClassUtils.toClassName(module.getShortName() + "Module");
  }

  @Override
  public List<String> getQualifiedSuperinterfaceClassNames(IModelDefinition definition) {
    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
    return config == null
        ? CollectionUtil.emptyList()
        : config.getInterfacesToImplement();
  }

  /**
   * Binds an XML namespace, which is normally associated with one or more Module,
   * with a provided Java package name.
   *
   * @param namespace
   *          an XML namespace URI
   * @param packageName
   *          the package name to associate with the namespace
   * @throws IllegalStateException
   *           if the binding configuration is changing a previously changed
   *           namespace to package binding
   */
  public void addModelBindingConfig(String namespace, String packageName) {
    if (namespaceToPackageNameMap.containsKey(namespace)) {
      String oldPackageName = namespaceToPackageNameMap.get(namespace);
      if (!oldPackageName.equals(packageName)) {
        throw new IllegalStateException(
            String.format("Attempt to redefine existing package name '%s' to '%s' for namespace '%s'",
                oldPackageName,
                packageName,
                namespace));
      } // else the same package name, so do nothing
    } else {
      namespaceToPackageNameMap.put(namespace, packageName);
    }
  }

  /**
   * Based on the current binding configuration, generate a Java package name for
   * the provided namespace. If the namespace is already mapped, such as through
   * the use of {@link #addModelBindingConfig(String, String)}, then the provided
   * package name will be used. If the namespace is not mapped, then the namespace
   * URI will be translated into a Java package name.
   *
   * @param namespace
   *          the namespace to generate a Java package name for
   * @return a Java package name
   */
  @NonNull
  protected String getPackageNameForNamespace(@NonNull String namespace) {
    String packageName = namespaceToPackageNameMap.get(namespace);
    if (packageName == null) {
      packageName = ClassUtils.toPackageName(namespace);
    }
    return packageName;
  }

  /**
   * Get the binding configuration for the provided Module.
   *
   * @param module
   *          the Module module
   * @return the configuration for the Module or {@code null} if there is no
   *         configuration
   */
  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull IModule module) {
    String moduleUri = ObjectUtils.notNull(module.getLocation().toString());
    return getMetaschemaBindingConfiguration(moduleUri);

  }

  /**
   * Get the binding configuration for the Module modulke located at the provided
   * {@code moduleUri}.
   *
   * @param moduleUri
   *          the location of the Module module
   * @return the configuration for the Module module or {@code null} if there is
   *         no configuration
   */
  @Nullable
  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull String moduleUri) {
    return moduleUrlToMetaschemaBindingConfigurationMap.get(moduleUri);
  }

  /**
   * Set the binding configuration for the Module module located at the provided
   * {@code moduleUri}.
   *
   * @param moduleUri
   *          the location of the Module module
   * @param config
   *          the Module binding configuration
   * @return the old configuration for the Module module or {@code null} if there
   *         was no previous configuration
   */
  public MetaschemaBindingConfiguration addMetaschemaBindingConfiguration(
      @NonNull String moduleUri,
      @NonNull MetaschemaBindingConfiguration config) {
    Objects.requireNonNull(moduleUri, "moduleUri");
    Objects.requireNonNull(config, "config");
    return moduleUrlToMetaschemaBindingConfigurationMap.put(moduleUri, config);
  }

  /**
   * Load the binding configuration from the provided {@code file}.
   *
   * @param file
   *          the configuration resource
   * @throws IOException
   *           if an error occurred while reading the {@code file}
   */
  public void load(Path file) throws IOException {
    URL resource = file.toAbsolutePath().normalize().toUri().toURL();
    load(resource);
  }

  /**
   * Load the binding configuration from the provided {@code file}.
   *
   * @param file
   *          the configuration resource
   * @throws IOException
   *           if an error occurred while reading the {@code file}
   */
  public void load(File file) throws IOException {
    load(file.toPath());
  }

  /**
   * Load the binding configuration from the provided {@code resource}.
   *
   * @param resource
   *          the configuration resource
   * @throws IOException
   *           if an error occurred while reading the {@code resource}
   */
  public void load(URL resource) throws IOException {
    MetaschemaBindingsDocument xml;
    try {
      xml = MetaschemaBindingsDocument.Factory.parse(resource);
    } catch (XmlException ex) {
      throw new IOException(ex);
    }

    MetaschemaBindingsType bindings = xml.getMetaschemaBindings();

    for (ModelBindingType model : bindings.getModelBindingList()) {
      processModelBindingConfig(model);
    }

    for (MetaschemaBindingType metaschema : bindings.getMetaschemaBindingList()) {
      try {
        processMetaschemaBindingConfig(resource, metaschema);
      } catch (MalformedURLException | URISyntaxException ex) {
        throw new IOException(ex);
      }
    }
  }

  private void processModelBindingConfig(ModelBindingType model) {
    String namespace = model.getNamespace();

    if (model.isSetJava()) {
      JavaModelBindingType java = model.getJava();
      if (java.isSetUsePackageName()) {
        addModelBindingConfig(namespace, java.getUsePackageName());
      }
    }
  }

  private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindingType metaschema)
      throws MalformedURLException, URISyntaxException {
    String href = metaschema.getHref();
    URL moduleUrl = new URL(configResource, href);
    String moduleUri = ObjectUtils.notNull(moduleUrl.toURI().normalize().toString());

    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
    if (metaschemaConfig == null) {
      metaschemaConfig = new MetaschemaBindingConfiguration();
      addMetaschemaBindingConfiguration(moduleUri, metaschemaConfig);
    }
    for (ObjectDefinitionBindingType assemblyBinding : metaschema.getDefineAssemblyBindingList()) {
      String name = ObjectUtils.requireNonNull(assemblyBinding.getName());
      IDefinitionBindingConfiguration config = metaschemaConfig.getAssemblyDefinitionBindingConfig(name);
      config = processDefinitionBindingConfiguration(config, assemblyBinding);
      metaschemaConfig.addAssemblyDefinitionBindingConfig(name, config);
    }

    for (ObjectDefinitionBindingType fieldBinding : metaschema.getDefineFieldBindingList()) {
      String name = ObjectUtils.requireNonNull(fieldBinding.getName());
      IDefinitionBindingConfiguration config = metaschemaConfig.getFieldDefinitionBindingConfig(name);
      config = processDefinitionBindingConfiguration(config, fieldBinding);
      metaschemaConfig.addFieldDefinitionBindingConfig(name, config);
    }
  }

  @NonNull
  private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration(
      @Nullable IDefinitionBindingConfiguration oldConfig,
      @NonNull ObjectDefinitionBindingType objectDefinitionBinding) {
    IMutableDefinitionBindingConfiguration config = oldConfig == null
        ? new DefaultDefinitionBindingConfiguration()
        : new DefaultDefinitionBindingConfiguration(oldConfig);

    if (objectDefinitionBinding.isSetJava()) {
      JavaObjectDefinitionBindingType java = objectDefinitionBinding.getJava();
      if (java.isSetUseClassName()) {
        config.setClassName(ObjectUtils.notNull(java.getUseClassName()));
      }

      if (java.isSetExtendBaseClass()) {
        config.setQualifiedBaseClassName(ObjectUtils.notNull(java.getExtendBaseClass()));
      }

      for (String interfaceName : java.getImplementInterfaceList()) {
        config.addInterfaceToImplement(ObjectUtils.notNull(interfaceName));
      }
    }
    return config;
  }

  public static final class MetaschemaBindingConfiguration {
    private final Map<String, IDefinitionBindingConfiguration> assemblyBindingConfigs = new ConcurrentHashMap<>();
    private final Map<String, IDefinitionBindingConfiguration> fieldBindingConfigs = new ConcurrentHashMap<>();

    private MetaschemaBindingConfiguration() {
    }

    /**
     * Get the binding configuration for the {@link IAssemblyDefinition} with the
     * provided {@code name}.
     *
     * @param name
     *          the definition name
     * @return the definition's binding configuration or {@code null} if no
     *         configuration is provided
     */
    @Nullable
    public IDefinitionBindingConfiguration getAssemblyDefinitionBindingConfig(@NonNull String name) {
      return assemblyBindingConfigs.get(name);
    }

    /**
     * Get the binding configuration for the {@link IFieldDefinition} with the
     * provided {@code name}.
     *
     * @param name
     *          the definition name
     * @return the definition's binding configuration or {@code null} if no
     *         configuration is provided
     */
    @Nullable
    public IDefinitionBindingConfiguration getFieldDefinitionBindingConfig(@NonNull String name) {
      return fieldBindingConfigs.get(name);
    }

    /**
     * Set the binding configuration for the {@link IAssemblyDefinition} with the
     * provided {@code name}.
     *
     * @param name
     *          the definition name
     * @param config
     *          the new binding configuration for the definition
     * @return the definition's old binding configuration or {@code null} if no
     *         configuration was previously provided
     */
    @Nullable
    public IDefinitionBindingConfiguration addAssemblyDefinitionBindingConfig(@NonNull String name,
        @NonNull IDefinitionBindingConfiguration config) {
      return assemblyBindingConfigs.put(name, config);
    }

    /**
     * Set the binding configuration for the {@link IFieldDefinition} with the
     * provided {@code name}.
     *
     * @param name
     *          the definition name
     * @param config
     *          the new binding configuration for the definition
     * @return the definition's old binding configuration or {@code null} if no
     *         configuration was previously provided
     */
    @Nullable
    public IDefinitionBindingConfiguration addFieldDefinitionBindingConfig(@NonNull String name,
        @NonNull IDefinitionBindingConfiguration config) {
      return fieldBindingConfigs.put(name, config);
    }
  }
}