001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.databind.codegen.config;
028
029import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
030import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
031import gov.nist.secauto.metaschema.core.model.IFlagContainer;
032import gov.nist.secauto.metaschema.core.model.IModule;
033import gov.nist.secauto.metaschema.core.util.ObjectUtils;
034import gov.nist.secauto.metaschema.databind.codegen.ClassUtils;
035import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaModelBindingType;
036import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaObjectDefinitionBindingType;
037import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingType;
038import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsDocument;
039import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsType;
040import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ModelBindingType;
041import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ObjectDefinitionBindingType;
042
043import org.apache.xmlbeans.XmlException;
044
045import java.io.File;
046import java.io.IOException;
047import java.net.MalformedURLException;
048import java.net.URI;
049import java.net.URISyntaxException;
050import java.net.URL;
051import java.nio.file.Path;
052import java.util.Map;
053import java.util.Objects;
054import java.util.concurrent.ConcurrentHashMap;
055
056import edu.umd.cs.findbugs.annotations.NonNull;
057import edu.umd.cs.findbugs.annotations.Nullable;
058
059public class DefaultBindingConfiguration implements IBindingConfiguration {
060  private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>();
061  // metaschema location -> ModelType -> Definition Name -> IBindingConfiguration
062  private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap
063      = new ConcurrentHashMap<>();
064
065  @Override
066  public String getPackageNameForModule(IModule module) {
067    URI namespace = module.getXmlNamespace();
068    return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString()));
069  }
070
071  /**
072   * Retrieve the binding configuration for the provided {@code definition}.
073   *
074   * @param definition
075   *          the definition to get the config for
076   * @return the binding configuration or {@code null} if there is not
077   *         configuration
078   */
079  @Nullable
080  public IDefinitionBindingConfiguration getBindingConfigurationForDefinition(
081      @NonNull IFlagContainer definition) {
082    String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toString());
083    String definitionName = definition.getName();
084
085    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
086
087    IDefinitionBindingConfiguration retval = null;
088    if (metaschemaConfig != null) {
089      switch (definition.getModelType()) {
090      case ASSEMBLY:
091        retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName);
092        break;
093      case FIELD:
094        retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName);
095        break;
096      default:
097        throw new UnsupportedOperationException(
098            String.format("Unsupported definition type '%s'", definition.getModelType()));
099      }
100    }
101    return retval;
102  }
103
104  @Override
105  public String getQualifiedBaseClassName(IFlagContainer definition) {
106    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
107
108    String retval = null;
109    if (config != null) {
110      retval = config.getQualifiedBaseClassName();
111    }
112    return retval;
113  }
114
115  @Override
116  public String getClassName(IFlagContainer definition) {
117    IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
118
119    String retval = null;
120    if (config != null) {
121      retval = config.getClassName();
122    }
123
124    if (retval == null) {
125      retval = ClassUtils.toClassName(definition.getName());
126    }
127    return retval;
128  }
129
130  @Override
131  public @NonNull String getClassName(@NonNull IModule module) {
132    // TODO: make this configurable
133    return ClassUtils.toClassName(module.getShortName() + "Module");
134  }
135
136  /**
137   * Binds an XML namespace, which is normally associated with one or more Module,
138   * with a provided Java package name.
139   *
140   * @param namespace
141   *          an XML namespace URI
142   * @param packageName
143   *          the package name to associate with the namespace
144   * @throws IllegalStateException
145   *           if the binding configuration is changing a previously changed
146   *           namespace to package binding
147   */
148  public void addModelBindingConfig(String namespace, String packageName) {
149    if (namespaceToPackageNameMap.containsKey(namespace)) {
150      String oldPackageName = namespaceToPackageNameMap.get(namespace);
151      if (!oldPackageName.equals(packageName)) {
152        throw new IllegalStateException(
153            String.format("Attempt to redefine existing package name '%s' to '%s' for namespace '%s'",
154                oldPackageName,
155                packageName,
156                namespace));
157      } // else the same package name, so do nothing
158    } else {
159      namespaceToPackageNameMap.put(namespace, packageName);
160    }
161  }
162
163  /**
164   * Based on the current binding configuration, generate a Java package name for
165   * the provided namespace. If the namespace is already mapped, such as through
166   * the use of {@link #addModelBindingConfig(String, String)}, then the provided
167   * package name will be used. If the namespace is not mapped, then the namespace
168   * URI will be translated into a Java package name.
169   *
170   * @param namespace
171   *          the namespace to generate a Java package name for
172   * @return a Java package name
173   */
174  @NonNull
175  protected String getPackageNameForNamespace(@NonNull String namespace) {
176    String packageName = namespaceToPackageNameMap.get(namespace);
177    if (packageName == null) {
178      packageName = ClassUtils.toPackageName(namespace);
179    }
180    return packageName;
181  }
182
183  /**
184   * Get the binding configuration for the provided Module.
185   *
186   * @param module
187   *          the Module module
188   * @return the configuration for the Module or {@code null} if there is no
189   *         configuration
190   */
191  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull IModule module) {
192    String moduleUri = ObjectUtils.notNull(module.getLocation().toString());
193    return getMetaschemaBindingConfiguration(moduleUri);
194
195  }
196
197  /**
198   * Get the binding configuration for the Module modulke located at the provided
199   * {@code moduleUri}.
200   *
201   * @param moduleUri
202   *          the location of the Module module
203   * @return the configuration for the Module module or {@code null} if there is
204   *         no configuration
205   */
206  @Nullable
207  protected MetaschemaBindingConfiguration getMetaschemaBindingConfiguration(@NonNull String moduleUri) {
208    return moduleUrlToMetaschemaBindingConfigurationMap.get(moduleUri);
209  }
210
211  /**
212   * Set the binding configuration for the Module module located at the provided
213   * {@code moduleUri}.
214   *
215   * @param moduleUri
216   *          the location of the Module module
217   * @param config
218   *          the Module binding configuration
219   * @return the old configuration for the Module module or {@code null} if there
220   *         was no previous configuration
221   */
222  public MetaschemaBindingConfiguration addMetaschemaBindingConfiguration(
223      @NonNull String moduleUri,
224      @NonNull MetaschemaBindingConfiguration config) {
225    Objects.requireNonNull(moduleUri, "moduleUri");
226    Objects.requireNonNull(config, "config");
227    return moduleUrlToMetaschemaBindingConfigurationMap.put(moduleUri, config);
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(Path file) throws IOException {
239    URL resource = file.toUri().toURL();
240    load(resource);
241  }
242
243  /**
244   * Load the binding configuration from the provided {@code file}.
245   *
246   * @param file
247   *          the configuration resource
248   * @throws IOException
249   *           if an error occurred while reading the {@code file}
250   */
251  public void load(File file) throws IOException {
252    URL resource = file.toURI().toURL();
253    load(resource);
254  }
255
256  /**
257   * Load the binding configuration from the provided {@code resource}.
258   *
259   * @param resource
260   *          the configuration resource
261   * @throws IOException
262   *           if an error occurred while reading the {@code resource}
263   */
264  public void load(URL resource) throws IOException {
265    MetaschemaBindingsDocument xml;
266    try {
267      xml = MetaschemaBindingsDocument.Factory.parse(resource);
268    } catch (XmlException ex) {
269      throw new IOException(ex);
270    }
271
272    MetaschemaBindingsType bindings = xml.getMetaschemaBindings();
273
274    for (ModelBindingType model : bindings.getModelBindingList()) {
275      processModelBindingConfig(model);
276    }
277
278    for (MetaschemaBindingType metaschema : bindings.getMetaschemaBindingList()) {
279      try {
280        processMetaschemaBindingConfig(resource, metaschema);
281      } catch (MalformedURLException | URISyntaxException ex) {
282        throw new IOException(ex);
283      }
284    }
285  }
286
287  private void processModelBindingConfig(ModelBindingType model) {
288    String namespace = model.getNamespace();
289
290    if (model.isSetJava()) {
291      JavaModelBindingType java = model.getJava();
292      if (java.isSetUsePackageName()) {
293        addModelBindingConfig(namespace, java.getUsePackageName());
294      }
295    }
296  }
297
298  private void processMetaschemaBindingConfig(URL configResource, MetaschemaBindingType metaschema)
299      throws MalformedURLException, URISyntaxException {
300    String href = metaschema.getHref();
301    URL moduleUrl = new URL(configResource, href);
302    String moduleUri = ObjectUtils.notNull(moduleUrl.toURI().toString());
303
304    MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
305    if (metaschemaConfig == null) {
306      metaschemaConfig = new MetaschemaBindingConfiguration();
307      addMetaschemaBindingConfiguration(moduleUri, metaschemaConfig);
308    }
309    for (ObjectDefinitionBindingType assemblyBinding : metaschema.getDefineAssemblyBindingList()) {
310      String name = ObjectUtils.requireNonNull(assemblyBinding.getName());
311      IDefinitionBindingConfiguration config = metaschemaConfig.getAssemblyDefinitionBindingConfig(name);
312      config = processDefinitionBindingConfiguration(config, assemblyBinding);
313      metaschemaConfig.addAssemblyDefinitionBindingConfig(name, config);
314    }
315
316    for (ObjectDefinitionBindingType fieldBinding : metaschema.getDefineFieldBindingList()) {
317      String name = ObjectUtils.requireNonNull(fieldBinding.getName());
318      IDefinitionBindingConfiguration config = metaschemaConfig.getFieldDefinitionBindingConfig(name);
319      config = processDefinitionBindingConfiguration(config, fieldBinding);
320      metaschemaConfig.addFieldDefinitionBindingConfig(name, config);
321    }
322  }
323
324  @NonNull
325  private static IMutableDefinitionBindingConfiguration processDefinitionBindingConfiguration(
326      @Nullable IDefinitionBindingConfiguration oldConfig,
327      @NonNull ObjectDefinitionBindingType objectDefinitionBinding) {
328    IMutableDefinitionBindingConfiguration config;
329    if (oldConfig != null) {
330      config = new DefaultDefinitionBindingConfiguration(oldConfig);
331    } else {
332      config = new DefaultDefinitionBindingConfiguration();
333    }
334
335    if (objectDefinitionBinding.isSetJava()) {
336      JavaObjectDefinitionBindingType java = objectDefinitionBinding.getJava();
337      if (java.isSetUseClassName()) {
338        config.setClassName(ObjectUtils.notNull(java.getUseClassName()));
339      }
340
341      if (java.isSetExtendBaseClass()) {
342        config.setQualifiedBaseClassName(ObjectUtils.notNull(java.getExtendBaseClass()));
343      }
344
345      for (String interfaceName : java.getImplementInterfaceList()) {
346        config.addInterfaceToImplement(ObjectUtils.notNull(interfaceName));
347      }
348    }
349    return config;
350  }
351
352  public static final class MetaschemaBindingConfiguration {
353    private final Map<String, IDefinitionBindingConfiguration> assemblyBindingConfigs = new ConcurrentHashMap<>();
354    private final Map<String, IDefinitionBindingConfiguration> fieldBindingConfigs = new ConcurrentHashMap<>();
355
356    private MetaschemaBindingConfiguration() {
357    }
358
359    /**
360     * Get the binding configuration for the {@link IAssemblyDefinition} with the
361     * provided {@code name}.
362     *
363     * @param name
364     *          the definition name
365     * @return the definition's binding configuration or {@code null} if no
366     *         configuration is provided
367     */
368    @Nullable
369    public IDefinitionBindingConfiguration getAssemblyDefinitionBindingConfig(@NonNull String name) {
370      return assemblyBindingConfigs.get(name);
371    }
372
373    /**
374     * Get the binding configuration for the {@link IFieldDefinition} with the
375     * provided {@code name}.
376     *
377     * @param name
378     *          the definition name
379     * @return the definition's binding configuration or {@code null} if no
380     *         configuration is provided
381     */
382    @Nullable
383    public IDefinitionBindingConfiguration getFieldDefinitionBindingConfig(@NonNull String name) {
384      return fieldBindingConfigs.get(name);
385    }
386
387    /**
388     * Set the binding configuration for the {@link IAssemblyDefinition} 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 addAssemblyDefinitionBindingConfig(@NonNull String name,
400        @NonNull IDefinitionBindingConfiguration config) {
401      return assemblyBindingConfigs.put(name, config);
402    }
403
404    /**
405     * Set the binding configuration for the {@link IFieldDefinition} with the
406     * provided {@code name}.
407     *
408     * @param name
409     *          the definition name
410     * @param config
411     *          the new binding configuration for the definition
412     * @return the definition's old binding configuration or {@code null} if no
413     *         configuration was previously provided
414     */
415    @Nullable
416    public IDefinitionBindingConfiguration addFieldDefinitionBindingConfig(@NonNull String name,
417        @NonNull IDefinitionBindingConfiguration config) {
418      return fieldBindingConfigs.put(name, config);
419    }
420  }
421}