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