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
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 }