1
2
3
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
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
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
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 }