1
2
3
4
5
6 package dev.metaschema.databind.model.impl;
7
8 import java.lang.reflect.Field;
9 import java.util.ArrayList;
10 import java.util.Arrays;
11 import java.util.LinkedHashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Objects;
15 import java.util.stream.Collectors;
16 import java.util.stream.Stream;
17
18 import dev.metaschema.core.model.DefaultAssemblyModelBuilder;
19 import dev.metaschema.core.model.IChoiceInstance;
20 import dev.metaschema.core.model.IContainerModelAssemblySupport;
21 import dev.metaschema.core.util.CollectionUtil;
22 import dev.metaschema.core.util.ObjectUtils;
23 import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
24 import dev.metaschema.databind.model.IBoundInstanceModel;
25 import dev.metaschema.databind.model.IBoundInstanceModelAny;
26 import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
27 import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
28 import dev.metaschema.databind.model.IBoundInstanceModelField;
29 import dev.metaschema.databind.model.IBoundInstanceModelNamed;
30 import dev.metaschema.databind.model.annotations.BoundAny;
31 import dev.metaschema.databind.model.annotations.BoundAssembly;
32 import dev.metaschema.databind.model.annotations.BoundChoice;
33 import dev.metaschema.databind.model.annotations.BoundChoiceGroup;
34 import dev.metaschema.databind.model.annotations.BoundField;
35 import dev.metaschema.databind.model.annotations.Ignore;
36 import edu.umd.cs.findbugs.annotations.NonNull;
37 import edu.umd.cs.findbugs.annotations.Nullable;
38
39
40
41
42 final class AssemblyModelGenerator {
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 private static final class BoundAssemblyModelBuilder<
67 MI extends IBoundInstanceModel<?>,
68 NMI extends IBoundInstanceModelNamed<?>,
69 FI extends IBoundInstanceModelField<?>,
70 AI extends IBoundInstanceModelAssembly,
71 CI extends IChoiceInstance,
72 CGI extends IBoundInstanceModelChoiceGroup>
73 extends DefaultAssemblyModelBuilder<MI, NMI, FI, AI, CI, CGI> {
74
75
76
77
78
79
80
81
82
83
84
85 void appendChoiceOnly(@NonNull CI instance) {
86 getChoiceInstances().add(instance);
87 }
88 }
89
90 @NonNull
91 public static IContainerModelAssemblySupport<
92 IBoundInstanceModel<?>,
93 IBoundInstanceModelNamed<?>,
94 IBoundInstanceModelField<?>,
95 IBoundInstanceModelAssembly,
96 IChoiceInstance,
97 IBoundInstanceModelChoiceGroup> of(@NonNull DefinitionAssembly containingDefinition) {
98 BoundAssemblyModelBuilder<IBoundInstanceModel<?>,
99 IBoundInstanceModelNamed<?>,
100 IBoundInstanceModelField<?>,
101 IBoundInstanceModelAssembly,
102 IChoiceInstance,
103 IBoundInstanceModelChoiceGroup> builder = new BoundAssemblyModelBuilder<>();
104
105 List<IBoundInstanceModel<?>> modelInstances = CollectionUtil.unmodifiableList(ObjectUtils.notNull(
106 getModelInstanceStream(containingDefinition, containingDefinition.getBoundClass())
107 .collect(Collectors.toUnmodifiableList())));
108
109
110 Map<String, List<ChoiceInstanceEntry>> choiceGroups = groupByChoiceId(modelInstances);
111
112
113 validateChoiceAdjacency(choiceGroups, containingDefinition.getBoundClass());
114
115
116 Map<String, BoundInstanceModelChoice> choiceInstances = new LinkedHashMap<>();
117 for (Map.Entry<String, List<ChoiceInstanceEntry>> entry : choiceGroups.entrySet()) {
118 String choiceId = entry.getKey();
119 List<IBoundInstanceModelNamed<?>> instances = ObjectUtils.notNull(entry.getValue().stream()
120 .map(ChoiceInstanceEntry::getInstance)
121 .collect(Collectors.toList()));
122 choiceInstances.put(choiceId, new BoundInstanceModelChoice(
123 ObjectUtils.notNull(choiceId), containingDefinition, instances));
124 }
125
126 for (IBoundInstanceModel<?> instance : modelInstances) {
127 if (instance instanceof IBoundInstanceModelNamed) {
128 IBoundInstanceModelNamed<?> named = (IBoundInstanceModelNamed<?>) instance;
129 if (instance instanceof IBoundInstanceModelField) {
130 builder.append((IBoundInstanceModelField<?>) named);
131 } else if (instance instanceof IBoundInstanceModelAssembly) {
132 builder.append((IBoundInstanceModelAssembly) named);
133 }
134 } else if (instance instanceof IBoundInstanceModelChoiceGroup) {
135 IBoundInstanceModelChoiceGroup choiceGroup = (IBoundInstanceModelChoiceGroup) instance;
136 builder.append(choiceGroup);
137 }
138 }
139
140
141
142 for (BoundInstanceModelChoice choice : choiceInstances.values()) {
143 assert choice != null;
144 builder.appendChoiceOnly(choice);
145 }
146
147
148 IBoundInstanceModelAny anyInstance
149 = findBoundAnyInstance(containingDefinition, containingDefinition.getBoundClass());
150 if (anyInstance != null) {
151 builder.setAnyInstance(anyInstance);
152 }
153
154 return builder.buildAssembly();
155 }
156
157
158
159
160
161
162
163
164 @NonNull
165 private static Map<String, List<ChoiceInstanceEntry>> groupByChoiceId(
166 @NonNull List<IBoundInstanceModel<?>> modelInstances) {
167 Map<String, List<ChoiceInstanceEntry>> choiceGroups = new LinkedHashMap<>();
168
169 int index = 0;
170 for (IBoundInstanceModel<?> instance : modelInstances) {
171 if (instance instanceof IBoundInstanceModelNamed) {
172 IBoundInstanceModelNamed<?> named = (IBoundInstanceModelNamed<?>) instance;
173 Field field = named.getField();
174 BoundChoice annotation = field.getAnnotation(BoundChoice.class);
175 if (annotation != null) {
176 choiceGroups
177 .computeIfAbsent(annotation.choiceId(), k -> new ArrayList<>())
178 .add(new ChoiceInstanceEntry(index, named));
179 }
180 }
181 index++;
182 }
183
184 return choiceGroups;
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198 private static void validateChoiceAdjacency(
199 @NonNull Map<String, List<ChoiceInstanceEntry>> choiceGroups,
200 @NonNull Class<?> boundClass) {
201 for (Map.Entry<String, List<ChoiceInstanceEntry>> entry : choiceGroups.entrySet()) {
202 String choiceId = entry.getKey();
203 List<ChoiceInstanceEntry> instances = entry.getValue();
204
205 if (instances.size() > 1) {
206
207 for (int i = 1; i < instances.size(); i++) {
208 int prevIndex = instances.get(i - 1).getIndex();
209 int currIndex = instances.get(i).getIndex();
210 if (currIndex != prevIndex + 1) {
211 throw new IllegalStateException(String.format(
212 "Choice fields with choiceId '%s' are not adjacent in class '%s'. "
213 + "All fields in a choice must be declared consecutively.",
214 choiceId,
215 boundClass.getName()));
216 }
217 }
218 }
219 }
220 }
221
222
223
224
225 private static final class ChoiceInstanceEntry {
226 private final int index;
227 @NonNull
228 private final IBoundInstanceModelNamed<?> instance;
229
230 ChoiceInstanceEntry(int index, @NonNull IBoundInstanceModelNamed<?> instance) {
231 this.index = index;
232 this.instance = instance;
233 }
234
235 int getIndex() {
236 return index;
237 }
238
239 @NonNull
240 IBoundInstanceModelNamed<?> getInstance() {
241 return instance;
242 }
243 }
244
245 private static IBoundInstanceModel<?> newBoundModelInstance(
246 @NonNull Field field,
247 @NonNull IBoundDefinitionModelAssembly definition) {
248 IBoundInstanceModel<?> retval = null;
249 if (field.isAnnotationPresent(BoundAssembly.class)) {
250 retval = IBoundInstanceModelAssembly.newInstance(field, definition);
251 } else if (field.isAnnotationPresent(BoundField.class)) {
252 retval = IBoundInstanceModelField.newInstance(field, definition);
253 } else if (field.isAnnotationPresent(BoundChoiceGroup.class)) {
254 retval = IBoundInstanceModelChoiceGroup.newInstance(field, definition);
255 }
256 return retval;
257 }
258
259 @NonNull
260 private static Stream<IBoundInstanceModel<?>> getModelInstanceStream(
261 @NonNull IBoundDefinitionModelAssembly definition,
262 @NonNull Class<?> clazz) {
263
264 Stream<IBoundInstanceModel<?>> superInstances;
265 Class<?> superClass = clazz.getSuperclass();
266 if (superClass == null) {
267 superInstances = Stream.empty();
268 } else {
269
270 superInstances = getModelInstanceStream(definition, superClass);
271 }
272
273 return ObjectUtils.notNull(Stream.concat(superInstances, Arrays.stream(clazz.getDeclaredFields())
274
275 .filter(field -> !field.isAnnotationPresent(Ignore.class))
276
277 .filter(field -> field.isAnnotationPresent(BoundField.class)
278 || field.isAnnotationPresent(BoundAssembly.class)
279 || field.isAnnotationPresent(BoundChoiceGroup.class))
280 .map(field -> {
281 assert field != null;
282
283 IBoundInstanceModel<?> retval = newBoundModelInstance(field, definition);
284 if (retval == null) {
285 throw new IllegalStateException(
286 String.format("The field '%s' on class '%s' is not bound", field.getName(), clazz.getName()));
287 }
288 return retval;
289 })
290 .filter(Objects::nonNull)));
291 }
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307 @Nullable
308 private static IBoundInstanceModelAny findBoundAnyInstance(
309 @NonNull IBoundDefinitionModelAssembly containingDefinition,
310 @NonNull Class<?> clazz) {
311 IBoundInstanceModelAny result = null;
312
313
314 Class<?> superClass = clazz.getSuperclass();
315 if (superClass != null) {
316 result = findBoundAnyInstance(containingDefinition, superClass);
317 }
318
319
320 for (Field field : clazz.getDeclaredFields()) {
321 if (field.isAnnotationPresent(BoundAny.class)) {
322 if (result != null) {
323 throw new IllegalStateException(String.format(
324 "Multiple @BoundAny fields found in class hierarchy of '%s'."
325 + " Only one @BoundAny field is allowed per assembly.",
326 containingDefinition.getBoundClass().getName()));
327 }
328 result = IBoundInstanceModelAny.newInstance(field, containingDefinition);
329 }
330 }
331
332 return result;
333 }
334
335 private AssemblyModelGenerator() {
336
337 }
338 }