1
2
3
4
5
6 package dev.metaschema.databind.io;
7
8 import java.io.IOException;
9 import java.util.ArrayList;
10 import java.util.Collection;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Set;
16 import java.util.stream.Collectors;
17
18 import dev.metaschema.core.model.IAssemblyInstance;
19 import dev.metaschema.core.model.IBoundObject;
20 import dev.metaschema.core.model.IChoiceInstance;
21 import dev.metaschema.core.model.IFieldInstance;
22 import dev.metaschema.core.model.IFlagInstance;
23 import dev.metaschema.core.model.IModelInstance;
24 import dev.metaschema.core.model.INamedModelInstanceAbsolute;
25 import dev.metaschema.core.util.ObjectUtils;
26 import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
27 import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
28 import dev.metaschema.databind.model.IBoundProperty;
29 import edu.umd.cs.findbugs.annotations.NonNull;
30 import edu.umd.cs.findbugs.annotations.Nullable;
31
32
33
34
35
36 public abstract class AbstractProblemHandler implements IProblemHandler {
37 private final boolean validateRequiredFields;
38
39
40
41
42
43
44 protected AbstractProblemHandler() {
45 this(true);
46 }
47
48
49
50
51
52
53
54
55 protected AbstractProblemHandler(boolean validateRequiredFields) {
56 this.validateRequiredFields = validateRequiredFields;
57 }
58
59
60
61
62
63
64
65 protected boolean isValidateRequiredFields() {
66 return validateRequiredFields;
67 }
68
69 @Override
70 public void handleMissingInstances(
71 IBoundDefinitionModelComplex parentDefinition,
72 IBoundObject targetObject,
73 Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
74
75 handleMissingInstances(parentDefinition, targetObject, unhandledInstances, null);
76 }
77
78 @Override
79 public void handleMissingInstances(
80 IBoundDefinitionModelComplex parentDefinition,
81 IBoundObject targetObject,
82 Collection<? extends IBoundProperty<?>> unhandledInstances,
83 @Nullable ValidationContext context) throws IOException {
84 if (isValidateRequiredFields()) {
85 validateRequiredFields(parentDefinition, unhandledInstances, context);
86 }
87 applyDefaults(targetObject, unhandledInstances);
88 }
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 protected void validateRequiredFields(
108 @NonNull IBoundDefinitionModelComplex parentDefinition,
109 @NonNull Collection<? extends IBoundProperty<?>> unhandledInstances,
110 @Nullable ValidationContext context) throws IOException {
111
112
113 Set<String> unhandledNames = new HashSet<>();
114 for (IBoundProperty<?> instance : unhandledInstances) {
115 assert instance != null;
116 unhandledNames.add(getInstanceName(instance, context));
117 }
118
119
120 Map<String, IChoiceInstance> instanceToChoice = buildInstanceToChoiceMap(parentDefinition, context);
121
122
123 List<IBoundProperty<?>> missingFlags = new ArrayList<>();
124 List<IBoundProperty<?>> missingFields = new ArrayList<>();
125 List<IBoundProperty<?>> missingAssemblies = new ArrayList<>();
126
127 for (IBoundProperty<?> instance : unhandledInstances) {
128 assert instance != null;
129 if (isRequiredAndMissingDefault(instance)) {
130 String instanceName = getInstanceName(instance, context);
131 IChoiceInstance choice = instanceToChoice.get(instanceName);
132
133 if (choice != null) {
134
135 if (!isChoiceSatisfied(choice, unhandledNames, context)) {
136
137 addToTypeList(instance, missingFlags, missingFields, missingAssemblies);
138 }
139
140 } else {
141
142 addToTypeList(instance, missingFlags, missingFields, missingAssemblies);
143 }
144 }
145 }
146
147 if (!missingFlags.isEmpty() || !missingFields.isEmpty() || !missingAssemblies.isEmpty()) {
148 throw new IOException(formatMissingPropertiesMessage(
149 parentDefinition, missingFlags, missingFields, missingAssemblies, context));
150 }
151 }
152
153
154
155
156
157
158
159
160
161
162
163
164
165 private static void addToTypeList(
166 @NonNull IBoundProperty<?> instance,
167 @NonNull List<IBoundProperty<?>> missingFlags,
168 @NonNull List<IBoundProperty<?>> missingFields,
169 @NonNull List<IBoundProperty<?>> missingAssemblies) {
170 if (instance instanceof IFlagInstance) {
171 missingFlags.add(instance);
172 } else if (instance instanceof IFieldInstance) {
173 missingFields.add(instance);
174 } else if (instance instanceof IAssemblyInstance) {
175 missingAssemblies.add(instance);
176 } else {
177
178 missingFields.add(instance);
179 }
180 }
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 @NonNull
198 private static String formatMissingPropertiesMessage(
199 @NonNull IBoundDefinitionModelComplex parentDefinition,
200 @NonNull List<IBoundProperty<?>> missingFlags,
201 @NonNull List<IBoundProperty<?>> missingFields,
202 @NonNull List<IBoundProperty<?>> missingAssemblies,
203 @Nullable ValidationContext context) {
204
205 StringBuilder message = new StringBuilder();
206 String parentName = getParentName(parentDefinition, context);
207 Format format = context != null ? context.getFormat() : Format.JSON;
208
209 int totalMissing = missingFlags.size() + missingFields.size() + missingAssemblies.size();
210
211 if (totalMissing == 1) {
212
213 IBoundProperty<?> missing = ObjectUtils.notNull(!missingFlags.isEmpty() ? missingFlags.get(0)
214 : !missingFields.isEmpty() ? missingFields.get(0)
215 : missingAssemblies.get(0));
216 String type = getPropertyTypeName(missing, format, false);
217 String name = getInstanceName(missing, context);
218 message.append(String.format("Missing required %s '%s' in '%s'", type, name, parentName));
219 } else if (hasSingleType(missingFlags, missingFields, missingAssemblies)) {
220
221 List<IBoundProperty<?>> list = !missingFlags.isEmpty() ? missingFlags
222 : !missingFields.isEmpty() ? missingFields
223 : missingAssemblies;
224 String type = getPropertyTypeName(ObjectUtils.notNull(list.get(0)), format, true);
225 String names = formatNameList(list, context);
226 message.append(String.format("Missing required %s in '%s': %s", type, parentName, names));
227 } else {
228
229 message.append(String.format("Missing required properties in '%s':", parentName));
230 if (!missingFlags.isEmpty()) {
231 message.append("\n ").append(getFormatPropertyGroupLabel(true, format))
232 .append(": ").append(formatNameList(missingFlags, context));
233 }
234 if (!missingFields.isEmpty()) {
235 message.append("\n ").append(getFormatPropertyGroupLabel(false, format))
236 .append(": ").append(formatNameList(missingFields, context));
237 }
238 if (!missingAssemblies.isEmpty()) {
239 message.append("\n ").append(getFormatPropertyGroupLabel(false, format))
240 .append(": ").append(formatNameList(missingAssemblies, context));
241 }
242 }
243
244
245 if (context != null) {
246 String location = context.formatLocation();
247 if (!location.isEmpty()) {
248 message.append("\n Location: ").append(location);
249 }
250 String path = context.getPath();
251 if (!"/".equals(path) && !path.isEmpty()) {
252 message.append("\n Path: ").append(path);
253 }
254 }
255
256 return ObjectUtils.notNull(message.toString());
257 }
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277 @NonNull
278 private static String getFormatPropertyTypeName(boolean isFlag, @NonNull Format format, boolean plural) {
279 switch (format) {
280 case XML:
281 if (isFlag) {
282 return plural ? "attributes" : "attribute";
283 }
284 return plural ? "elements" : "element";
285 case JSON:
286 case YAML:
287 return plural ? "properties" : "property";
288 default:
289
290 return plural ? "properties" : "property";
291 }
292 }
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307 @NonNull
308 private static String getFormatPropertyGroupLabel(boolean isFlags, @NonNull Format format) {
309 String typeName = getFormatPropertyTypeName(isFlags, format, true);
310 return Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
311 }
312
313
314
315
316 private static boolean hasSingleType(
317 List<IBoundProperty<?>> flags,
318 List<IBoundProperty<?>> fields,
319 List<IBoundProperty<?>> assemblies) {
320 int nonEmpty = 0;
321 if (!flags.isEmpty()) {
322 nonEmpty++;
323 }
324 if (!fields.isEmpty()) {
325 nonEmpty++;
326 }
327 if (!assemblies.isEmpty()) {
328 nonEmpty++;
329 }
330 return nonEmpty == 1;
331 }
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347 @NonNull
348 private static String getPropertyTypeName(
349 @NonNull IBoundProperty<?> instance,
350 @NonNull Format format,
351 boolean plural) {
352 boolean isFlag = instance instanceof IFlagInstance;
353 return getFormatPropertyTypeName(isFlag, format, plural);
354 }
355
356
357
358
359 @NonNull
360 private static String formatNameList(
361 @NonNull List<IBoundProperty<?>> instances,
362 @Nullable ValidationContext context) {
363 return ObjectUtils.notNull(instances.stream()
364 .map(i -> getInstanceName(ObjectUtils.notNull(i), context))
365 .collect(Collectors.joining(", ")));
366 }
367
368
369
370
371
372
373
374
375
376
377 @NonNull
378 private static String getParentName(
379 @NonNull IBoundDefinitionModelComplex parentDefinition,
380 @Nullable ValidationContext context) {
381
382 return parentDefinition.getEffectiveName();
383 }
384
385
386
387
388
389
390
391
392
393
394 @NonNull
395 private static Map<String, IChoiceInstance> buildInstanceToChoiceMap(
396 @NonNull IBoundDefinitionModelComplex parentDefinition,
397 @Nullable ValidationContext context) {
398 Map<String, IChoiceInstance> result = new HashMap<>();
399
400 if (parentDefinition instanceof IBoundDefinitionModelAssembly) {
401 IBoundDefinitionModelAssembly assembly = (IBoundDefinitionModelAssembly) parentDefinition;
402 for (IChoiceInstance choice : assembly.getChoiceInstances()) {
403 for (INamedModelInstanceAbsolute modelInstance : choice.getNamedModelInstances()) {
404
405 result.put(modelInstance.getEffectiveName(), choice);
406 }
407 }
408 }
409
410 return result;
411 }
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432 private static boolean isChoiceSatisfied(
433 @NonNull IChoiceInstance choice,
434 @NonNull Set<String> unhandledNames,
435 @Nullable ValidationContext context) {
436
437 for (INamedModelInstanceAbsolute modelInstance : choice.getNamedModelInstances()) {
438
439 String name = modelInstance.getEffectiveName();
440 if (!unhandledNames.contains(name)) {
441
442 return true;
443 }
444 }
445
446
447
448 return choice.getMinOccurs() == 0;
449 }
450
451
452
453
454
455
456
457
458 private static boolean isRequiredAndMissingDefault(@NonNull IBoundProperty<?> instance) {
459
460 Object defaultValue = instance.getResolvedDefaultValue();
461 if (defaultValue != null) {
462
463 return false;
464 }
465
466
467 if (instance instanceof IFlagInstance) {
468 return ((IFlagInstance) instance).isRequired();
469 } else if (instance instanceof IModelInstance) {
470 return ((IModelInstance) instance).getMinOccurs() > 0;
471 }
472
473
474 return false;
475 }
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491 @NonNull
492 private static String getInstanceName(
493 @NonNull IBoundProperty<?> instance,
494 @Nullable ValidationContext context) {
495
496 if (instance instanceof IFlagInstance) {
497 return ((IFlagInstance) instance).getEffectiveName();
498 } else if (instance instanceof IFieldInstance) {
499 return ((IFieldInstance) instance).getEffectiveName();
500 } else if (instance instanceof IAssemblyInstance) {
501 return ((IAssemblyInstance) instance).getEffectiveName();
502 }
503
504 return instance.getJsonName();
505 }
506
507
508
509
510
511
512
513
514
515
516
517
518
519 protected static void applyDefaults(
520 @NonNull Object targetObject,
521 @NonNull Collection<? extends IBoundProperty<?>> unhandledInstances) throws IOException {
522 for (IBoundProperty<?> instance : unhandledInstances) {
523 assert instance != null;
524 Object value = instance.getResolvedDefaultValue();
525 if (value != null) {
526 instance.setValue(targetObject, value);
527 }
528 }
529 }
530 }