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
110  String getClassName(@NonNull IModule module) {
111    // TODO: make this configurable
112    return ClassUtils.toClassName(module.getShortName() + "Module");
113  }
114
115  @Override
116  public List<String> getQualifiedSuperinterfaceClassNames(IModelDefinition definition) {
117    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
118    return config == null
119        ? CollectionUtil.emptyList()
120        : config.getInterfacesToImplement();
121  }
122
123  /**
124   * Binds an XML namespace, which is normally associated with one or more Module,
125   * with a provided Java package name.
126   *
127   * @param namespace
128   *          an XML namespace URI
129   * @param packageName
130   *          the package name to associate with the namespace
131   * @throws IllegalStateException
132   *           if the binding configuration is changing a previously changed
133   *           namespace to package binding
134   */
135  public void addModelBindingConfig(String namespace, String packageName) {
136    if (namespaceToPackageNameMap.containsKey(namespace)) {
137      String oldPackageName = namespaceToPackageNameMap.get(namespace);
138      if (!oldPackageName.equals(packageName)) {
139        throw new IllegalStateException(
140            String.format("Attempt to redefine existing package name '%s' to '%s' for namespace '%s'",
141                oldPackageName,
142                packageName,
143                namespace));
144      } // else the same package name, so do nothing
145    } else {
146      namespaceToPackageNameMap.put(namespace, packageName);
147    }
148  }
149
150  /**
151   * Based on the current binding configuration, generate a Java package name for
152   * the provided namespace. If the namespace is already mapped, such as through
153   * the use of {@link #addModelBindingConfig(String, String)}, then the provided
154   * package name will be used. If the namespace is not mapped, then the namespace
155   * URI will be translated into a Java package name.
156   *
157   * @param namespace
158   *          the namespace to generate a Java package name for
159   * @return a Java package name
160   */
161  @NonNull
162  protected String getPackageNameForNamespace(@NonNull String namespace) {
163    String packageName = namespaceToPackageNameMap.get(namespace);
164    if (packageName == null) {
165      packageName = ClassUtils.toPackageName(namespace);
166    }
167    return packageName;
168  }
169
170  /**
171   * Get the binding configuration for the provided Module.
172   *
173   * @param module
174   *          the Module module
175   * @return the configuration for the Module or {@code null} if there is no
176   *         configuration
177   */
178  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull IModule module) {
179    String moduleUri = ObjectUtils.notNull(module.getLocation().toString());
180    return getMetaschemaBindingConfiguration(moduleUri);
181
182  }
183
184  /**
185   * Get the binding configuration for the Module modulke located at the provided
186   * {@code moduleUri}.
187   *
188   * @param moduleUri
189   *          the location of the Module module
190   * @return the configuration for the Module module or {@code null} if there is
191   *         no configuration
192   */
193  @Nullable
194  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull String moduleUri) {
195    return moduleUrlToMetaschemaBindingConfigurationMap.get(moduleUri);
196  }
197
198  /**
199   * Set the binding configuration for the Module module located at the provided
200   * {@code moduleUri}.
201   *
202   * @param moduleUri
203   *          the location of the Module module
204   * @param config
205   *          the Module binding configuration
206   * @return the old configuration for the Module module or {@code null} if there
207   *         was no previous configuration
208   */
209  public MetaschemaBindingConfiguration addMetaschemaBindingConfiguration(
210      @NonNull String moduleUri,
211      @NonNull MetaschemaBindingConfiguration config) {
212    Objects.requireNonNull(moduleUri, "moduleUri");
213    Objects.requireNonNull(config, "config");
214    return moduleUrlToMetaschemaBindingConfigurationMap.put(moduleUri, config);
215  }
216
217  /**
218   * Load the binding configuration from the provided {@code file}.
219   *
220   * @param file
221   *          the configuration resource
222   * @throws IOException
223   *           if an error occurred while reading the {@code file}
224   */
225  public void load(Path file) throws IOException {
226    URL resource = file.toAbsolutePath().normalize().toUri().toURL();
227    load(resource);
228  }
229
230  /**
231   * Load the binding configuration from the provided {@code file}.
232   *
233   * @param file
234   *          the configuration resource
235   * @throws IOException
236   *           if an error occurred while reading the {@code file}
237   */
238  public void load(File file) throws IOException {
239    load(file.toPath());
240  }
241
242  /**
243   * Load the binding configuration from the provided {@code resource}.
244   *
245   * @param resource
246   *          the configuration resource
247   * @throws IOException
248   *           if an error occurred while reading the {@code resource}
249   */
250  public void load(URL resource) throws IOException {
251    MetaschemaBindingsDocument xml;
252    try {
253      xml = MetaschemaBindingsDocument.Factory.parse(resource);
254    } catch (XmlException ex) {
255      throw new IOException(ex);
256    }
257
258    MetaschemaBindingsType bindings = xml.getMetaschemaBindings();
259
260    for (ModelBindingType model : bindings.getModelBindingList()) {
261      processModelBindingConfig(model);
262    }
263
264    for (MetaschemaBindingType metaschema : bindings.getMetaschemaBindingList()) {
265      try {
266        processMetaschemaBindingConfig(resource, metaschema);
267      } catch (MalformedURLException | URISyntaxException ex) {
268        throw new IOException(ex);
269      }
270    }
271  }
272
273  private void processModelBindingConfig(ModelBindingType model) {
274    String namespace = model.getNamespace();
275
276    if (model.isSetJava()) {
277      JavaModelBindingType java = model.getJava();
278      if (java.isSetUsePackageName()) {
279        addModelBindingConfig(namespace, java.getUsePackageName());
280      }
281    }
282  }
283
284  private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindingType metaschema)
285      throws MalformedURLException, URISyntaxException {
286    String href = metaschema.getHref();
287    URL moduleUrl = new URL(configResource, href);
288    String moduleUri = ObjectUtils.notNull(moduleUrl.toURI().normalize().toString());
289
290    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
291    if (metaschemaConfig == null) {
292      metaschemaConfig = new MetaschemaBindingConfiguration();
293      addMetaschemaBindingConfiguration(moduleUri, metaschemaConfig);
294    }
295    for (ObjectDefinitionBindingType assemblyBinding : metaschema.getDefineAssemblyBindingList()) {
296      String name = ObjectUtils.requireNonNull(assemblyBinding.getName());
297      IDefinitionBindingConfiguration config = metaschemaConfig.getAssemblyDefinitionBindingConfig(name);
298      config = processDefinitionBindingConfiguration(config, assemblyBinding);
299      metaschemaConfig.addAssemblyDefinitionBindingConfig(name, config);
300    }
301
302    for (ObjectDefinitionBindingType fieldBinding : metaschema.getDefineFieldBindingList()) {
303      String name = ObjectUtils.requireNonNull(fieldBinding.getName());
304      IDefinitionBindingConfiguration config = metaschemaConfig.getFieldDefinitionBindingConfig(name);
305      config = processDefinitionBindingConfiguration(config, fieldBinding);
306      metaschemaConfig.addFieldDefinitionBindingConfig(name, config);
307    }
308  }
309
310  @NonNull
311  private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration(
312      @Nullable IDefinitionBindingConfiguration oldConfig,
313      @NonNull ObjectDefinitionBindingType objectDefinitionBinding) {
314    IMutableDefinitionBindingConfiguration config = oldConfig == null
315        ? new DefaultDefinitionBindingConfiguration()
316        : new DefaultDefinitionBindingConfiguration(oldConfig);
317
318    if (objectDefinitionBinding.isSetJava()) {
319      JavaObjectDefinitionBindingType java = objectDefinitionBinding.getJava();
320      if (java.isSetUseClassName()) {
321        config.setClassName(ObjectUtils.notNull(java.getUseClassName()));
322      }
323
324      if (java.isSetExtendBaseClass()) {
325        config.setQualifiedBaseClassName(ObjectUtils.notNull(java.getExtendBaseClass()));
326      }
327
328      for (String interfaceName : java.getImplementInterfaceList()) {
329        config.addInterfaceToImplement(ObjectUtils.notNull(interfaceName));
330      }
331    }
332    return config;
333  }
334
335  public static final class MetaschemaBindingConfiguration {
336    private final Map<String, IDefinitionBindingConfiguration> assemblyBindingConfigs = new ConcurrentHashMap<>();
337    private final Map<String, IDefinitionBindingConfiguration> fieldBindingConfigs = new ConcurrentHashMap<>();
338
339    private MetaschemaBindingConfiguration() {
340    }
341
342    /**
343     * Get the binding configuration for the {@link IAssemblyDefinition} with the
344     * provided {@code name}.
345     *
346     * @param name
347     *          the definition name
348     * @return the definition's binding configuration or {@code null} if no
349     *         configuration is provided
350     */
351    @Nullable
352    public IDefinitionBindingConfiguration getAssemblyDefinitionBindingConfig(@NonNull String name) {
353      return assemblyBindingConfigs.get(name);
354    }
355
356    /**
357     * Get the binding configuration for the {@link IFieldDefinition} with the
358     * provided {@code name}.
359     *
360     * @param name
361     *          the definition name
362     * @return the definition's binding configuration or {@code null} if no
363     *         configuration is provided
364     */
365    @Nullable
366    public IDefinitionBindingConfiguration getFieldDefinitionBindingConfig(@NonNull String name) {
367      return fieldBindingConfigs.get(name);
368    }
369
370    /**
371     * Set the binding configuration for the {@link IAssemblyDefinition} with the
372     * provided {@code name}.
373     *
374     * @param name
375     *          the definition name
376     * @param config
377     *          the new binding configuration for the definition
378     * @return the definition's old binding configuration or {@code null} if no
379     *         configuration was previously provided
380     */
381    @Nullable
382    public IDefinitionBindingConfiguration addAssemblyDefinitionBindingConfig(@NonNull String name,
383        @NonNull IDefinitionBindingConfiguration config) {
384      return assemblyBindingConfigs.put(name, config);
385    }
386
387    /**
388     * Set the binding configuration for the {@link IFieldDefinition} with the
389     * provided {@code name}.
390     *
391     * @param name
392     *          the definition name
393     * @param config
394     *          the new binding configuration for the definition
395     * @return the definition's old binding configuration or {@code null} if no
396     *         configuration was previously provided
397     */
398    @Nullable
399    public IDefinitionBindingConfiguration addFieldDefinitionBindingConfig(@NonNull String name,
400        @NonNull IDefinitionBindingConfiguration config) {
401      return fieldBindingConfigs.put(name, config);
402    }
403  }
404}