1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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   * A variety of utility functions for creating Module annotations.
64   */
65  @SuppressWarnings({
66      "PMD.GodClass", "PMD.CouplingBetweenObjects" // utility class
67  })
68  public final class AnnotationGenerator {
69    private static final Logger LOGGER = LogManager.getLogger(AnnotationGenerator.class);
70  
71    private AnnotationGenerator() {
72      // disable construction
73    }
74  
75    /**
76     * Get the default vale of the given member of an annotation.
77     *
78     * @param annotation
79     *          the annotation to analyze
80     * @param member
81     *          the annotation member to analyze
82     * @return the default value for the annotation member or {@code null} if there
83     *         is not default value
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        // no default value found
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    * Build a value constraints annotation.
128    *
129    * @param builder
130    *          the annotation builder
131    * @param definition
132    *          the definition to get the value constraints for
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    * Build a value constraints annotation.
156    *
157    * @param builder
158    *          the annotation builder
159    * @param definition
160    *          the definition to get the value constraints for
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    * Build an assembly constraints annotation.
191    *
192    * @param builder
193    *          the annotation builder
194    * @param definition
195    *          the definition to get the value constraints for
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 }