1
2
3
4
5
6 package dev.metaschema.databind.codegen.impl;
7
8 import com.squareup.javapoet.AnnotationSpec;
9 import com.squareup.javapoet.AnnotationSpec.Builder;
10
11 import org.apache.logging.log4j.LogBuilder;
12 import org.apache.logging.log4j.LogManager;
13 import org.apache.logging.log4j.Logger;
14
15 import java.lang.reflect.Method;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.regex.Pattern;
19
20 import dev.metaschema.core.datatype.IDataTypeAdapter;
21 import dev.metaschema.core.datatype.markup.MarkupLine;
22 import dev.metaschema.core.datatype.markup.MarkupMultiline;
23 import dev.metaschema.core.metapath.DynamicContext;
24 import dev.metaschema.core.metapath.item.ISequence;
25 import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
26 import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
27 import dev.metaschema.core.metapath.item.node.INodeItemFactory;
28 import dev.metaschema.core.model.IAssemblyDefinition;
29 import dev.metaschema.core.model.IFlagDefinition;
30 import dev.metaschema.core.model.IModelDefinition;
31 import dev.metaschema.core.model.INamedInstance;
32 import dev.metaschema.core.model.INamedModelInstanceAbsolute;
33 import dev.metaschema.core.model.constraint.IAllowedValue;
34 import dev.metaschema.core.model.constraint.IAllowedValuesConstraint;
35 import dev.metaschema.core.model.constraint.ICardinalityConstraint;
36 import dev.metaschema.core.model.constraint.IConstraint;
37 import dev.metaschema.core.model.constraint.IExpectConstraint;
38 import dev.metaschema.core.model.constraint.IIndexConstraint;
39 import dev.metaschema.core.model.constraint.IIndexHasKeyConstraint;
40 import dev.metaschema.core.model.constraint.IKeyField;
41 import dev.metaschema.core.model.constraint.ILet;
42 import dev.metaschema.core.model.constraint.IMatchesConstraint;
43 import dev.metaschema.core.model.constraint.IReportConstraint;
44 import dev.metaschema.core.model.constraint.IUniqueConstraint;
45 import dev.metaschema.core.qname.IEnhancedQName;
46 import dev.metaschema.core.util.ObjectUtils;
47 import dev.metaschema.databind.model.annotations.AllowedValue;
48 import dev.metaschema.databind.model.annotations.AllowedValues;
49 import dev.metaschema.databind.model.annotations.AssemblyConstraints;
50 import dev.metaschema.databind.model.annotations.Expect;
51 import dev.metaschema.databind.model.annotations.HasCardinality;
52 import dev.metaschema.databind.model.annotations.Index;
53 import dev.metaschema.databind.model.annotations.IndexHasKey;
54 import dev.metaschema.databind.model.annotations.IsUnique;
55 import dev.metaschema.databind.model.annotations.KeyField;
56 import dev.metaschema.databind.model.annotations.Let;
57 import dev.metaschema.databind.model.annotations.Matches;
58 import dev.metaschema.databind.model.annotations.Report;
59 import dev.metaschema.databind.model.annotations.ValueConstraints;
60 import edu.umd.cs.findbugs.annotations.NonNull;
61
62
63
64
65 @SuppressWarnings({
66 "PMD.GodClass", "PMD.CouplingBetweenObjects"
67 })
68 public final class AnnotationGenerator {
69 private static final Logger LOGGER = LogManager.getLogger(AnnotationGenerator.class);
70
71 private AnnotationGenerator() {
72
73 }
74
75
76
77
78
79
80
81
82
83
84
85 public static Object getDefaultValue(Class<?> annotation, String member) {
86 Method method;
87 try {
88 method = annotation.getDeclaredMethod(member);
89 } catch (NoSuchMethodException ex) {
90 throw new IllegalArgumentException(ex);
91 }
92 Object retval = null;
93 try {
94 retval = method.getDefaultValue();
95 } catch (@SuppressWarnings("unused") TypeNotPresentException ex) {
96
97 }
98 return retval;
99 }
100
101 private static void buildConstraint(Class<?> annotationType, AnnotationSpec.Builder annotation,
102 IConstraint constraint) {
103 String id = constraint.getId();
104 if (id != null) {
105 annotation.addMember("id", "$S", id);
106 }
107
108 String formalName = constraint.getFormalName();
109 if (formalName != null) {
110 annotation.addMember("formalName", "$S", formalName);
111 }
112
113 MarkupLine description = constraint.getDescription();
114 if (description != null) {
115 annotation.addMember("description", "$S", description.toMarkdown());
116 }
117
118 annotation.addMember("level", "$T.$L", IConstraint.Level.class, constraint.getLevel());
119
120 String target = constraint.getTarget().getPath();
121 if (!target.equals(getDefaultValue(annotationType, "target"))) {
122 annotation.addMember("target", "$S", target);
123 }
124 }
125
126
127
128
129
130
131
132
133
134 public static void buildValueConstraints(
135 @NonNull AnnotationSpec.Builder builder,
136 @NonNull IFlagDefinition definition) {
137
138 Map<IEnhancedQName, ? extends ILet> lets = definition.getLetExpressions();
139 if (!lets.isEmpty() || !definition.getConstraints().isEmpty()) {
140 AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class);
141 assert annotation != null;
142
143 applyLetAssignments(annotation, lets);
144 applyAllowedValuesConstraints(annotation, definition.getAllowedValuesConstraints());
145 applyIndexHasKeyConstraints(annotation, definition.getIndexHasKeyConstraints());
146 applyMatchesConstraints(annotation, definition.getMatchesConstraints());
147 applyExpectConstraints(annotation, definition.getExpectConstraints());
148 applyReportConstraints(annotation, definition.getReportConstraints());
149
150 builder.addMember("valueConstraints", "$L", annotation.build());
151 }
152 }
153
154
155
156
157
158
159
160
161
162 public static void buildValueConstraints(
163 @NonNull AnnotationSpec.Builder builder,
164 @NonNull IModelDefinition definition) {
165
166 Map<IEnhancedQName, ? extends ILet> lets = definition.getLetExpressions();
167 List<? extends IAllowedValuesConstraint> allowedValues = definition.getAllowedValuesConstraints();
168 List<? extends IIndexHasKeyConstraint> indexHasKey = definition.getIndexHasKeyConstraints();
169 List<? extends IMatchesConstraint> matches = definition.getMatchesConstraints();
170 List<? extends IExpectConstraint> expects = definition.getExpectConstraints();
171 List<? extends IReportConstraint> reports = definition.getReportConstraints();
172
173 if (!lets.isEmpty() || !allowedValues.isEmpty() || !indexHasKey.isEmpty() || !matches.isEmpty()
174 || !expects.isEmpty() || !reports.isEmpty()) {
175 AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class);
176 assert annotation != null;
177
178 applyLetAssignments(annotation, lets);
179 applyAllowedValuesConstraints(annotation, allowedValues);
180 applyIndexHasKeyConstraints(annotation, indexHasKey);
181 applyMatchesConstraints(annotation, matches);
182 applyExpectConstraints(annotation, expects);
183 applyReportConstraints(annotation, reports);
184
185 builder.addMember("valueConstraints", "$L", annotation.build());
186 }
187 }
188
189
190
191
192
193
194
195
196
197 public static void buildAssemblyConstraints(
198 @NonNull AnnotationSpec.Builder builder,
199 @NonNull IAssemblyDefinition definition) {
200
201 List<? extends IIndexConstraint> index = definition.getIndexConstraints();
202 List<? extends IUniqueConstraint> unique = definition.getUniqueConstraints();
203 List<? extends ICardinalityConstraint> cardinality = definition.getHasCardinalityConstraints();
204
205 if (!index.isEmpty() || !unique.isEmpty() || !cardinality.isEmpty()) {
206 AnnotationSpec.Builder annotation = ObjectUtils.notNull(AnnotationSpec.builder(AssemblyConstraints.class));
207
208 applyIndexConstraints(annotation, index);
209 applyUniqueConstraints(annotation, unique);
210 applyHasCardinalityConstraints(definition, annotation, cardinality);
211
212 builder.addMember("modelConstraints", "$L", annotation.build());
213 }
214 }
215
216 private static void applyLetAssignments(
217 @NonNull AnnotationSpec.Builder annotation,
218 @NonNull Map<IEnhancedQName, ? extends ILet> lets) {
219 for (ILet let : lets.values()) {
220 AnnotationSpec.Builder letAnnotation = AnnotationSpec.builder(Let.class);
221 letAnnotation.addMember("name", "$S", let.getName());
222 letAnnotation.addMember("target", "$S", let.getValueExpression().getPath());
223
224 MarkupMultiline remarks = let.getRemarks();
225 if (remarks != null) {
226 letAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
227 }
228
229 annotation.addMember("lets", "$L", letAnnotation.build());
230 }
231 }
232
233 private static void applyAllowedValuesConstraints(
234 @NonNull AnnotationSpec.Builder annotation,
235 @NonNull List<? extends IAllowedValuesConstraint> constraints) {
236 for (IAllowedValuesConstraint constraint : constraints) {
237 assert constraint != null;
238 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(AllowedValues.class);
239 buildConstraint(AllowedValues.class, constraintAnnotation, constraint);
240
241 boolean isAllowedOther = constraint.isAllowedOther();
242 if (isAllowedOther != (boolean) getDefaultValue(AllowedValues.class, "allowOthers")) {
243 constraintAnnotation.addMember("allowOthers", "$L", isAllowedOther);
244 }
245
246 for (IAllowedValue value : constraint.getAllowedValues().values()) {
247 AnnotationSpec.Builder valueAnnotation = AnnotationSpec.builder(AllowedValue.class);
248
249 valueAnnotation.addMember("value", "$S", value.getValue());
250 valueAnnotation.addMember("description", "$S", value.getDescription().toMarkdown());
251
252 String deprecatedVersion = value.getDeprecatedVersion();
253 if (deprecatedVersion != null) {
254 valueAnnotation.addMember("deprecatedVersion", "$S", deprecatedVersion);
255 }
256
257 constraintAnnotation.addMember("values", "$L", valueAnnotation.build());
258 }
259
260 MarkupMultiline remarks = constraint.getRemarks();
261 if (remarks != null) {
262 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
263 }
264 annotation.addMember("allowedValues", "$L", constraintAnnotation.build());
265 }
266 }
267
268 private static void applyIndexHasKeyConstraints(
269 @NonNull AnnotationSpec.Builder annotation,
270 @NonNull List<? extends IIndexHasKeyConstraint> constraints) {
271 for (IIndexHasKeyConstraint constraint : constraints) {
272 assert constraint != null;
273 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(IndexHasKey.class);
274 buildConstraint(IndexHasKey.class, constraintAnnotation, constraint);
275
276 constraintAnnotation.addMember("indexName", "$S", constraint.getIndexName());
277
278 buildKeyFields(constraintAnnotation, constraint.getKeyFields());
279
280 String message = constraint.getMessage();
281 if (message != null) {
282 constraintAnnotation.addMember("message", "$S", message);
283 }
284
285 MarkupMultiline remarks = constraint.getRemarks();
286 if (remarks != null) {
287 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
288 }
289
290 annotation.addMember("indexHasKey", "$L", constraintAnnotation.build());
291 }
292 }
293
294 private static void buildKeyFields(
295 @NonNull Builder constraintAnnotation,
296 @NonNull List<? extends IKeyField> keyFields) {
297 for (IKeyField key : keyFields) {
298 assert key != null;
299 AnnotationSpec.Builder keyAnnotation = AnnotationSpec.builder(KeyField.class);
300
301 String target = key.getTarget().getPath();
302 if (!target.equals(getDefaultValue(KeyField.class, "target"))) {
303 keyAnnotation.addMember("target", "$S", target);
304 }
305
306 Pattern pattern = key.getPattern();
307 if (pattern != null) {
308 keyAnnotation.addMember("pattern", "$S", pattern.pattern());
309 }
310
311 MarkupMultiline remarks = key.getRemarks();
312 if (remarks != null) {
313 keyAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
314 }
315
316 constraintAnnotation.addMember("keyFields", "$L", keyAnnotation.build());
317 }
318 }
319
320 private static void applyMatchesConstraints(
321 @NonNull AnnotationSpec.Builder annotation,
322 @NonNull List<? extends IMatchesConstraint> constraints) {
323 for (IMatchesConstraint constraint : constraints) {
324 assert constraint != null;
325
326 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Matches.class);
327 buildConstraint(Matches.class, constraintAnnotation, constraint);
328
329 Pattern pattern = constraint.getPattern();
330 if (pattern != null) {
331 constraintAnnotation.addMember("pattern", "$S", pattern.pattern());
332 }
333
334 IDataTypeAdapter<?> dataType = constraint.getDataType();
335 if (dataType != null) {
336 constraintAnnotation.addMember("typeAdapter", "$T.class", dataType.getClass());
337 }
338
339 String message = constraint.getMessage();
340 if (message != null) {
341 constraintAnnotation.addMember("message", "$S", message);
342 }
343
344 MarkupMultiline remarks = constraint.getRemarks();
345 if (remarks != null) {
346 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
347 }
348 annotation.addMember("matches", "$L", constraintAnnotation.build());
349 }
350 }
351
352 private static void applyExpectConstraints(
353 @NonNull AnnotationSpec.Builder annotation,
354 @NonNull List<? extends IExpectConstraint> constraints) {
355 for (IExpectConstraint constraint : constraints) {
356 assert constraint != null;
357
358 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Expect.class);
359
360 buildConstraint(Expect.class, constraintAnnotation, constraint);
361
362 constraintAnnotation.addMember("test", "$S", constraint.getTest().getPath());
363
364 if (constraint.getMessage() != null) {
365 constraintAnnotation.addMember("message", "$S", constraint.getMessage());
366 }
367
368 MarkupMultiline remarks = constraint.getRemarks();
369 if (remarks != null) {
370 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
371 }
372
373 annotation.addMember("expect", "$L", constraintAnnotation.build());
374 }
375 }
376
377 private static void applyReportConstraints(
378 @NonNull AnnotationSpec.Builder annotation,
379 @NonNull List<? extends IReportConstraint> constraints) {
380 for (IReportConstraint constraint : constraints) {
381 assert constraint != null;
382
383 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Report.class);
384
385 buildConstraint(Report.class, constraintAnnotation, constraint);
386
387 constraintAnnotation.addMember("test", "$S", constraint.getTest().getPath());
388
389 if (constraint.getMessage() != null) {
390 constraintAnnotation.addMember("message", "$S", constraint.getMessage());
391 }
392
393 MarkupMultiline remarks = constraint.getRemarks();
394 if (remarks != null) {
395 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
396 }
397
398 annotation.addMember("report", "$L", constraintAnnotation.build());
399 }
400 }
401
402 private static void applyIndexConstraints(
403 @NonNull AnnotationSpec.Builder annotation,
404 @NonNull List<? extends IIndexConstraint> constraints) {
405 for (IIndexConstraint constraint : constraints) {
406 assert constraint != null;
407
408 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Index.class);
409
410 buildConstraint(Index.class, constraintAnnotation, constraint);
411
412 constraintAnnotation.addMember("name", "$S", constraint.getName());
413
414 buildKeyFields(constraintAnnotation, constraint.getKeyFields());
415
416 String message = constraint.getMessage();
417 if (message != null) {
418 constraintAnnotation.addMember("message", "$S", message);
419 }
420
421 MarkupMultiline remarks = constraint.getRemarks();
422 if (remarks != null) {
423 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
424 }
425
426 annotation.addMember("index", "$L", constraintAnnotation.build());
427 }
428 }
429
430 private static void applyUniqueConstraints(
431 @NonNull AnnotationSpec.Builder annotation,
432 @NonNull List<? extends IUniqueConstraint> constraints) {
433 for (IUniqueConstraint constraint : constraints) {
434 assert constraint != null;
435
436 AnnotationSpec.Builder constraintAnnotation = ObjectUtils.notNull(AnnotationSpec.builder(IsUnique.class));
437
438 buildConstraint(IsUnique.class, constraintAnnotation, constraint);
439
440 buildKeyFields(constraintAnnotation, constraint.getKeyFields());
441
442 String message = constraint.getMessage();
443 if (message != null) {
444 constraintAnnotation.addMember("message", "$S", message);
445 }
446
447 MarkupMultiline remarks = constraint.getRemarks();
448 if (remarks != null) {
449 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
450 }
451
452 annotation.addMember("unique", "$L", constraintAnnotation.build());
453 }
454 }
455
456 private static void checkCardinalities(
457 @NonNull IAssemblyDefinition definition,
458 @NonNull ICardinalityConstraint constraint,
459 @NonNull ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet,
460 @NonNull LogBuilder logBuilder) {
461
462 LogBuilder warn = LOGGER.atWarn();
463 for (IDefinitionNodeItem<?, ?> item : instanceSet) {
464 INamedInstance instance = item.getInstance();
465 if (instance instanceof INamedModelInstanceAbsolute) {
466 INamedModelInstanceAbsolute modelInstance = (INamedModelInstanceAbsolute) instance;
467
468 checkMinOccurs(definition, constraint, modelInstance, logBuilder);
469 checkMaxOccurs(definition, constraint, modelInstance, logBuilder);
470 } else {
471 warn.log(String.format(
472 "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is not a model instance",
473 definition.getName(),
474 constraint.getMinOccurs(),
475 constraint.getTarget().getPath()));
476 }
477 }
478 }
479
480 private static void checkMinOccurs(
481 @NonNull IAssemblyDefinition definition,
482 @NonNull ICardinalityConstraint constraint,
483 @NonNull INamedModelInstanceAbsolute modelInstance,
484 @NonNull LogBuilder logBuilder) {
485 Integer minOccurs = constraint.getMinOccurs();
486 if (minOccurs != null) {
487 if (minOccurs == modelInstance.getMinOccurs()) {
488 logBuilder.log(String.format(
489 "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
490 + " targeted instance named '%s' that requires min-occurs=%d",
491 definition.getName(),
492 minOccurs,
493 constraint.getTarget().getPath(),
494 modelInstance.getName(),
495 modelInstance.getMinOccurs()));
496 } else if (minOccurs < modelInstance.getMinOccurs()) {
497 logBuilder.log(String.format(
498 "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
499 + " targeted instance named '%s' that requires min-occurs=%d",
500 definition.getName(),
501 minOccurs,
502 constraint.getTarget().getPath(),
503 modelInstance.getName(),
504 modelInstance.getMinOccurs()));
505 }
506 }
507 }
508
509 private static void checkMaxOccurs(
510 @NonNull IAssemblyDefinition definition,
511 @NonNull ICardinalityConstraint constraint,
512 @NonNull INamedModelInstanceAbsolute modelInstance,
513 @NonNull LogBuilder logBuilder) {
514 Integer maxOccurs = constraint.getMaxOccurs();
515 if (maxOccurs != null) {
516 if (maxOccurs == modelInstance.getMaxOccurs()) {
517 logBuilder.log(String.format(
518 "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
519 + " targeted instance named '%s' that requires max-occurs=%d",
520 definition.getName(),
521 maxOccurs,
522 constraint.getTarget().getPath(),
523 modelInstance.getName(),
524 modelInstance.getMaxOccurs()));
525 } else if (maxOccurs < modelInstance.getMaxOccurs()) {
526 logBuilder.log(String.format(
527 "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
528 + " targeted instance named '%s' that requires max-occurs=%d",
529 definition.getName(),
530 maxOccurs,
531 constraint.getTarget().getPath(),
532 modelInstance.getName(),
533 modelInstance.getMaxOccurs()));
534 }
535 }
536 }
537
538 private static void applyHasCardinalityConstraints(
539 @NonNull IAssemblyDefinition definition,
540 @NonNull AnnotationSpec.Builder annotation,
541 @NonNull List<? extends ICardinalityConstraint> constraints) {
542
543 DynamicContext dynamicContext = new DynamicContext();
544 dynamicContext.disablePredicateEvaluation();
545
546 for (ICardinalityConstraint constraint : constraints) {
547 assert constraint != null;
548
549 IAssemblyNodeItem definitionNodeItem
550 = INodeItemFactory.instance().newAssemblyNodeItem(definition);
551
552 ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet
553 = constraint.matchTargets(definitionNodeItem, dynamicContext);
554
555 if (LOGGER.isWarnEnabled()) {
556 checkCardinalities(definition, constraint, instanceSet, ObjectUtils.notNull(LOGGER.atWarn()));
557 }
558
559 AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(HasCardinality.class);
560
561 buildConstraint(HasCardinality.class, constraintAnnotation, constraint);
562
563 Integer minOccurs = constraint.getMinOccurs();
564 if (minOccurs != null && !minOccurs.equals(getDefaultValue(HasCardinality.class, "minOccurs"))) {
565 constraintAnnotation.addMember("minOccurs", "$L", minOccurs);
566 }
567
568 Integer maxOccurs = constraint.getMaxOccurs();
569 if (maxOccurs != null && !maxOccurs.equals(getDefaultValue(HasCardinality.class, "maxOccurs"))) {
570 constraintAnnotation.addMember("maxOccurs", "$L", maxOccurs);
571 }
572
573 annotation.addMember("cardinality", "$L", constraintAnnotation.build());
574
575 MarkupMultiline remarks = constraint.getRemarks();
576 if (remarks != null) {
577 constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
578 }
579 }
580 }
581 }