001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind.codegen.config;
007
008import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
009import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
010import gov.nist.secauto.metaschema.core.model.IModelDefinition;
011import gov.nist.secauto.metaschema.core.model.IModule;
012import gov.nist.secauto.metaschema.core.util.CollectionUtil;
013import gov.nist.secauto.metaschema.core.util.ObjectUtils;
014import gov.nist.secauto.metaschema.databind.codegen.ClassUtils;
015import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaModelBindingType;
016import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaObjectDefinitionBindingType;
017import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingType;
018import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsDocument;
019import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsType;
020import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ModelBindingType;
021import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ObjectDefinitionBindingType;
022
023import org.apache.xmlbeans.XmlException;
024
025import java.io.File;
026import java.io.IOException;
027import java.net.MalformedURLException;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.nio.file.Path;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.concurrent.ConcurrentHashMap;
036
037import edu.umd.cs.findbugs.annotations.NonNull;
038import edu.umd.cs.findbugs.annotations.Nullable;
039
040public class DefaultBindingConfiguration implements IBindingConfiguration {
041  private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>();
042  // metaschema location -> ModelType -> Definition name -> IBindingConfiguration
043  private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap
044      = new ConcurrentHashMap<>();
045
046  @Override
047  public String getPackageNameForModule(IModule module) {
048    URI namespace = module.getXmlNamespace();
049    return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString()));
050  }
051
052  /**
053   * Retrieve the binding configuration for the provided {@code definition}.
054   *
055   * @param definition
056   *          the definition to get the config for
057   * @return the binding configuration or {@code null} if there is not
058   *         configuration
059   */
060  @Nullable
061  public IDefinitionBindingConfiguration getBindingConfigurationForDefinition(
062      @NonNull IModelDefinition definition) {
063    String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString());
064    String definitionName = definition.getName();
065
066    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
067
068    IDefinitionBindingConfiguration retval = null;
069    if (metaschemaConfig != null) {
070      switch (definition.getModelType()) {
071      case ASSEMBLY:
072        retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName);
073        break;
074      case FIELD:
075        retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName);
076        break;
077      default:
078        throw new UnsupportedOperationException(
079            String.format("Unsupported definition type '%s'", definition.getModelType()));
080      }
081    }
082    return retval;
083  }
084
085  @Override
086  public String getQualifiedBaseClassName(IModelDefinition definition) {
087    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
088    return config == null
089        ? null
090        : config.getQualifiedBaseClassName();
091  }
092
093  @Override
094  public String getClassName(IModelDefinition definition) {
095    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
096
097    String retval = null;
098    if (config != null) {
099      retval = config.getClassName();
100    }
101
102    if (retval == null) {
103      retval = ClassUtils.toClassName(definition.getName());
104    }
105    return retval;
106  }
107
108  @Override
109  public @NonNull String getClassName(@NonNull IModule module) {
110    // TODO: make this configurable
111    return ClassUtils.toClassName(module.getShortName() + "Module");
112  }
113
114  @Override
115  public List<String> getQualifiedSuperinterfaceClassNames(IModelDefinition definition) {
116    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
117    return config == null
118        ? CollectionUtil.emptyList()
119        : config.getInterfacesToImplement();
120  }
121
122  /**
123   * Binds an XML namespace, which is normally associated with one or more Module,
124   * with a provided Java package name.
125   *
126   * @param namespace
127   *          an XML namespace URI
128   * @param packageName
129   *          the package name to associate with the namespace
130   * @throws IllegalStateException
131   *           if the binding configuration is changing a previously changed
132   *           namespace to package binding
133   */
134  public void addModelBindingConfig(String namespace, String packageName) {
135    if (namespaceToPackageNameMap.containsKey(namespace)) {
136      String oldPackageName = namespaceToPackageNameMap.get(namespace);
137      if (!oldPackageName.equals(packageName)) {
138        throw new IllegalStateException(
139            String.format("Attempt to redefine existing package name '%s' to '%s' for namespace '%s'",
140                oldPackageName,
141                packageName,
142                namespace));
143      } // else the same package name, so do nothing
144    } else {
145      namespaceToPackageNameMap.put(namespace, packageName);
146    }
147  }
148
149  /**
150   * Based on the current binding configuration, generate a Java package name for
151   * the provided namespace. If the namespace is already mapped, such as through
152   * the use of {@link #addModelBindingConfig(String, String)}, then the provided
153   * package name will be used. If the namespace is not mapped, then the namespace
154   * URI will be translated into a Java package name.
155   *
156   * @param namespace
157   *          the namespace to generate a Java package name for
158   * @return a Java package name
159   */
160  @NonNull
161  protected String getPackageNameForNamespace(@NonNull String namespace) {
162    String packageName = namespaceToPackageNameMap.get(namespace);
163    if (packageName == null) {
164      packageName = ClassUtils.toPackageName(namespace);
165    }
166    return packageName;
167  }
168
169  /**
170   * Get the binding configuration for the provided Module.
171   *
172   * @param module
173   *          the Module module
174   * @return the configuration for the Module or {@code null} if there is no
175   *         configuration
176   */
177  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull IModule module) {
178    String moduleUri = ObjectUtils.notNull(module.getLocation().toString());
179    return getMetaschemaBindingConfiguration(moduleUri);
180
181  }
182
183  /**
184   * Get the binding configuration for the Module modulke located at the provided
185   * {@code moduleUri}.
186   *
187   * @param moduleUri
188   *          the location of the Module module
189   * @return the configuration for the Module module or {@code null} if there is
190   *         no configuration
191   */
192  @Nullable
193  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull String moduleUri) {
194    return moduleUrlToMetaschemaBindingConfigurationMap.get(moduleUri);
195  }
196
197  /**
198   * Set the binding configuration for the Module module located at the provided
199   * {@code moduleUri}.
200   *
201   * @param moduleUri
202   *          the location of the Module module
203   * @param config
204   *          the Module binding configuration
205   * @return the old configuration for the Module module or {@code null} if there
206   *         was no previous configuration
207   */
208  public MetaschemaBindingConfiguration addMetaschemaBindingConfiguration(
209      @NonNull String moduleUri,
210      @NonNull MetaschemaBindingConfiguration config) {
211    Objects.requireNonNull(moduleUri, "moduleUri");
212    Objects.requireNonNull(config, "config");
213    return moduleUrlToMetaschemaBindingConfigurationMap.put(moduleUri, config);
214  }
215
216  /**
217   * Load the binding configuration from the provided {@code file}.
218   *
219   * @param file
220   *          the configuration resource
221   * @throws IOException
222   *           if an error occurred while reading the {@code file}
223   */
224  public void load(Path file) throws IOException {
225    URL resource = file.toAbsolutePath().normalize().toUri().toURL();
226    load(resource);
227  }
228
229  /**
230   * Load the binding configuration from the provided {@code file}.
231   *
232   * @param file
233   *          the configuration resource
234   * @throws IOException
235   *           if an error occurred while reading the {@code file}
236   */
237  public void load(File file) throws IOException {
238    load(file.toPath());
239  }
240
241  /**
242   * Load the binding configuration from the provided {@code resource}.
243   *
244   * @param resource
245   *          the configuration resource
246   * @throws IOException
247   *           if an error occurred while reading the {@code resource}
248   */
249  public void load(URL resource) throws IOException {
250    MetaschemaBindingsDocument xml;
251    try {
252      xml = MetaschemaBindingsDocument.Factory.parse(resource);
253    } catch (XmlException ex) {
254      throw new IOException(ex);
255    }
256
257    MetaschemaBindingsType bindings = xml.getMetaschemaBindings();
258
259    for (ModelBindingType model : bindings.getModelBindingList()) {
260      processModelBindingConfig(model);
261    }
262
263    for (MetaschemaBindingType metaschema : bindings.getMetaschemaBindingList()) {
264      try {
265        processMetaschemaBindingConfig(resource, metaschema);
266      } catch (MalformedURLException | URISyntaxException ex) {
267        throw new IOException(ex);
268      }
269    }
270  }
271
272  private void processModelBindingConfig(ModelBindingType model) {
273    String namespace = model.getNamespace();
274
275    if (model.isSetJava()) {
276      JavaModelBindingType java = model.getJava();
277      if (java.isSetUsePackageName()) {
278        addModelBindingConfig(namespace, java.getUsePackageName());
279      }
280    }
281  }
282
283  private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindingType metaschema)
284      throws MalformedURLException, URISyntaxException {
285    String href = metaschema.getHref();
286    URL moduleUrl = new URL(configResource, href);
287    String moduleUri = ObjectUtils.notNull(moduleUrl.toURI().normalize().toString());
288
289    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
290    if (metaschemaConfig == null) {
291      metaschemaConfig = new MetaschemaBindingConfiguration();
292      addMetaschemaBindingConfiguration(moduleUri, metaschemaConfig);
293    }
294    for (ObjectDefinitionBindingType assemblyBinding : metaschema.getDefineAssemblyBindingList()) {
295      String name = ObjectUtils.requireNonNull(assemblyBinding.getName());
296      IDefinitionBindingConfiguration config = metaschemaConfig.getAssemblyDefinitionBindingConfig(name);
297      config = processDefinitionBindingConfiguration(config, assemblyBinding);
298      metaschemaConfig.addAssemblyDefinitionBindingConfig(name, config);
299    }
300
301    for (ObjectDefinitionBindingType fieldBinding : metaschema.getDefineFieldBindingList()) {
302      String name = ObjectUtils.requireNonNull(fieldBinding.getName());
303      IDefinitionBindingConfiguration config = metaschemaConfig.getFieldDefinitionBindingConfig(name);
304      config = processDefinitionBindingConfiguration(config, fieldBinding);
305      metaschemaConfig.addFieldDefinitionBindingConfig(name, config);
306    }
307  }
308
309  @NonNull
310  private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration(
311      @Nullable IDefinitionBindingConfiguration oldConfig,
312      @NonNull ObjectDefinitionBindingType objectDefinitionBinding) {
313    IMutableDefinitionBindingConfiguration config = oldConfig == null
314        ? new DefaultDefinitionBindingConfiguration()
315        : new DefaultDefinitionBindingConfiguration(oldConfig);
316
317    if (objectDefinitionBinding.isSetJava()) {
318      JavaObjectDefinitionBindingType java = objectDefinitionBinding.getJava();
319      if (java.isSetUseClassName()) {
320        config.setClassName(ObjectUtils.notNull(java.getUseClassName()));
321      }
322
323      if (java.isSetExtendBaseClass()) {
324        config.setQualifiedBaseClassName(ObjectUtils.notNull(java.getExtendBaseClass()));
325      }
326
327      for (String interfaceName : java.getImplementInterfaceList()) {
328        config.addInterfaceToImplement(ObjectUtils.notNull(interfaceName));
329      }
330    }
331    return config;
332  }
333
334  public static final class MetaschemaBindingConfiguration {
335    private final Map<String, IDefinitionBindingConfiguration> assemblyBindingConfigs = new ConcurrentHashMap<>();
336    private final Map<String, IDefinitionBindingConfiguration> fieldBindingConfigs = new ConcurrentHashMap<>();
337
338    private MetaschemaBindingConfiguration() {
339    }
340
341    /**
342     * Get the binding configuration for the {@link IAssemblyDefinition} with the
343     * provided {@code name}.
344     *
345     * @param name
346     *          the definition name
347     * @return the definition's binding configuration or {@code null} if no
348     *         configuration is provided
349     */
350    @Nullable
351    public IDefinitionBindingConfiguration getAssemblyDefinitionBindingConfig(@NonNull String name) {
352      return assemblyBindingConfigs.get(name);
353    }
354
355    /**
356     * Get the binding configuration for the {@link IFieldDefinition} with the
357     * provided {@code name}.
358     *
359     * @param name
360     *          the definition name
361     * @return the definition's binding configuration or {@code null} if no
362     *         configuration is provided
363     */
364    @Nullable
365    public IDefinitionBindingConfiguration getFieldDefinitionBindingConfig(@NonNull String name) {
366      return fieldBindingConfigs.get(name);
367    }
368
369    /**
370     * Set the binding configuration for the {@link IAssemblyDefinition} with the
371     * provided {@code name}.
372     *
373     * @param name
374     *          the definition name
375     * @param config
376     *          the new binding configuration for the definition
377     * @return the definition's old binding configuration or {@code null} if no
378     *         configuration was previously provided
379     */
380    @Nullable
381    public IDefinitionBindingConfiguration addAssemblyDefinitionBindingConfig(@NonNull String name,
382        @NonNull IDefinitionBindingConfiguration config) {
383      return assemblyBindingConfigs.put(name, config);
384    }
385
386    /**
387     * Set the binding configuration for the {@link IFieldDefinition} with the
388     * provided {@code name}.
389     *
390     * @param name
391     *          the definition name
392     * @param config
393     *          the new binding configuration for the definition
394     * @return the definition's old binding configuration or {@code null} if no
395     *         configuration was previously provided
396     */
397    @Nullable
398    public IDefinitionBindingConfiguration addFieldDefinitionBindingConfig(@NonNull String name,
399        @NonNull IDefinitionBindingConfiguration config) {
400      return fieldBindingConfigs.put(name, config);
401    }
402  }
403}