1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen.typeinfo;
7   
8   import com.squareup.javapoet.ClassName;
9   
10  import org.apache.commons.lang3.StringUtils;
11  import org.apache.logging.log4j.LogManager;
12  import org.apache.logging.log4j.Logger;
13  
14  import java.util.Collections;
15  import java.util.HashSet;
16  import java.util.LinkedHashSet;
17  import java.util.List;
18  import java.util.Map;
19  import java.util.Set;
20  import java.util.concurrent.ConcurrentHashMap;
21  import java.util.concurrent.locks.Lock;
22  import java.util.concurrent.locks.ReentrantLock;
23  import java.util.stream.Collectors;
24  
25  import dev.metaschema.core.model.IAssemblyDefinition;
26  import dev.metaschema.core.model.IAssemblyInstanceGrouped;
27  import dev.metaschema.core.model.IChoiceGroupInstance;
28  import dev.metaschema.core.model.IFieldDefinition;
29  import dev.metaschema.core.model.IFieldInstanceGrouped;
30  import dev.metaschema.core.model.IModelDefinition;
31  import dev.metaschema.core.model.IModule;
32  import dev.metaschema.core.model.INamedInstance;
33  import dev.metaschema.core.model.INamedModelInstanceGrouped;
34  import dev.metaschema.core.util.ObjectUtils;
35  import dev.metaschema.databind.codegen.ClassUtils;
36  import dev.metaschema.databind.codegen.config.IBindingConfiguration;
37  import dev.metaschema.databind.codegen.config.IChoiceGroupBindingConfiguration;
38  import dev.metaschema.databind.codegen.config.IDefinitionBindingConfiguration;
39  import dev.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
40  import dev.metaschema.databind.codegen.typeinfo.def.IDefinitionTypeInfo;
41  import dev.metaschema.databind.codegen.typeinfo.def.IFieldDefinitionTypeInfo;
42  import dev.metaschema.databind.codegen.typeinfo.def.IModelDefinitionTypeInfo;
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  
45  @SuppressWarnings("PMD.CouplingBetweenObjects")
46  class DefaultTypeResolver implements ITypeResolver {
47    private static final Logger LOGGER = LogManager.getLogger(DefaultTypeResolver.class);
48  
49    private final Map<String, Set<String>> packageToClassNamesMap = new ConcurrentHashMap<>();
50    private final Lock classNameLock = new ReentrantLock();
51  
52    private final Map<IModelDefinition, ClassName> definitionToTypeMap = new ConcurrentHashMap<>();
53    private final Map<IModule, ClassName> moduleToTypeMap = new ConcurrentHashMap<>();
54    private final Map<IAssemblyDefinition, IAssemblyDefinitionTypeInfo> assemblyDefinitionToTypeInfoMap
55        = new ConcurrentHashMap<>();
56    private final Map<IFieldDefinition, IFieldDefinitionTypeInfo> fieldDefinitionToTypeInfoMap
57        = new ConcurrentHashMap<>();
58  
59    private final Lock propertyNameLock = new ReentrantLock();
60    private final Map<IDefinitionTypeInfo, Set<String>> typeInfoToPropertyNameMap = new ConcurrentHashMap<>();
61  
62    @NonNull
63    private final IBindingConfiguration bindingConfiguration;
64  
65    public DefaultTypeResolver(@NonNull IBindingConfiguration bindingConfiguration) {
66      this.bindingConfiguration = bindingConfiguration;
67    }
68  
69    @Override
70    public IBindingConfiguration getBindingConfiguration() {
71      return bindingConfiguration;
72    }
73  
74    @Override
75    public IAssemblyDefinitionTypeInfo getTypeInfo(@NonNull IAssemblyDefinition definition) {
76      return ObjectUtils.notNull(assemblyDefinitionToTypeInfoMap.computeIfAbsent(
77          definition,
78          def -> IAssemblyDefinitionTypeInfo.newTypeInfo(ObjectUtils.notNull(def),
79              this)));
80    }
81  
82    @Override
83    public IFieldDefinitionTypeInfo getTypeInfo(@NonNull IFieldDefinition definition) {
84      return ObjectUtils.notNull(fieldDefinitionToTypeInfoMap.computeIfAbsent(
85          definition,
86          def -> IFieldDefinitionTypeInfo.newTypeInfo(ObjectUtils.notNull(def),
87              this)));
88    }
89  
90    @Override
91    public IModelDefinitionTypeInfo getTypeInfo(@NonNull IModelDefinition definition) {
92      IModelDefinitionTypeInfo retval;
93      if (definition instanceof IAssemblyDefinition) {
94        retval = getTypeInfo((IAssemblyDefinition) definition);
95      } else if (definition instanceof IFieldDefinition) {
96        retval = getTypeInfo((IFieldDefinition) definition);
97      } else {
98        throw new IllegalStateException(String.format("Unknown type '%s'",
99            definition.getClass().getName()));
100     }
101     return retval;
102   }
103 
104   @Override
105   public IGroupedNamedModelInstanceTypeInfo getTypeInfo(
106       @NonNull INamedModelInstanceGrouped modelInstance,
107       @NonNull IChoiceGroupTypeInfo choiceGroupTypeInfo) {
108     IGroupedNamedModelInstanceTypeInfo retval;
109     if (modelInstance instanceof IAssemblyInstanceGrouped) {
110       retval = getTypeInfo((IAssemblyInstanceGrouped) modelInstance, choiceGroupTypeInfo);
111     } else if (modelInstance instanceof IFieldInstanceGrouped) {
112       retval = getTypeInfo((IFieldInstanceGrouped) modelInstance, choiceGroupTypeInfo);
113     } else {
114       throw new IllegalStateException(String.format("Unknown type '%s'",
115           modelInstance.getClass().getName()));
116     }
117     return retval;
118   }
119 
120   @NonNull
121   private static IGroupedAssemblyInstanceTypeInfo getTypeInfo(
122       @NonNull IAssemblyInstanceGrouped modelInstance,
123       @NonNull IChoiceGroupTypeInfo choiceGroupTypeInfo) {
124     return new GroupedAssemblyInstanceTypeInfo(modelInstance, choiceGroupTypeInfo);
125   }
126 
127   @NonNull
128   private static IGroupedFieldInstanceTypeInfo getTypeInfo(
129       @NonNull IFieldInstanceGrouped modelInstance,
130       @NonNull IChoiceGroupTypeInfo choiceGroupTypeInfo) {
131     return new GroupedFieldInstanceTypeInfo(modelInstance, choiceGroupTypeInfo);
132   }
133 
134   @NonNull
135   private ClassName getFlagContainerClassName(
136       @NonNull IModelDefinition definition,
137       @NonNull String packageName,
138       @NonNull String suggestedClassName) {
139     ClassName retval;
140     if (definition.isInline()) {
141       // this is a local definition, which means a child class needs to be generated
142       INamedInstance inlineInstance = definition.getInlineInstance();
143       IModelDefinition parentDefinition = inlineInstance.getContainingDefinition();
144       ClassName parentClassName = getClassName(parentDefinition);
145       retval = getSubclassName(parentClassName, suggestedClassName, definition);
146     } else {
147       String className = generateClassName(packageName, suggestedClassName, definition);
148       retval = ObjectUtils.notNull(ClassName.get(packageName, className));
149     }
150     return retval;
151   }
152 
153   @Override
154   public ClassName getSubclassName(
155       @NonNull ClassName parentClass,
156       @NonNull String suggestedClassName,
157       @NonNull IModelDefinition definition) {
158     String name = generateClassName(
159         ObjectUtils.notNull(parentClass.canonicalName()),
160         ClassUtils.toClassName(suggestedClassName),
161         definition);
162     return ObjectUtils.notNull(parentClass.nestedClass(name));
163   }
164 
165   @Override
166   @NonNull
167   public ClassName getClassName(@NonNull IModelDefinition definition) {
168     return ObjectUtils.notNull(definitionToTypeMap.computeIfAbsent(
169         definition,
170         def -> {
171           String packageName = getBindingConfiguration().getPackageNameForModule(def.getContainingModule());
172           String suggestedClassName = getBindingConfiguration().getClassName(definition);
173           return getFlagContainerClassName(def, packageName, suggestedClassName);
174         }));
175   }
176 
177   @Override
178   public ClassName getClassName(@NonNull INamedModelInstanceTypeInfo typeInfo) {
179     return getClassName(typeInfo.getInstance().getDefinition());
180   }
181 
182   @Override
183   public ClassName getClassName(IChoiceGroupInstance instance) {
184     IAssemblyDefinition parent = instance.getContainingDefinition();
185     IDefinitionBindingConfiguration config = getBindingConfiguration().getBindingConfigurationForDefinition(parent);
186     if (config != null) {
187       IChoiceGroupBindingConfiguration choiceConfig = config.getChoiceGroupBindings().get(instance.getGroupAsName());
188       if (choiceConfig != null && choiceConfig.getItemTypeName() != null) {
189         return ObjectUtils.notNull(ClassName.bestGuess(choiceConfig.getItemTypeName()));
190       }
191     }
192     return ObjectUtils.notNull(ClassName.get(Object.class));
193   }
194 
195   @Override
196   public ClassName getClassName(IModule module) {
197     return ObjectUtils.notNull(moduleToTypeMap.computeIfAbsent(
198         module,
199         mod -> {
200           assert mod != null;
201           String packageName = getBindingConfiguration().getPackageNameForModule(mod);
202           String className = getBindingConfiguration().getClassName(mod);
203           String classNameBase = className;
204           int index = 1;
205           classNameLock.lock();
206           try {
207             while (isClassNameClash(packageName, className)) {
208               className = classNameBase + Integer.toString(index);
209             }
210             addClassName(packageName, className);
211           } finally {
212             classNameLock.unlock();
213           }
214           return ClassName.get(packageName, className);
215         }));
216   }
217 
218   @NonNull
219   protected Set<String> getClassNamesFor(@NonNull String packageOrTypeName) {
220     classNameLock.lock();
221     try {
222       return ObjectUtils.notNull(packageToClassNamesMap.computeIfAbsent(
223           packageOrTypeName,
224           pkg -> Collections.synchronizedSet(new LinkedHashSet<>())));
225     } finally {
226       classNameLock.unlock();
227     }
228   }
229 
230   protected boolean isClassNameClash(@NonNull String packageOrTypeName, @NonNull String className) {
231     classNameLock.lock();
232     try {
233       return getClassNamesFor(packageOrTypeName).contains(className);
234     } finally {
235       classNameLock.unlock();
236     }
237   }
238 
239   protected boolean addClassName(@NonNull String packageOrTypeName, @NonNull String className) {
240     classNameLock.lock();
241     try {
242       return getClassNamesFor(packageOrTypeName).add(className);
243     } finally {
244       classNameLock.unlock();
245     }
246   }
247 
248   private String generateClassName(
249       @NonNull String packageOrTypeName,
250       @NonNull String suggestedClassName,
251       @NonNull IModelDefinition definition) {
252     @NonNull
253     String retval = suggestedClassName;
254     boolean clash = false;
255     classNameLock.lock();
256     try {
257       Set<String> classNames = getClassNamesFor(packageOrTypeName);
258       if (classNames.contains(suggestedClassName)) {
259         clash = true;
260         // first try to append the metaschema's short name
261         String metaschemaShortName = definition.getContainingModule().getShortName();
262         retval = ClassUtils.toClassName(suggestedClassName + StringUtils.capitalize(metaschemaShortName));
263       }
264 
265       String classNameBase = retval;
266       int index = 1;
267       while (classNames.contains(retval)) {
268         retval = classNameBase + Integer.toString(index++);
269       }
270       classNames.add(retval);
271     } finally {
272       classNameLock.unlock();
273     }
274 
275     if (clash && LOGGER.isWarnEnabled()) {
276       LOGGER.warn(String.format(
277           "Class name '%s', based on '%s' in '%s', clashes with another bound class. Using '%s' instead.",
278           suggestedClassName,
279           definition.getName(),
280           definition.getContainingModule().getLocation(),
281           retval));
282     }
283     return retval;
284   }
285 
286   @Override
287   public ClassName getBaseClassName(IModelDefinition definition) {
288     String className = bindingConfiguration.getQualifiedBaseClassName(definition);
289     ClassName retval = null;
290     if (className != null) {
291       retval = ClassName.bestGuess(className);
292     }
293     return retval;
294   }
295 
296   @Override
297   public List<ClassName> getSuperinterfaces(IModelDefinition definition) {
298     List<String> classNames = bindingConfiguration.getQualifiedSuperinterfaceClassNames(definition);
299     return ObjectUtils.notNull(classNames.stream()
300         .map(ClassName::bestGuess)
301         .collect(Collectors.toUnmodifiableList()));
302   }
303 
304   @Override
305   public String getPackageName(@NonNull IModule module) {
306     return bindingConfiguration.getPackageNameForModule(module);
307   }
308 
309   @Override
310   @NonNull
311   public String getPropertyName(IDefinitionTypeInfo parent, String name) {
312     propertyNameLock.lock();
313     try {
314       Set<String> propertyNames = typeInfoToPropertyNameMap.computeIfAbsent(parent, key -> new HashSet<>());
315 
316       String retval = name;
317       int index = 0;
318       while (propertyNames.contains(retval)) {
319         // append an integer value to make the name unique
320         retval = ClassUtils.toPropertyName(name + Integer.toString(++index));
321       }
322       propertyNames.add(retval);
323       return retval;
324     } finally {
325       propertyNameLock.unlock();
326     }
327   }
328 
329 }