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