1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.codegen.config;
7   
8   import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
9   import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
10  import gov.nist.secauto.metaschema.core.model.IModelDefinition;
11  import gov.nist.secauto.metaschema.core.model.IModule;
12  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.databind.codegen.ClassUtils;
15  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaModelBindingType;
16  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.JavaObjectDefinitionBindingType;
17  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingType;
18  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsDocument;
19  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.MetaschemaBindingsType;
20  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ModelBindingType;
21  import gov.nist.secauto.metaschema.databind.codegen.xmlbeans.ObjectDefinitionBindingType;
22  
23  import org.apache.xmlbeans.XmlException;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.nio.file.Path;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.concurrent.ConcurrentHashMap;
36  
37  import edu.umd.cs.findbugs.annotations.NonNull;
38  import edu.umd.cs.findbugs.annotations.Nullable;
39  
40  public class DefaultBindingConfiguration implements IBindingConfiguration {
41    private final Map<String, String> namespaceToPackageNameMap = new ConcurrentHashMap<>();
42    // metaschema location -> ModelType -> Definition name -> IBindingConfiguration
43    private final Map<String, MetaschemaBindingConfiguration> moduleUrlToMetaschemaBindingConfigurationMap
44        = new ConcurrentHashMap<>();
45  
46    @Override
47    public String getPackageNameForModule(IModule module) {
48      URI namespace = module.getXmlNamespace();
49      return getPackageNameForNamespace(ObjectUtils.notNull(namespace.toASCIIString()));
50    }
51  
52    /**
53     * Retrieve the binding configuration for the provided {@code definition}.
54     *
55     * @param definition
56     *          the definition to get the config for
57     * @return the binding configuration or {@code null} if there is not
58     *         configuration
59     */
60    @Nullable
61    public IDefinitionBindingConfiguration getBindingConfigurationForDefinition(
62        @NonNull IModelDefinition definition) {
63      String moduleUri = ObjectUtils.notNull(definition.getContainingModule().getLocation().toASCIIString());
64      String definitionName = definition.getName();
65  
66      MetaschemaBindingConfiguration metaschemaConfig = getMetaschemaBindingConfiguration(moduleUri);
67  
68      IDefinitionBindingConfiguration retval = null;
69      if (metaschemaConfig != null) {
70        switch (definition.getModelType()) {
71        case ASSEMBLY:
72          retval = metaschemaConfig.getAssemblyDefinitionBindingConfig(definitionName);
73          break;
74        case FIELD:
75          retval = metaschemaConfig.getFieldDefinitionBindingConfig(definitionName);
76          break;
77        default:
78          throw new UnsupportedOperationException(
79              String.format("Unsupported definition type '%s'", definition.getModelType()));
80        }
81      }
82      return retval;
83    }
84  
85    @Override
86    public String getQualifiedBaseClassName(IModelDefinition definition) {
87      IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
88      return config == null
89          ? null
90          : config.getQualifiedBaseClassName();
91    }
92  
93    @Override
94    public String getClassName(IModelDefinition definition) {
95      IDefinitionBindingConfiguration config = getBindingConfigurationForDefinition(definition);
96  
97      String retval = null;
98      if (config != null) {
99        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 }