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.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.util.ObjectUtils;
36  import gov.nist.secauto.metaschema.databind.model.annotations.AllowedValue;
37  import gov.nist.secauto.metaschema.databind.model.annotations.AllowedValues;
38  import gov.nist.secauto.metaschema.databind.model.annotations.AssemblyConstraints;
39  import gov.nist.secauto.metaschema.databind.model.annotations.Expect;
40  import gov.nist.secauto.metaschema.databind.model.annotations.HasCardinality;
41  import gov.nist.secauto.metaschema.databind.model.annotations.Index;
42  import gov.nist.secauto.metaschema.databind.model.annotations.IndexHasKey;
43  import gov.nist.secauto.metaschema.databind.model.annotations.IsUnique;
44  import gov.nist.secauto.metaschema.databind.model.annotations.KeyField;
45  import gov.nist.secauto.metaschema.databind.model.annotations.Let;
46  import gov.nist.secauto.metaschema.databind.model.annotations.Matches;
47  import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints;
48  
49  import org.apache.logging.log4j.LogBuilder;
50  import org.apache.logging.log4j.LogManager;
51  import org.apache.logging.log4j.Logger;
52  
53  import java.lang.reflect.Method;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.regex.Pattern;
57  
58  import javax.xml.namespace.QName;
59  
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;
93      try {
94        retval = method.getDefaultValue();
95      } catch (TypeNotPresentException ex) {
96        retval = null; // NOPMD readability
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();
121     if (!target.equals(getDefaultValue(annotationType, "target"))) {
122       annotation.addMember("target", "$S", target);
123     }
124   }
125 
126   public static void buildValueConstraints(
127       @NonNull AnnotationSpec.Builder builder,
128       @NonNull IFlagDefinition definition) {
129 
130     Map<QName, ? extends ILet> lets = definition.getLetExpressions();
131     if (!lets.isEmpty() || !definition.getConstraints().isEmpty()) {
132       AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class);
133       assert annotation != null;
134 
135       applyLetAssignments(annotation, lets);
136       applyAllowedValuesConstraints(annotation, definition.getAllowedValuesConstraints());
137       applyIndexHasKeyConstraints(annotation, definition.getIndexHasKeyConstraints());
138       applyMatchesConstraints(annotation, definition.getMatchesConstraints());
139       applyExpectConstraints(annotation, definition.getExpectConstraints());
140 
141       builder.addMember("valueConstraints", "$L", annotation.build());
142     }
143   }
144 
145   public static void buildValueConstraints(
146       @NonNull AnnotationSpec.Builder builder,
147       @NonNull IModelDefinition definition) {
148 
149     Map<QName, ? extends ILet> lets = definition.getLetExpressions();
150     List<? extends IAllowedValuesConstraint> allowedValues = definition.getAllowedValuesConstraints();
151     List<? extends IIndexHasKeyConstraint> indexHasKey = definition.getIndexHasKeyConstraints();
152     List<? extends IMatchesConstraint> matches = definition.getMatchesConstraints();
153     List<? extends IExpectConstraint> expects = definition.getExpectConstraints();
154 
155     if (!lets.isEmpty() || !allowedValues.isEmpty() || !indexHasKey.isEmpty() || !matches.isEmpty()
156         || !expects.isEmpty()) {
157       AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class);
158       assert annotation != null;
159 
160       applyLetAssignments(annotation, lets);
161       applyAllowedValuesConstraints(annotation, allowedValues);
162       applyIndexHasKeyConstraints(annotation, indexHasKey);
163       applyMatchesConstraints(annotation, matches);
164       applyExpectConstraints(annotation, expects);
165 
166       builder.addMember("valueConstraints", "$L", annotation.build());
167     }
168   }
169 
170   public static void buildAssemblyConstraints(
171       @NonNull AnnotationSpec.Builder builder,
172       @NonNull IAssemblyDefinition definition) {
173 
174     List<? extends IIndexConstraint> index = definition.getIndexConstraints();
175     List<? extends IUniqueConstraint> unique = definition.getUniqueConstraints();
176     List<? extends ICardinalityConstraint> cardinality = definition.getHasCardinalityConstraints();
177 
178     if (!index.isEmpty() || !unique.isEmpty() || !cardinality.isEmpty()) {
179       AnnotationSpec.Builder annotation = ObjectUtils.notNull(AnnotationSpec.builder(AssemblyConstraints.class));
180 
181       applyIndexConstraints(annotation, index);
182       applyUniqueConstraints(annotation, unique);
183       applyHasCardinalityConstraints(definition, annotation, cardinality);
184 
185       builder.addMember("modelConstraints", "$L", annotation.build());
186     }
187   }
188 
189   private static void applyLetAssignments(
190       @NonNull AnnotationSpec.Builder annotation,
191       @NonNull Map<QName, ? extends ILet> lets) {
192     for (ILet let : lets.values()) {
193       AnnotationSpec.Builder letAnnotation = AnnotationSpec.builder(Let.class);
194       letAnnotation.addMember("name", "$S", let.getName());
195       letAnnotation.addMember("target", "$S", let.getValueExpression().getPath());
196 
197       // TODO: Support remarks
198       // MarkupMultiline remarks = let.getRemarks();
199       // if (remarks != null) {
200       // constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
201       // }
202 
203       annotation.addMember("lets", "$L", letAnnotation.build());
204     }
205   }
206 
207   private static void applyAllowedValuesConstraints(
208       @NonNull AnnotationSpec.Builder annotation,
209       @NonNull List<? extends IAllowedValuesConstraint> constraints) {
210     for (IAllowedValuesConstraint constraint : constraints) {
211       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(AllowedValues.class);
212       buildConstraint(AllowedValues.class, constraintAnnotation, constraint);
213 
214       boolean isAllowedOther = constraint.isAllowedOther();
215       if (isAllowedOther != (boolean) getDefaultValue(AllowedValues.class, "allowOthers")) {
216         constraintAnnotation.addMember("allowOthers", "$L", isAllowedOther);
217       }
218 
219       for (IAllowedValue value : constraint.getAllowedValues().values()) {
220         AnnotationSpec.Builder valueAnnotation = AnnotationSpec.builder(AllowedValue.class);
221 
222         valueAnnotation.addMember("value", "$S", value.getValue());
223         valueAnnotation.addMember("description", "$S", value.getDescription().toMarkdown());
224 
225         constraintAnnotation.addMember("values", "$L", valueAnnotation.build());
226       }
227 
228       MarkupMultiline remarks = constraint.getRemarks();
229       if (remarks != null) {
230         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
231       }
232       annotation.addMember("allowedValues", "$L", constraintAnnotation.build());
233     }
234   }
235 
236   private static void applyIndexHasKeyConstraints(
237       @NonNull AnnotationSpec.Builder annotation,
238       @NonNull List<? extends IIndexHasKeyConstraint> constraints) {
239     for (IIndexHasKeyConstraint constraint : constraints) {
240       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(IndexHasKey.class);
241       buildConstraint(IndexHasKey.class, constraintAnnotation, constraint);
242 
243       constraintAnnotation.addMember("indexName", "$S", constraint.getIndexName());
244 
245       buildKeyFields(constraintAnnotation, constraint.getKeyFields());
246 
247       MarkupMultiline remarks = constraint.getRemarks();
248       if (remarks != null) {
249         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
250       }
251 
252       annotation.addMember("indexHasKey", "$L", constraintAnnotation.build());
253     }
254   }
255 
256   private static void buildKeyFields(
257       @NonNull Builder constraintAnnotation,
258       @NonNull List<? extends IKeyField> keyFields) {
259     for (IKeyField key : keyFields) {
260       AnnotationSpec.Builder keyAnnotation = AnnotationSpec.builder(KeyField.class);
261 
262       String target = key.getTarget();
263       if (!target.equals(getDefaultValue(KeyField.class, "target"))) {
264         keyAnnotation.addMember("target", "$S", target);
265       }
266 
267       Pattern pattern = key.getPattern();
268       if (pattern != null) {
269         keyAnnotation.addMember("pattern", "$S", pattern.pattern());
270       }
271 
272       MarkupMultiline remarks = key.getRemarks();
273       if (remarks != null) {
274         keyAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
275       }
276 
277       constraintAnnotation.addMember("keyFields", "$L", keyAnnotation.build());
278     }
279   }
280 
281   private static void applyMatchesConstraints(
282       @NonNull AnnotationSpec.Builder annotation,
283       @NonNull List<? extends IMatchesConstraint> constraints) {
284     for (IMatchesConstraint constraint : constraints) {
285       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Matches.class);
286       buildConstraint(Matches.class, constraintAnnotation, constraint);
287 
288       Pattern pattern = constraint.getPattern();
289       if (pattern != null) {
290         constraintAnnotation.addMember("pattern", "$S", pattern.pattern());
291       }
292 
293       IDataTypeAdapter<?> dataType = constraint.getDataType();
294       if (dataType != null) {
295         constraintAnnotation.addMember("typeAdapter", "$T.class", dataType.getClass());
296       }
297 
298       MarkupMultiline remarks = constraint.getRemarks();
299       if (remarks != null) {
300         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
301       }
302       annotation.addMember("matches", "$L", constraintAnnotation.build());
303     }
304   }
305 
306   private static void applyExpectConstraints(
307       @NonNull AnnotationSpec.Builder annotation,
308       @NonNull List<? extends IExpectConstraint> constraints) {
309     for (IExpectConstraint constraint : constraints) {
310       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Expect.class);
311 
312       buildConstraint(Expect.class, constraintAnnotation, constraint);
313 
314       constraintAnnotation.addMember("test", "$S", constraint.getTest());
315 
316       if (constraint.getMessage() != null) {
317         constraintAnnotation.addMember("message", "$S", constraint.getMessage());
318       }
319 
320       MarkupMultiline remarks = constraint.getRemarks();
321       if (remarks != null) {
322         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
323       }
324 
325       annotation.addMember("expect", "$L", constraintAnnotation.build());
326     }
327   }
328 
329   private static void applyIndexConstraints(
330       @NonNull AnnotationSpec.Builder annotation,
331       @NonNull List<? extends IIndexConstraint> constraints) {
332     for (IIndexConstraint constraint : constraints) {
333       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Index.class);
334 
335       buildConstraint(Index.class, constraintAnnotation, constraint);
336 
337       constraintAnnotation.addMember("name", "$S", constraint.getName());
338 
339       buildKeyFields(constraintAnnotation, constraint.getKeyFields());
340 
341       MarkupMultiline remarks = constraint.getRemarks();
342       if (remarks != null) {
343         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
344       }
345 
346       annotation.addMember("index", "$L", constraintAnnotation.build());
347     }
348   }
349 
350   private static void applyUniqueConstraints(
351       @NonNull AnnotationSpec.Builder annotation,
352       @NonNull List<? extends IUniqueConstraint> constraints) {
353     for (IUniqueConstraint constraint : constraints) {
354       AnnotationSpec.Builder constraintAnnotation = ObjectUtils.notNull(AnnotationSpec.builder(IsUnique.class));
355 
356       buildConstraint(IsUnique.class, constraintAnnotation, constraint);
357 
358       buildKeyFields(constraintAnnotation, constraint.getKeyFields());
359 
360       MarkupMultiline remarks = constraint.getRemarks();
361       if (remarks != null) {
362         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
363       }
364 
365       annotation.addMember("unique", "$L", constraintAnnotation.build());
366     }
367   }
368 
369   @SuppressWarnings({
370       "PMD.GuardLogStatement" // guarded in outer calls
371   })
372   private static void checkCardinalities(
373       @NonNull IAssemblyDefinition definition,
374       @NonNull ICardinalityConstraint constraint,
375       @NonNull ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet,
376       @NonNull LogBuilder logBuilder) {
377 
378     LogBuilder warn = LOGGER.atWarn();
379     for (IDefinitionNodeItem<?, ?> item : instanceSet.getValue()) {
380       INamedInstance instance = item.getInstance();
381       if (instance instanceof INamedModelInstanceAbsolute) {
382         INamedModelInstanceAbsolute modelInstance = (INamedModelInstanceAbsolute) instance;
383 
384         checkMinOccurs(definition, constraint, modelInstance, logBuilder);
385         checkMaxOccurs(definition, constraint, modelInstance, logBuilder);
386       } else {
387         warn.log(String.format(
388             "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is not a model instance",
389             definition.getName(), constraint.getMinOccurs(), constraint.getTarget()));
390       }
391     }
392   }
393 
394   @SuppressWarnings({
395       "PMD.GuardLogStatement" // guarded in outer calls
396   })
397   private static void checkMinOccurs(
398       @NonNull IAssemblyDefinition definition,
399       @NonNull ICardinalityConstraint constraint,
400       @NonNull INamedModelInstanceAbsolute modelInstance,
401       @NonNull LogBuilder logBuilder) {
402     Integer minOccurs = constraint.getMinOccurs();
403     if (minOccurs != null) {
404       if (minOccurs == modelInstance.getMinOccurs()) {
405         logBuilder.log(String.format(
406             "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
407                 + " targeted instance named '%s' that requires min-occurs=%d",
408             definition.getName(), minOccurs, constraint.getTarget(),
409             modelInstance.getName(),
410             modelInstance.getMinOccurs()));
411       } else if (minOccurs < modelInstance.getMinOccurs()) {
412         logBuilder.log(String.format(
413             "Definition '%s' has min-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
414                 + " targeted instance named '%s' that requires min-occurs=%d",
415             definition.getName(), minOccurs, constraint.getTarget(),
416             modelInstance.getName(),
417             modelInstance.getMinOccurs()));
418       }
419     }
420   }
421 
422   @SuppressWarnings({
423       "PMD.GuardLogStatement" // guarded in outer calls
424   })
425   private static void checkMaxOccurs(
426       @NonNull IAssemblyDefinition definition,
427       @NonNull ICardinalityConstraint constraint,
428       @NonNull INamedModelInstanceAbsolute modelInstance,
429       @NonNull LogBuilder logBuilder) {
430     Integer maxOccurs = constraint.getMaxOccurs();
431     if (maxOccurs != null) {
432       if (maxOccurs == modelInstance.getMaxOccurs()) {
433         logBuilder.log(String.format(
434             "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that is redundant with a"
435                 + " targeted instance named '%s' that requires max-occurs=%d",
436             definition.getName(), maxOccurs, constraint.getTarget(),
437             modelInstance.getName(),
438             modelInstance.getMaxOccurs()));
439       } else if (maxOccurs < modelInstance.getMaxOccurs()) {
440         logBuilder.log(String.format(
441             "Definition '%s' has max-occurs=%d cardinality constraint targeting '%s' that conflicts with a"
442                 + " targeted instance named '%s' that requires max-occurs=%d",
443             definition.getName(), maxOccurs, constraint.getTarget(),
444             modelInstance.getName(),
445             modelInstance.getMaxOccurs()));
446       }
447     }
448   }
449 
450   private static void applyHasCardinalityConstraints(
451       @NonNull IAssemblyDefinition definition,
452       @NonNull AnnotationSpec.Builder annotation,
453       @NonNull List<? extends ICardinalityConstraint> constraints) {
454 
455     DynamicContext dynamicContext = new DynamicContext();
456     dynamicContext.disablePredicateEvaluation();
457 
458     for (ICardinalityConstraint constraint : constraints) {
459 
460       IAssemblyNodeItem definitionNodeItem
461           = INodeItemFactory.instance().newAssemblyNodeItem(definition);
462 
463       ISequence<? extends IDefinitionNodeItem<?, ?>> instanceSet
464           = constraint.matchTargets(definitionNodeItem, dynamicContext);
465 
466       if (LOGGER.isWarnEnabled()) {
467         checkCardinalities(definition, constraint, instanceSet, ObjectUtils.notNull(LOGGER.atWarn()));
468       }
469 
470       AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(HasCardinality.class);
471 
472       buildConstraint(HasCardinality.class, constraintAnnotation, constraint);
473 
474       Integer minOccurs = constraint.getMinOccurs();
475       if (minOccurs != null && !minOccurs.equals(getDefaultValue(HasCardinality.class, "minOccurs"))) {
476         constraintAnnotation.addMember("minOccurs", "$L", minOccurs);
477       }
478 
479       Integer maxOccurs = constraint.getMaxOccurs();
480       if (maxOccurs != null && !maxOccurs.equals(getDefaultValue(HasCardinality.class, "maxOccurs"))) {
481         constraintAnnotation.addMember("maxOccurs", "$L", maxOccurs);
482       }
483 
484       annotation.addMember("cardinality", "$L", constraintAnnotation.build());
485 
486       MarkupMultiline remarks = constraint.getRemarks();
487       if (remarks != null) {
488         constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
489       }
490     }
491   }
492 }